Fattie
Fattie

Reputation: 12621

Use DispatchSemaphore in this "starts busy" speculative processing scenario

Imagine a screen S. Users arrive at S, look at stuff. There's a button B ...

|    |
|   B|
|    |
|    |

When you press B ..

func clickedB() {

   blockingSpinner = true
   longCalculation()
   blockingSpinner = false
   showResult()
}

func longCalculation() {

   // a few seconds
}

(We want the user to just wait, seeing a modal spinner, if/while the calculation is happening.)

Typically when a user arrives on screen S, they look at something else for a couple of seconds before touching B.

So ...

var waitor = DispatchSemaphore(value: 0) // or ???

func viewDidLoad() {

   DispatchQueue.global(qos: .background).async { longCalculation() }
}
func longCalculation() {

   something waitor
   do the calculation
   something waitor
   DispatchQueue.main.async {
     something waitor
   }
}
func clickedB() {

   // (note that ...   calculation may have finished ages ago
   // or we may be in the middle of it, it has a second or so remaining
   // or perhaps even this is the second+ time the user has clicked B)
   something waitor
   if/while longCalculation is still running,
       blockingSpinner = true
   blockingSpinner = false
   showResult()
}

I'm afraid I have no clue how to use DispatchSemaphore in that scenario.

The particular way they made wait() and signal() work don't seem to add up here.

How to use DispatchSemaphore in this scenario?

Upvotes: 4

Views: 450

Answers (1)

jscs
jscs

Reputation: 64002

You want just one thing to be able to proceed at a time, so your semaphore's value should be 1. Given that, you could also just as easily use an NSLock. But here's the outline of using the semaphore.

When you start the calculation, wait() (indefinitely) on the semaphore. As you've suggested, you can take advantage of the view controller lifecycle's inherent ordering to know that this will not actually block. Whenever you finish, `signal(). Obviously this should all be done in the background so that the main thread is not blocked.

When you process the button tap, test the semaphore by "waiting" on it with an instantaneous timeout, and show the spinner if you get .timedOut as a result. We can do this on the main thread, because whether the semaphore is available or times out, there will be no actual wait time. Note that no signal is needed here: if the waiting times out, the semaphore is re-incremented automatically. If the wait succeeds, then the job is done -- proceed directly to showing the result.

If the job is not done, you're showing the spinner; now wait (indefinitely) in the background. When that wait ends -- because the semaphore was signaled -- hop back to the main thread, dismiss the spinner, and present the results.

At this point, the semaphore's count is at 0, so if you will ever need to go through this code path again you must signal the semaphore.

Basically, filling out the code sketch you made:

class WhateverViewController : UIViewController
{
    private let semaphore = DispatchSemaphore(value: 1)

    override func viewDidLoad()
    {
        super.viewDidLoad()
        self.performLongCalculation()
    }

    private func performLongCalculation()
    {
        DispatchQueue.global(qos: .background).async {
            self.semaphore.wait()
            // Synchronous processing...
            self.semaphore.signal()
        }
    }

    private func buttonTapped()
    {
        if self.semaphore.isBusy {
            self.waitForResult()
        }
        else {
            self.showResult()
        }
    }

    private func buttonTappedAlternative()
    {
        // Show the spinner unconditionally, if you assume that the
        // calculation isn't already done.
        self.waitForResult()
    }

    private func waitForResult()
    {
        self.showSpinner()
        DispatchQueue.global(qos: .userInitiated).async {
            self.semaphore.wait()
            DispatchQueue.main.async {
                self.dismissSpinner()
                self.showResult()
            }
        }
    }

    private func showResult()
    {
        // Put stuff on screen
        self.semaphore.signal()
    }
}

Where buttonTapped is using a convenience on DispatchSemaphore

extension DispatchSemaphore
{
    var isBusy : Bool { return self.wait(timeout: .now()) == .timedOut }
}

And you could reverse this logic if you prefer: isIdle would just be self.wait(timeout: .now()) == .success

Upvotes: 1

Related Questions