iOS - Ensure Execution on Main Thread

iOS - Ensure execution on main thread

there any rule I can follow to be sure that my app executes my own code just in the main thread?

Typically you wouldn't need to do anything to ensure this — your list of things is usually enough. Unless you're interacting with some API that happens to spawn a thread and run your code in the background, you'll be running on the main thread.

If you want to be really sure, you can do things like

[self performSelectorOnMainThread:@selector(myMethod:) withObject:anObj waitUntilDone:YES];

to execute a method on the main thread. (There's a GCD equivalent too.)

How do I ensure my DispatchQueue executes some code on the main thread specifically?

Yes, you can nest a dispatch to one queue inside a dispatch to another queue. We frequently do so.

But be very careful. Just wrapping an asynchronous dispatch to the main queue with a dispatch from your synchronizing queue is insufficient. Your first example is not thread safe. That array that you are accessing from the main thread might be mutating from your synchronization queue:

Sample Image

This is a race condition because you potentially have multiple threads (your synchronization queue’s thread and the main thread) interacting with the same collection. Rather than having your dispatched block to the main queue just interact objects directly, you should make a copy of of it, and that’s what you reference inside the dispatch to the main queue.

For example, you might want to do the following:

func process(completion: @escaping (String) -> Void) {
syncQueue.sync {
let result = ... // note, this runs on thread associated with `syncQueue` ...

DispatchQueue.main.async {
completion(result) // ... but this runs on the main thread
}
}
}

That ensures that the main queue is not interacting with any internal properties of this class, but rather just the result that was created in this closure passed to syncQueue.


Note, all of this is unrelated to it being a singleton. But since you brought up the topic, I’d advise against singletons for model data. It’s fine for sinks, stateless controllers, and the like, but not generally advised for model data.

I’d definitely discourage the practice of initiating UI controls updates directly from the singleton. I’d be inclined to provide these methods completion handler closures, and let the caller take care of the resulting UI updates. Sure, if you want to dispatch the closure to the main queue (as a convenience, common in many third party API), that’s fine. But the singleton shouldn’t be reaching in and update UI controls itself.

I’m assuming you did all of this just for illustrative purposes, but I added this word of caution to future readers who might not appreciate these concerns.

Make sure function runs on main thread only

This is fine. You can also use GCD to execute code on the main thread.

Checkout this SO post.

GCD to perform task in main thread

`Task` blocks main thread when calling async function inside

Consider:

func populate() async {
var items = [String]()
for i in 0 ..< 4_000_000 {
items.append("\(i)")
}
self.items = items
}

You have marked populate with async, but there is nothing asynchronous here, and it will block the calling thread.

Then consider:

func populate() {
Task {
var items = [String]()
for i in 0 ..< 4_000_000 {
items.append("\(i)")
}
self.items = items
}
}

That looks like it must be asynchronous, since it is launching a Task. But this Task runs on the same actor, and because the task is slow and synchronous, it will block the current executor.

If you do not want it to run on the main actor, you can either:

  1. You can leave the view model on the main actor, but manually move the slow synchronous process to a detached task:

    @MainActor
    class ViewModel: ObservableObject {
    @Published var items = [String]()

    func populate() {
    Task.detached {
    var items = [String]()
    for i in 0 ..< .random(in: 4_000_000...5_000_000) { // made it random so I could see values change
    items.append("\(i)")
    }
    await MainActor.run { [items] in
    self.items = items
    }
    }
    }
    }
  2. For the sake of completeness, you could also make the view model to be its own actor, and only designate the relevant observed properties as being on the main actor:

    actor ViewModel: ObservableObject {
    @MainActor
    @Published var items = [String]()

    func populate() {
    Task {
    var items = [String]()
    for i in 0 ..< .random(in: 4_000_000...5_000_000) { // made it random so I could see values change
    items.append("\(i)")
    }
    await MainActor.run { [items] in
    self.items = items
    }
    }
    }
    }

I would generally lean towards the former, but both approaches work.

Either way, it will allow the slow process to not block the main thread. Here I tapped on the button twice:

Sample Image


See WWDC 2021 videos Swift concurrency: Behind the scenes, Protect mutable state with Swift actors, and Swift concurrency: Update a sample app, all of which are useful when trying to grok the transition from GCD to Swift concurrency.

Are uikit methods guaranteed to be run on main thread?

You're confused.

A given call chain is either run on the main thread or on a background thread. Most of the code you deal with is invoked through system events, and is guaranteed to be run on the main thread. There are only a limited number of cases where your code will be invoked from a background thread. Only in those cases do you need to wrap UIKit calls in code that redirects the call to the main thread.

The most common reason for your code to be running from a background thread is because you invoked it that way - often using Grand Central Dispatch.

You might invoke some code like this:

DispatchQueue.global(qos: .userInitiated).async { [weak self] in
//This code will be on a background thread
//Do time-consuming work here

//This calls back to the main thread.
DispatchQueue.main.async { [weak self] in
//Put your code to display your results in the UI here.
}
}

View controller functions, button actions, etc are always called on the main thread. If the system calls some method on a background thread, it will be well documented.

Some functions that take completion handlers invoke those completion handlers on a background thread. The NSURLSession (URLSession) class, for example, invokes its completion handlers and delegate methods on a delegate queue that defaults to a background thread. So when you pass a completion handler to an instance of URLSession, you need to make sure your UIKit code is wrapped in a call to dispatch_async() (DispatchQueue.main.async() in Swift 3) or a similar method of passing code to the main thread to execute.

Returning value while dispatching to the main thread

I can't reproduce any issue with your second example. You didn't show how you're calling getValue, so I made something up:

func getValue() -> Bool {
return DispatchQueue.main.sync {
guard let url = URL(string: "testing://testing") else { return false }
return UIApplication.shared.canOpenURL(url)
}
}
override func viewDidLoad() {
super.viewDidLoad()
DispatchQueue.global(qos:.background).async {
let ok = self.getValue()
print(ok) // false, the right answer
}
}

There's no "crash", so I would just go with that. When I use the scheme testing: I get false, and when I change testing: to https:, I return true, so clearly the method call is working.



Related Topics



Leave a reply



Submit