How to run an #MainActor func synchronously? - swift

I have an #MainActor function that I want to run synchronously. So far, I've come up with this:
static func runOnMainThreadAndWait<T>(
action: #MainActor #Sendable () throws -> T
) rethrows -> T {
if Thread.isMainThread {
return try unsafeExecuteWithoutActorChecking(action)
} else {
return try DispatchQueue.main.sync(execute: action)
}
}
#preconcurrency
private static func unsafeExecuteWithoutActorChecking<T>(
_ action: () throws -> T
) rethrows -> T {
return try action()
}
This works, but the first case of that if statement feels janky, and with -warn-concurrency even unsafeExecuteWithoutActorChecking(_:) doesn't suffice:
Converting function value of type '#MainActor #Sendable () throws -> T' to '() throws -> T' loses global actor 'MainActor'
What's the safest way to implement this call?
Edit, for more details:
The function is usually called from the main thread -- specifically, when a view is created -- and the return value is needed to render the views. It was originally written under the assumption that this would be the only way it was called, so no safety mechanisms were put in place. I have been going back through the code and adding #MainActor annotations and similar safety checks where appropriate. Here, I need to support the common case (calling it synchronously while on the main thread) while making the incorrect behavior (running on a background thread) impossible. I added an asynchronous overload that calls await MainActor.run ... as a stopgap measure, but most of the time awaiting it is unnecessary and only adds noise.
I've seen a variation on this solution from before Swift's structured concurrency:
static func runOnMainThreadSync<T>(action: () throws -> T) rethrows -> T {
if Thread.isMainThread {
return try action()
} else {
return try DispatchQueue.main.sync(execute: action)
}
}
and I thought I could just add #MainActor to this.

Running something in the #MainActor context does not mean that you are running on the main thread.
It does mean that your code will run synchronously relative to other code running on the Main Actor, which includes code running in the main thread. But all that means is that while your code is running in the main actor context, if it's not running on the main thread, then the main thread will be suspended. (no other main actor code will be running in parallel)
So your if check is not just janky, it's bogus.
You don't say much about the calling context where you are trying to run something on the main actor, but the easiest solution is probably to use
await MainActor.run {
... some code here ...
}

You said:
I've seen a variation on this solution from before Swift's structured concurrency:
static func runOnMainThreadSync<T>(action: () throws -> T) rethrows -> T >{
if Thread.isMainThread {
return try action()
} else {
return try DispatchQueue.main.sync(execute: action)
}
}
and I thought I could just add #MainActor to this.
You can (and simplify it significantly):
extension MainActor {
#MainActor static func runOnMainThread<T>(action: #MainActor #Sendable () throws -> T) rethrows -> T {
try action()
}
}
But I would advise against this. One could use MainActor.run.
Or, even better, we can just mark the methods that must be run on a particular actor, and the compiler will perform compile-time checks (rather than the above run-time checks). If you call it from a context not isolated to the particular actor the compiler will tell you that you have to await. And only if you have a series of await suspension points to the main actor and would like to wrap that in a single task would you ever need to use MainActor.run.
But with Swift concurrency, there is no guessing, like we might have had to do in GCD. Nowadays, this runOnMainThreadSync pattern (anti-pattern?) is unnecessary. In Swift concurrency, we are generally explicit about which methods and properties are actor isolated, eliminating the ambiguity.

Related

Swift and Core Data: calling 'await perform' inside another 'await perform'

