i saw this thread Swift 5.5 Concurrency: how to serialize async Tasks to replace an OperationQueue with maxConcurrentOperationCount = 1?
but i am not clear on whether two scheduled Tasks will execute serially. What i mean is, if i had a piece of code
func fetchImages() {
Task.init {
let fetch = await loadImages()
}
Task.init {
let fetch1 = await loadImages1()
}
}
will first task always finish before second task starts? The application i am trying to get is to execute task 1 as soon as possible (save time) but task 2 relies on the result of task 1 so it needs to wait for task 1 to finish before proceeding. Task 2 also is only conditionally triggered upon an event so they cannot be in the same Task.
You asked:
will first task always finish before second task starts?
No, it will not. Whenever you see await, that is a “suspension point” at which Swift concurrency is free to switch to another task. In short, these can run concurrently. Let me illustrate that with Xcode Instruments:
import os.log
private let log = OSLog(subsystem: "Test", category: .pointsOfInterest)
class Foo {
func fetchImages() {
Task {
let fetch = await loadImages()
print("done loadImages")
}
Task {
let fetch1 = await loadImages1()
print("done loadImages1")
}
}
func loadImages() async {
// start “points of interest” interval
let id = OSSignpostID(log: log)
os_signpost(.begin, log: log, name: #function, signpostID: id, "start")
// perform 3-second asynchronous task
try? await Task.sleep(nanoseconds: 3 * NSEC_PER_SEC)
// end “points of interest” interval
os_signpost(.end, log: log, name: #function, signpostID: id, "end")
}
func loadImages1() async {
// start “points of interest” interval
let id = OSSignpostID(log: log)
os_signpost(.begin, log: log, name: #function, signpostID: id, "start")
// perform 1-second asynchronous task
try? await Task.sleep(nanoseconds: 1 * NSEC_PER_SEC)
// end “points of interest” interval
os_signpost(.end, log: log, name: #function, signpostID: id, "end")
}
}
Profiling (in Xcode, either press command-i or choose from the menu, “Product” » “Profile”) this with the “time profiler” in Instruments, you can see that these run concurrently:
The trick is to have the second task await the first one. E.g.,
func fetchImages() {
let firstTask = Task {
let fetch = await loadImages()
print("done loadImages")
}
Task {
_ = await firstTask.result
let fetch1 = await loadImages1()
print("done loadImages1")
}
}
Or you can store the first task in some property:
var firstTask: Task<Void, Never>? // you may need to adjust the `Success` and `Failure` types to match your real example
func fetchImages() {
firstTask = Task {
let fetch = await loadImages()
print("done loadImages")
}
Task {
_ = await firstTask?.result
let fetch1 = await loadImages1()
print("done loadImages1")
}
}
When you do that, you can see the sequential execution:
FWIW, this concept, of using await on the prior task was the motivating idea behind that other answer that you referenced. That is a more generalized rendition of the above. Hopefully this illustrates the mechanism outlined in that other answer.
Related
I created a function to add my course events to the calendar app using EventKit.
After learning the swift concurrency, I want to update my code to make the progress much faster, namely using the detached task or TaskGroup to add these events.
Synchronize code without detached task or task group:
func export_test() {
Task.detached {
for i in 0...15 {
print("Task \(i): Start")
let courseEvent = EKEvent(eventStore: eventStore)
courseEvent.title = "TEST"
courseEvent.location = "TEST LOC"
courseEvent.startDate = .now
courseEvent.endDate = .now.addingTimeInterval(3600)
courseEvent.calendar = eventStore.defaultCalendarForNewEvents
courseEvent.addRecurrenceRule(EKRecurrenceRule(recurrenceWith: .daily, interval: 1, end: nil))
do {
try eventStore.save(courseEvent, span: .futureEvents)
} catch { print(error.localizedDescription) }
print("Task \(i): Finished")
}
}
}
Doing the same thing using the TaskGroup :
func export_test() {
Task.detached {
await withTaskGroup(of: Void.self) { group in
for i in 0...15 {
group.addTask {
print("Task \(i): Start")
let courseEvent = EKEvent(eventStore: eventStore)
courseEvent.title = "TEST"
courseEvent.location = "TEST LOC"
courseEvent.startDate = .now
courseEvent.endDate = .now.addingTimeInterval(3600)
courseEvent.calendar = eventStore.defaultCalendarForNewEvents
courseEvent.addRecurrenceRule(EKRecurrenceRule(recurrenceWith: .daily, interval: 1, end: nil))
do {
try eventStore.save(courseEvent, span: .futureEvents)
} catch { print(error.localizedDescription) }
print("Task \(i): Finished")
}
}
}
}
}
The output of the TaskGroup version:
Task 0: Start
Task 1: Start
Task 2: Start
Task 4: Start
Task 3: Start
Task 5: Start
Task 6: Start
Task 7: Start
Task 0: Finished
Task 8: Start
Task 1: Finished
Task 9: Start
Sometimes, only a few tasks will been done, and others will not, or even never been started (I created 16 tasks but only printed 9 in this example). Sometimes, all of these events can be added.
In my point of view, I have created 16 child tasks in the TaskGroup.
Each child task will add one event to the Calendar. I think in this way, I can take the full advantage of the multi-core performance (maybe it's actually not. 🙃)
If I put the for-loop inside the group.addTask closure, it will always have the expected result, but in this way, we only have a single loop so the TaskGroup may no longer needed.
I'm really exhausted🙃🙃.
snapshot:
Snapshot
To see all status messages that the tasks have finished you have to await each result and print it there
func export_test() {
Task.detached {
await withTaskGroup(of: String.self) { group in
for i in 0...15 {
group.addTask {
print("Task \(i): Start")
let courseEvent = EKEvent(eventStore: eventStore)
courseEvent.title = "TEST"
courseEvent.location = "TEST LOC"
courseEvent.startDate = .now
courseEvent.endDate = .now.addingTimeInterval(3600)
courseEvent.calendar = eventStore.defaultCalendarForNewEvents
courseEvent.addRecurrenceRule(EKRecurrenceRule(recurrenceWith: .daily, interval: 1, end: nil))
do {
try eventStore.save(courseEvent, span: .futureEvents)
} catch { print(error.localizedDescription) }
return "Task \(i): Finished"
}
}
for await taskStatus in group {
print(taskStatus)
}
}
}
}
After a long time researching every aspect of the code. I found that the problem is try eventStore.save(courseEvent, span: .futureEvents).
Alternatively, I use try eventStore.save(..., span: ..., commit: false) and eventStore.commit() to solve the problem.
I think this is caused by the data races because I'm using swift concurrency. While one event is saving, another one calls save method to save again, leading to data conflicts (just my guess.)
To solve this, luckily, we can commit a batch of events later using eventStore.commit() to avoid data conflicts. The result is what I expected !!🥳
And after that optimization, the performance of this function is up to 25% faster (exactly 136ms faster). (haha. Perfect.)
Final Code (in Swift 5.7):
func export_test() {
Task.detached {
await withTaskGroup(of: Void.self) { group in
for i in 0...15 {
group.addTask {
print("Task \(i): Start")
let courseEvent = EKEvent(eventStore: eventStore)
courseEvent.title = "TEST"
courseEvent.location = "TEST LOC"
courseEvent.startDate = .now
courseEvent.endDate = .now.addingTimeInterval(3600)
courseEvent.calendar = eventStore.defaultCalendarForNewEvents
courseEvent.addRecurrenceRule(EKRecurrenceRule(recurrenceWith: .daily, interval: 1, end: nil))
try eventStore.save(courseEvent, span: .futureEvents, commit: false)
}
}
}
eventStore.commit()
}
}
How do you write a unit test that checks whether an async function doesn't timeout?
I'm trying with regular XCTestExpectation, but because await suspends everything, it can't wait for the expectation.
In the code below, I'm checking that loader.perform() doesn't take more than 1 second to execute.
func testLoaderSuccess() async throws {
let expectation = XCTestExpectation(description: "doesn't timeout")
let result = try await loader.perform()
XCTAssert(result.value == 42)
wait(for: [expectation], timeout: 1) // execution never gets here
expectation.fulfill()
}
It might be prudent to cancel the task if it times out:
func testA() async throws {
let expectation = XCTestExpectation(description: "timeout")
let task = Task {
let result = try await loader.perform()
XCTAssertEqual(result, 42)
expectation.fulfill()
}
wait(for: [expectation], timeout: 1)
task.cancel()
}
If you do not, perform may continue to run even after testA finishes in the failure scenario.
The other approach would be to use a task group:
func testB() async throws {
try await withThrowingTaskGroup(of: Void.self) { group in
group.addTask {
let result = try await self.loader.perform()
XCTAssertEqual(result, 42)
}
group.addTask {
try await Task.sleep(nanoseconds: 1 * NSEC_PER_SEC)
XCTFail("Timed out")
}
let _ = try await group.next() // wait for the first one
group.cancelAll() // cancel the other one
}
}
You need to structure this in a different way.
You need to create a new Task. In this Task execute and await the async code. After awaiting fulfill the expectation.
Your code did not work because the Thread the Test runs on will stop at wait(for: for the expectation to fulfill, what it never does as the line comes after wait(for:.
func testLoaderSuccess() throws {
let expectation = XCTestExpectation(description: "doesn't timeout")
Task{
try await Task.sleep(nanoseconds: 500_000_000)
expectation.fulfill()
}
wait(for: [expectation], timeout: 1)
// Assertions here because only then is assured that
// everything completed
}
The sequence that worked for me both locally and on CI is the following:
func testLoaderSuccess() async throws {
Task {
let result = try await loader.perform()
XCTAssert(result.value == 42)
expectation.fulfill()
}
wait(for: [expectation], timeout: 1)
}
I need a serial queue that instead of building up a backlog of tasks, only performs the task it's doing and the latest one queued since that started. Any job waiting in the queue for execution should be discarded if a new one comes in before it starts. Been trying to make it work using actor, async and await as follows but it's a bit advanced considering I only learned this stuff today. Is this close?
actor Worker {
var task: Task <Void, Never>? = nil
var next: Item? = nil
var latestResult = false
func analyseItem(_ item: Item) async -> Bool {
// make our item next (but could be overwritten)
next = item
// let anything that's running complete
await task?.value
// start a task for the latest request
task = Task {
latestResult = next?.processItem()
task = nil
}
return latestResult
}
}
I made a small project in Playgrounds
import Combine
import Foundation
import _Concurrency
actor Analyzer {
var timestamp = Date()
var flag = false
func analyzeData(timestamp: Date) async throws {
if flag { return }
flag = true
print("analyzing #\(timestamp)...")
try await Task.sleep(nanoseconds: 3_000_000_000)
print("analyzing #\(timestamp) complete.")
flag = false
}
}
let analyzer = Analyzer()
var cancellable: AnyCancellable
cancellable = Timer.publish(every: 2, on: .main, in: .default)
.autoconnect()
.receive(on: DispatchQueue.main)
.map({ output -> Date in
Task {
try? await analyzer.analyzeData(timestamp: output)
}
return output
})
.sink(receiveValue: { date in
// print(date)
})
Since flag is isolated, tasks will be analyzed every 4 seconds, because the timers fired every 2 and tasks last 3 seconds.
I'd like to create an async function which itself using async calls. I also want to ensure that only one call is actively processed in any moment. So I want an async #synchronized function.
How to do that? Wrapping the function's body inside the dispatchQueue.sync {} does not work as it expects synchronised code. Also it seems that DispatchQueue in general is not designed to have async code blocks / tasks to execute.
This code communicates with hardware, so async in nature, that's why I want an async interface for my library. (I don't want to block the app while the stages of communication happen.) But certain operations can't be executed parallel on the hardware, so I have to go through synchronisation so the certain operations won't happen at the same time.
You can have every Task await the prior one. And you can use actor make sure that you are only running one at a time. The trick is, because of actor reentrancy, you have to put that "await prior Task" logic in a synchronous method.
E.g., you can do:
actor Experiment {
private var previousTask: Task<Void, Error>?
func startSomethingAsynchronous() {
previousTask = Task { [previousTask] in
let _ = await previousTask?.result
try await self.doSomethingAsynchronous()
}
}
private func doSomethingAsynchronous() async throws {
let id = OSSignpostID(log: log)
os_signpost(.begin, log: log, name: "Task", signpostID: id, "Start")
try await Task.sleep(nanoseconds: 2 * NSEC_PER_SEC)
os_signpost(.end, log: log, name: "Task", signpostID: id, "End")
}
}
Now I am using os_signpost so I can watch this serial behavior from Xcode Instruments. Anyway, you could start three tasks like so:
import os.log
private let log = OSLog(subsystem: "Experiment", category: .pointsOfInterest)
class ViewController: NSViewController {
let experiment = Experiment()
func startExperiment() {
for _ in 0 ..< 3 {
Task { await experiment.startSomethingAsynchronous() }
}
os_signpost(.event, log: log, name: "Done starting tasks")
}
...
}
And Instruments can visually demonstrate the sequential behavior (where the ⓢ shows us where the submitting of all the tasks finished), but you can see the sequential execution of the tasks on the timeline:
I actually like to abstract this serial behavior into its own type:
actor SerialTasks<Success> {
private var previousTask: Task<Success, Error>?
func add(block: #Sendable #escaping () async throws -> Success) {
previousTask = Task { [previousTask] in
let _ = await previousTask?.result
return try await block()
}
}
}
And then the asynchronous function for which you need this serial behavior would use the above, e.g.:
class Experiment {
let serialTasks = SerialTasks<Void>()
func startSomethingAsynchronous() async {
await serialTasks.add {
try await self.doSomethingAsynchronous()
}
}
private func doSomethingAsynchronous() async throws {
let id = OSSignpostID(log: log)
os_signpost(.begin, log: log, name: "Task", signpostID: id, "Start")
try await Task.sleep(nanoseconds: 2 * NSEC_PER_SEC)
os_signpost(.end, log: log, name: "Task", signpostID: id, "End")
}
}
The accepted answer does indeed make tasks run serially but it does not wait for a task to be finished and does not propagate errors. I use the following alternative to support the catching of errors.
actor SerialTaskQueue<T> {
private let dispatchQueue: DispatchQueue
init(label: String) {
dispatchQueue = DispatchQueue(label: label)
}
#discardableResult
func add(block: #Sendable #escaping () async throws -> T) async throws -> T {
try await withCheckedThrowingContinuation { continuation in
dispatchQueue.sync {
let semaphore = DispatchSemaphore(value: 0)
Task {
defer {
semaphore.signal()
}
do {
let result = try await block()
continuation.resume(returning: result)
} catch {
continuation.resume(throwing: error)
}
}
semaphore.wait()
}
}
}
}
I'm trying to make sure I understand the behavior of await. Suppose we have the following functions:
func do() async {
//code
}
func stuff() async {
//code
}
The following statements will run do and stuff sequentially:
await do()
await stuff()
But the following statement will run do and stuff in parallel correct?
await (do(), stuff())
I'm not sure how to check in Xcode if my code runs in parallel or in sequence.
Short answer:
If you want concurrent execution, either use async let pattern or a task group.
Long answer:
You said:
But the following statement will run do and stuff in parallel correct?
await (do(), stuff())
No, they will not.
This is best illustrated empirically by:
Make the task take enough time that concurrency behavior can easily be manifested; and
Use “Points of Interest” instrument (e.g., by picking the “Time Profiler” template) in Instruments to graphically represent intervals graphically over time.
Consider this code, using the tuple approach:
import os.log
private let log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: .pointsOfInterest)
And:
func example() async {
let values = await (self.doSomething(), self.doSomethingElse())
print(values)
}
func doSomething() async -> Int {
spin(#function)
return 1
}
func doSomethingElse() async -> Int {
spin(#function)
return 42
}
func spin(_ name: StaticString) {
let id = OSSignpostID(log: log)
os_signpost(.begin, log: log, name: name, signpostID: id, "begin")
let start = CACurrentMediaTime()
while CACurrentMediaTime() - start < 1 { } // spin for one second
os_signpost(.end, log: log, name: name, signpostID: id, "end")
}
That results in a graph that shows that it is not happening concurrently:
Whereas:
func example() async {
async let foo = self.doSomething()
async let bar = self.doSomethingElse()
let values = await (foo, bar)
print(values)
}
That does result in concurrent execution:
Now, in the above examples, I changed the functions so that they returned values (as that is really the only context where using tuples makes any practical sense).
But if they did not return values and you wanted them to run in parallel, you might use a task group:
func experiment() async {
await withTaskGroup(of: Void.self) { group in
group.addTask { await self.doSomething() }
group.addTask { await self.doSomethingElse() }
}
}
func doSomething() async {
spin(#function)
}
func doSomethingElse() async {
spin(#function)
}
That results in the same graph where they run in parallel.
You can also just create Task instances and then await them:
func experiment() async {
async let task1 = Task { await doSomething() }
async let task2 = Task { await doSomethingElse() }
_ = await task1
_ = await task2
}
But task groups offer greater flexibility when the number of tasks being created may not be known at compile-time.
It seems that in order to get concurrent execution you must have an explicit async let for each function:
actor A {
var t = 1
func tt() -> Int {
for i in 0 ... 1000000 {
t += i
}
let s = t
t = 1
return s
}
}
var a = A()
var b = A()
func go() {
Task {
var d = Date()
await (a.tt(), b.tt())
print("time=1",d.timeIntervalSinceNow)
d = Date()
await a.tt()
await b.tt()
print("time2=",d.timeIntervalSinceNow)
d = Date()
async let q = (a.tt(), b.tt())
await q
print("time3=",d.timeIntervalSinceNow)
d = Date()
async let q1 = a.tt()
async let q2 = b.tt()
await q1
await q2
print("time4=",d.timeIntervalSinceNow)
d = Date()
async let q3 = a.tt()
async let q4 = b.tt()
await (q3, q4)
print("time5=",d.timeIntervalSinceNow)
}
}
printout:
time1= -0.4335060119628906
time2= -0.435217022895813
time3= -0.4430699348449707
time4= -0.23430800437927246
time5= -0.23900198936462402