13

I have a function func getValue() -> Bool that's called from a background thread. This is intentional and also required. Now, the getValue() needs to execute something on the main thread, in this case it needs to access UIApplication.shared.canOpenURL, which must be run on the main queue.

This is my current function:

func getValue() -> Bool {
    guard let url = URL(string: "someurl") else { return false }
    return UIApplication.shared.canOpenURL(url)
}

How can I convert that function to a thread safe one, namely to make sure it always runs on the main thread, without

  • calling the function from the main thread to begin with
  • refactoring the function to return the value in a closure

I've tried this:

// This causes a deadlock, see https://stackoverflow.com/a/42484670/1531270
func getValue() -> Bool {
    var flag = false

    let group = DispatchGroup()
    group.enter()
    DispatchQueue.main.async {
        if let url = URL(string: "someurl"), UIApplication.shared.canOpenURL(url) {
            flag = true
        }
        group.leave()
    }
    group.wait()

    return flag
}

and this:

// This crashes with EXC_BREAKPOINT (SIGTRAP) dispatch_sync called on queue already owned by current thread
func getValue() -> Bool {
    return DispatchQueue.main.sync {
        guard let url = URL(string: "someurl") else { return false }
        return UIApplication.shared.canOpenURL(url)
    }
}

but neither of them works. Any ideas?

imas145
  • 1,959
  • 1
  • 23
  • 32
  • Don't use DispatchGroup, group.enter() ans leave in the above code. – vivekDas Aug 18 '18 at 14:20
  • @vivekDas If you mean like this `func getValue() -> Bool { var flag = false DispatchQueue.main.async { if let url = URL(string: "someurl"), UIApplication.shared.canOpenURL(url) { flag = true } } return flag }` then that won't work either since the code inside the dispatch async is never executed, as the function has already returned a value. – imas145 Aug 18 '18 at 14:28
  • Okay, you are not doing any UI activity, so I think without using any main queue you can do this ? Have you tried this ? – vivekDas Aug 18 '18 at 14:30
  • @vivekDas The reason I'm asking this question is because the main thread checker in Xcode pointed this out, and it does make since as I'm calling UIApplication, which is a part of UIKit. It would probably work fine, but I'd like to follow best practices. – imas145 Aug 18 '18 at 14:32
  • @matt So the crash (which you couldn't reproduce) occurs when I'm dispatching to the main queue from the main queue (yeah, my bad - I said this is called from a background queue but that's not always the case). I added a check for this and it now seems to work. Xcode paused at a main thread check once even in the main queue block, but I haven't been able to reproduce that, so could've just been a glitch. I'll do more testing before accepting an answer. – imas145 Aug 18 '18 at 16:15
  • "yeah, my bad - I said this is called from a background queue but that's not always the case" Definitely. As I was about to say, it sounds like something is wrong with how you're calling `getValue`. But you _didn't show that code_. You left out the most important piece of the question. — By the way, your `getValue` can check to see that it's being called on the correct thread; maybe you should add that. – matt Aug 18 '18 at 16:17
  • @matt Of course I would've included that code had I deemed it important. I had this false notion that you couldn't call sync on the main thread, so I didn't pay much attention to the crash I was already expecting. Anyway, like I said, I added a thread check and everything seems to be working. Really appreciate your help, but not the tone. – imas145 Aug 18 '18 at 16:27
  • Right, but "deemed" and "false notion" are key here. "deemed" suggests it's not an [MCVE](https://stackoverflow.com/help/mcve) and "false notion" suggests it's an [x-y question](https://meta.stackexchange.com/questions/66377/what-is-the-xy-problem). I'm just trying to point out a better way of asking questions. – matt Aug 18 '18 at 16:40
  • @matt You're correct on that, this is not a great question, and I better understand why. – imas145 Aug 18 '18 at 16:46
  • "which must be run on the main queue" why? – ScottyBlades Apr 24 '22 at 07:20

2 Answers2

12

You're looking for a semaphore - try this:

DispatchQueue.global(qos: .background).async {
    var value: Bool? = nil
    let semaphore = DispatchSemaphore(value: 0)
    DispatchQueue.main.async {
        let alert = UIAlertController(title: "Choose one", message: "Take your time, I'll wait", preferredStyle: .alert)
        alert.addAction(UIAlertAction(title: "true", style: .default, handler: { _ in
            value = true
            semaphore.signal()
        }))
        alert.addAction(UIAlertAction(title: "false", style: .default, handler: { _ in
            value = false
            semaphore.signal()
        }))
        self.present(alert, animated: true, completion: nil)
    }
    semaphore.wait()
    print("Choice: \(value!)")
}

Or to use your example from above:

func getValue() -> Bool {
    var flag = false
    let semaphore = DispatchSemaphore(value: 0)

    DispatchQueue.main.async {
        if let url = URL(string: "someurl"), UIApplication.shared.canOpenURL(url) {
            flag = true
            semaphore.signal()
        }
    }
    semaphore.wait()
    return flag
}
sam-w
  • 7,478
  • 1
  • 47
  • 77
9

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.

matt
  • 515,959
  • 87
  • 875
  • 1,141