I am using the iOS15 Swift + Core Data code for async/await, and while using private queue concurrency, I am trying to call a await perform block inside another await perform block:
await privateMainContext.perform({
var displayName: String? = nil
await self.managedObjectContext?.perform {
displayName = self.displayName
}
// ... use displayName for privateContext object
})
But I get an error with the compiler that says:
Cannot pass function of type '() async -> ()' to parameter expecting
synchronous function type
If I remove the await from the inner perform call, it works OK, but doesn't that mean the self.managedObjectContext will call that in a block that might not execute before I need to use the displayName property? With old Objective-C code, I would call performBlockAndWait within another context's performBlockAndWait block and it worked fine.
The fundamental problem here is that the "perform" function is declared like this:
func perform<T>(schedule: NSManagedObjectContext.ScheduledTaskType,
_ block: #escaping () throws -> T) async rethrows -> T
And the relevant part to your question is block: #escaping () throws -> T. It takes a block that has to be synchronous code (the code in the block can't be suspended).
If it could suspend it would be:
block: #escaping () async throws -> T
(note the addition of async)
To solve the problem your block has to be synchronous code. That's what happens when you remove the await as you've noticed. Now you're going to be calling the perform method of the managed context whose Swift representation is:
func perform(_ block: #escaping () -> Void)
which is a synchronous call, but it runs its block asyncronously using GCD and you are right to be concerned.
The only synchronous call that also runs the block synchronously is performAndWait. So to solve your problem you can use:
await privateMainContext.perform({
var displayName: String? = nil
self.managedObjectContext?.performAndWait {
displayName = self.displayName
}
// ... use displayName for privateContext object
})
The only goal no served by this code is the apparent desire to convert all this code to use Swift Concurrency to the exclusion of any GCD. But there are plenty of examples where the GCD and Swift concurrency models are not equivalent. For example, it's straightforward to create a bunch of blocks and ensure they run one after the other by putting them into a serial queue. It's not easy to create plan a bunch of Tasks and coordinate them so that they run sequentially.

What's difference between `add(_)` and `add(_) async`?

I don't understand what's the difference between add(_) and add(_) async method. Like the below code, the MyActor has two add methods and one of them uses async keyword. They can exist at the same time. If I comment out the second add method it will output AAAA. If both add methods exist at the same time, output "BBBBB"。
actor MyActor {
var num: Int = 0
func add(_ value: Int) {
print("AAAAA")
num += value
}
func add(_ value: Int) async {
print("BBBBB")
num += value
}
}
let actor = MyActor()
Task {
await actor.add(200)
print(await actor.num)
}
Supplementary:
With the second add method commented out, I defined let actor = MyActor() outside Task and I noticed the add method signed as add(_:). If move let actor = MyActor() inside Task the add method signed as add(_:) async
The difference emerges inside the actor, for example
actor MyActor {
func addSync(_ value: Int) {
}
func addAsync(_ value: Int) async {
}
func f() {
addSync(0)
}
func g() async {
addSync(0)
await addAsync(0)
}
}
In the actor method f and g you can call addSync synchronously. While outside the actor, you need always call an actor method with the await keyword as if the method is asynchronous:
func outside() async {
let actor = MyActor()
await actor.addSync(0)
}
Async in Swift allows for structured concurrency, which will improve the readability of complex asynchronous code. Completion closures are no longer needed, and calling into multiple asynchronous methods after each other is a lot more readable.
Async stands for asynchronous and can be seen as a method attribute making it clear that a method performs asynchronous work. An example of such a method looks as follows:
func fetchImages() async throws -> [UIImage] {
// .. perform data request
}
The fetchImages method is defined as async throwing, which means that it’s performing a failable asynchronous job. The method would return a collection of images if everything went well or throws an error if something went wrong.
How async replaces closure completion callbacks
Async methods replace the often seen closure completion callbacks. Completion callbacks were common in Swift to return from an asynchronous task, often combined with a Result type parameter. The above method would have been written as followed:
func fetchImages(completion: (Result<[UIImage], Error>) -> Void) {
// .. perform data request
}
Defining a method using a completion closure is still possible in Swift today, but it has a few downsides that are solved by using async instead:
You have to make sure yourself to call the completion closure in each possible method exit. Not doing so will possibly result in an app waiting for a result endlessly.
Closures are harder to read. It’s not as easy to reason about the order of execution as compared to how easy it is with structured concurrency.
Retain cycles need to be avoided using weak references.
Implementors need to switch over the result to get the outcome. It’s not possible to use try catch statements from the implementation level.
These downsides are based on the closure version using the relatively new Result enum. It’s likely that a lot of projects still make use of completion callbacks without this enumeration:
func fetchImages(completion: ([UIImage]?, Error?) -> Void) {
// .. perform data request
}
Defining a method like this makes it even harder to reason about the outcome on the caller’s side. Both value and error are optional, which requires us to perform an unwrap in any case. Unwrapping these optionals results in more code clutter which does not help to improve readability.

Swift Call async function on main actor from synchronous context

I am trying to understand as to why following piece of code throws an assertion. What I am trying to do is to call asyncFunc() on main thread/main actor from call site. I don't want to decorate asyncFunc with #MainActor as I want the function to be thread agnostic.
func asyncFunc() async -> String? {
dispatchPrecondition(condition: .onQueue(.main))
return "abc"
}
func callSite() {
Task { #MainActor in
await asyncFunc()
}
}
My understanding was that Task { #MainActor ...} would execute all the following code on MainActor/main thread.
Currently actors in swift are reentrant as mentioned in proposal:
Actor-isolated functions are reentrant. When an actor-isolated function suspends, reentrancy allows other work to execute on the actor before the original actor-isolated function resumes, which we refer to as interleaving. Reentrancy eliminates a source of deadlocks, where two actors depend on each other, can improve overall performance by not unnecessarily blocking work on actors, and offers opportunities for better scheduling of (e.g.) higher-priority tasks.
Hence, your asyncFunc is not run on MainActor as it will block the actor from processing other high-priority tasks. To fix this you can try following two approaches:
Make your function synchronous, so that it will run on MainActor:
func syncFunc() -> String? {
dispatchPrecondition(condition: .onQueue(.main))
return "abc"
}
func callSite() {
Task { #MainActor in
syncFunc()
}
}
Or explicitly mark your asynchronous function to be run on MainActor:
#MainActor
func asyncFunc() async -> String? {
dispatchPrecondition(condition: .onQueue(.main))
return "abc"
}
func callSite() {
Task {
await asyncFunc()
}
}
Note that same reentrancy rule applies here as well, synchronous calls in asyncFunc (i.e. dispatchPrecondition) will be executed on MainActor while any asynchronous call inside asyncFunc will not be called on MainActor.

Using Dispatch.main in code called from XCTestCase does not work

I have a function that is a async wrapper around a synchronous function.
The synchronous function is like:
class Foo {
class func bar() -> [Int] {
return [1,2,3]
}
class func asyncBar(completion: #escaping ([Int]) -> Void) {
DispatchQueue.global(qos: .userInitiated).async {
let intArray = bar()
DispatchQueue.main.async {
completion(intArray)
}
}
}
}
When I call it from a XCTestCase completion does not run.
Is there some sort of perverse interaction between the way unit tests are done in XCode and the main thread?
I can find no documentation on the Web about this.
I have to use the main thread for the callback as it interacts with the Gui.
My test case looks something like:
func testAsyncBar() throws {
var run = true
func stopThisThing(ints: [Int]) {
run = false
}
Foo.asyncBar(completion: stopThisThing)
while run {
print ("Running...")
usleep(100000)
}
}
The busy loop at the end never stops.
Your test’s while loop will block the main thread (if the test runs on the main thread). As such, the closure dispatched via DispatchQueue.main.async can never run, and your run Boolean will never get reset. This results in a deadlock. You can confirm this by printing Thread.isMainThread or adding a test like dispatchPrecondition(condition: .onQueue(.main)).
Fortunately, unit tests have a simple mechanism that avoids this deadlock.
If you want unit test to wait for some asynchronous process, use an XCTestExpectation:
func testAsyncBar() throws {
let e = expectation(description: "asyncBar")
func stopThisThing(ints: [Int]) {
e.fulfill()
}
Foo.asyncBar(completion: stopThisThing)
waitForExpectations(timeout: 5)
}
This avoids problems introduced by the while loop that otherwise blocked the thread.
See Testing Asynchronous Operations with Expectations.

Race condition in unit tests

I'm currently testing a number of classes that do network stuff like REST API calls, and a Realm database is mutated in the process. When I run all the different tests I have at once, race conditions appear (but of course, when I run them one by one, they all pass). How can I reliably make the tests pass?
I have tried to call the mentioned functions in a GCD block like this:
DispatchQueue.main.async {
self.function.start()
}
One of my tests are still failing, so I guess the above didn't work. I have enabled Thread Sanitizer and it reports, from time to time, that race conditions appear.
I can't post code, so I'm looking for conceptual solutions.
Typically some form of dependency injection. Be it an internally exposed var to the DispatchQueue, a default argument in a function with the queue, or a constructor argument. You just need some way to pass a test queue that dispatches the event when you need to.
DispatchQueue.main.async will schedule the block async to the callee on the main queue and therefore isn't guarenteed by the time you make an assertion.
Example (disclaimer: I'm typing from memory so it might not compile but it gives the idea):
// In test code.
struct TestQueue: DispatchQueue {
// make sure to impement other necessary protocol methods
func async(block: () -> Void) {
// you can even have some different behavior for when to execute the block.
// also you can pass XCTestExpectations to this TestQueue to be fulfilled if necessary.
block()
}
}
// In source code. In test, pass the Test Queue to the first argument
func doSomething(queue: DispatchQueue = DispatchQueue.main, completion: () -> Void) {
queue.async(block: completion)
}
Other methods of testing async and eliminating race conditions revolve around craftily fulfilling an XCTestExpectation.
If you have access to the completion block that is eventually invoked:
// In source
class Subject {
func doSomethingAsync(completion: () -> Void) {
...
}
}
// In test
func testDoSomethingAsync() {
let subject = Subject()
let expect = expectation(description: "does something asnyc")
subject.doSomethingAsync {
expect.fulfill()
}
wait(for: [expect], timeout: 1.0)
// assert something here
// or the wait may be good enough as it will fail if not fulfilled
}
If you don't have access to the completion block it usually means finding a way to inject or subclass a test double that you can set an XCTestExpectation on and will eventually fulfill the expectation when the async work has completed.