Swift, Dispatch_Group_Wait Not Waiting

Swift, dispatch_group_wait not waiting

When using dispatch_group_async to call methods that are, themselves, asynchronous, the group will finish as soon as all of the asynchronous tasks have started, but will not wait for them to finish. Instead, you can manually call dispatch_group_enter before you make the asynchronous call, and then call dispatch_group_leave when the asynchronous call finish. Then dispatch_group_wait will now behave as expected.

To accomplish this, though, first change downloadImage to include completion handler parameter:

private func downloadImage(serverFile: AdFileInfo, completionHandler: (NSError?)->()) {
let destinationPath = imageDirectoryURL.URLByAppendingPathComponent(serverFile.fileName)

Alamofire.download(.GET, serverFile.imageUrl) { temporaryURL, response in return destinationPath }
.response { _, _, _, error in
if let error = error {
print("Error downloading \(serverFile.fileName): \(error)")
} else {
print("Done downloading \(serverFile.fileName)")
}
completionHandler(error)
}
}

I've made that a completion handler that passes back the error code. Tweak that as you see fit, but hopefully it illustrates the idea.

But, having provided the completion handler, now, when you do the downloads, you can create a group, "enter" the group before you initiate each download, "leave" the group when the completion handler is called asynchronously.

But dispatch_group_wait can deadlock if you're not careful, can block the UI if done from the main thread, etc. Better, you can use dispatch_group_notify to achieve the desired behavior.

func downloadImages(_ imageFilesOnServer: [AdFileInfo], completionHandler: @escaping (Int) -> ()) {
let group = DispatchGroup()

var downloaded = 0

group.notify(queue: .main) {
completionHandler(downloaded)
}

for serverFile in imageFilesOnServer {
group.enter()

print("Start downloading \(serverFile.fileName)")

downloadImage(serverFile) { error in
defer { group.leave() }

if error == nil {
downloaded += 1
}
}
}
}

And you'd call it like so:

downloadImages(arrayOfAdFileInfo) { downloaded in
// initiate whatever you want when the downloads are done

print("All Done! \(downloaded) downloaded successfully.")
}

// but don't do anything contingent upon the downloading of the images here

For Swift 2 and Alamofire 3 answer, see previous revision of this answer.

Swift, dispatch_group_wait not waiting

When using dispatch_group_async to call methods that are, themselves, asynchronous, the group will finish as soon as all of the asynchronous tasks have started, but will not wait for them to finish. Instead, you can manually call dispatch_group_enter before you make the asynchronous call, and then call dispatch_group_leave when the asynchronous call finish. Then dispatch_group_wait will now behave as expected.

To accomplish this, though, first change downloadImage to include completion handler parameter:

private func downloadImage(serverFile: AdFileInfo, completionHandler: (NSError?)->()) {
let destinationPath = imageDirectoryURL.URLByAppendingPathComponent(serverFile.fileName)

Alamofire.download(.GET, serverFile.imageUrl) { temporaryURL, response in return destinationPath }
.response { _, _, _, error in
if let error = error {
print("Error downloading \(serverFile.fileName): \(error)")
} else {
print("Done downloading \(serverFile.fileName)")
}
completionHandler(error)
}
}

I've made that a completion handler that passes back the error code. Tweak that as you see fit, but hopefully it illustrates the idea.

But, having provided the completion handler, now, when you do the downloads, you can create a group, "enter" the group before you initiate each download, "leave" the group when the completion handler is called asynchronously.

But dispatch_group_wait can deadlock if you're not careful, can block the UI if done from the main thread, etc. Better, you can use dispatch_group_notify to achieve the desired behavior.

func downloadImages(_ imageFilesOnServer: [AdFileInfo], completionHandler: @escaping (Int) -> ()) {
let group = DispatchGroup()

var downloaded = 0

group.notify(queue: .main) {
completionHandler(downloaded)
}

for serverFile in imageFilesOnServer {
group.enter()

print("Start downloading \(serverFile.fileName)")

downloadImage(serverFile) { error in
defer { group.leave() }

if error == nil {
downloaded += 1
}
}
}
}

And you'd call it like so:

downloadImages(arrayOfAdFileInfo) { downloaded in
// initiate whatever you want when the downloads are done

print("All Done! \(downloaded) downloaded successfully.")
}

// but don't do anything contingent upon the downloading of the images here

For Swift 2 and Alamofire 3 answer, see previous revision of this answer.

dispatch_group_wait not waiting

Never mind, I solved by ditching the Facebook SDK and using contentsOfURL function instead because it's synchronous.

Dispatch Group wait stuck forever

Assuming that getActionField is being called on the main queue and the understanding that Alamofire calls its completion blocks on the main queue (bad design in my opinion), you are running into a deadlock since the call to wait is now blocking the main queue and none of the calls to leave can be made.

You must never use the same thread to call wait and leave.

The simplest solution is to replace the use of wait with notify.

group.notify(queue: DispatchQueue.main) {
completion(resreturn)
}

You should avoid the use of wait in general. Especially if you are already using a completion handler and there is no need for the method to wait.

How to handle group wait result in Swift 3

This code snippet raises a variety of different questions:

  1. I notice that the behavior differs a bit between the playground and when you run it in an app. I suspect it's some idiosyncrasy of needsIndefiniteExecution of PlaygroundPage and GCD. I'd suggest testing this in an app. With the caveat of the points I raise below, it works as expected when I ran this from an app.

  2. I notice that you've employed this pattern:

    __dispatch_group_async(group, queue) {
    ...
    }

    I would suggest:

    queue.async(group: group) {
    ...
    }
  3. You are doing group.suspend(). A couple of caveats:

    • One suspends queues, not groups.

    • And if you ever call suspend(), make sure you have a corresponding call to resume() somewhere.

    • Also, remember that suspend() stops future blocks from starting, but it doesn't do anything with the blocks that may already be running. If you want to stop blocks that are already running, you may want to cancel them.

    • Finally, note that you can only suspend queues and sources that you create. You can't (and shouldn't) suspend a global queue.

  4. I also notice that you're using wait on the same queue that you dispatched the test() call. In this case, you're getting away with that because it is a concurrent queue, but this sort of pattern invites deadlocks. I'd suggest avoiding wait altogether if you can, and certainly don't do it on the same queue that you called it from. Again, it's not a problem here, but it's a pattern that might get you in trouble in the future.

    Personally, I might be inclined to use notify rather than wait to trigger the block of code to run when the two dispatched blocks are done. This eliminates any deadlock risk. And if I wanted to have a block of code to run after a certain amount of time (i.e. a timeout process), I might use a timer to trigger some cleanup process in case those two blocks were still running (perhaps canceling them; see How to stop a DispatchWorkItem in GCD?).

Dispatch Group wait till Timeout

stackoverflow.com/a/61192412/1801544 Please set timeout interval, I believe this is what you want

Waiting until the task finishes

If you need to hide the asynchronous nature of myFunction from the caller, use DispatchGroups to achieve this. Otherwise, use a completion block. Find samples for both below.


DispatchGroup Sample

You can either get notified when the group's enter() and leave() calls are balanced:

func myFunction() {
var a = 0

let group = DispatchGroup()
group.enter()

DispatchQueue.main.async {
a = 1
group.leave()
}

// does not wait. But the code in notify() is executed
// after enter() and leave() calls are balanced

group.notify(queue: .main) {
print(a)
}
}

or you can wait:

func myFunction() {
var a = 0

let group = DispatchGroup()
group.enter()

// avoid deadlocks by not using .main queue here
DispatchQueue.global(qos: .default).async {
a = 1
group.leave()
}

// wait ...
group.wait()

print(a) // you could also `return a` here
}

Note: group.wait() blocks the current queue (probably the main queue in your case), so you have to dispatch.async on another queue (like in the above sample code) to avoid a deadlock.


Completion Block Sample

func myFunction(completion: @escaping (Int)->()) {
var a = 0

DispatchQueue.main.async {
let b: Int = 1
a = b
completion(a) // call completion after you have the result
}
}


// on caller side:
myFunction { result in
print("result: \(result)")
}

Why my DispatchGroup's wait always timeout?

A couple of thoughts:

  1. As rmaddy said, if you want to wait 10 seconds, the correct syntax would be:

     let waitResult = dispatchGroup.wait(timeout: .now() + 10)

    Your syntax is not going to wait at all, and it's going to look like it instantly timed out.

  2. You shouldn't be waiting at all.

    First, you should never block the main thread. At worst, you could have your app killed by the watchdog process if you do this at the wrong time. At best, it's a horrible UX (i.e. the app will appear to be completely frozen to the end user).

    Second, depending upon how POSTRequest was implemented, you might introduce a new problem. Specifically, if POSTRequest calls its completion handler on the main queue (e.g. the default behavior for network libraries like Alamofire), you could deadlock until with wait timeout on the main thread expires. If you are going to wait (which you shouldn't do anyway), you want to make sure you never wait on the same queue that your completion handler will use.

    Note, if the completion handler isn't running on the same queue that you are waiting, there is no deadlock risk, but you still suffer from the problem I mentioned earlier, namely that you're blocking the main thread, which should always be avoided.

You should instead move all that code inside the closure. And if that closure is running on a background thread, make sure to DispatchQueue.main.async { ... } to dispatch it back to the main queue.

So, the correct pattern is something like the following.

  1. If you want your request to timeout after 10 seconds, build that into the original request. Note the timeoutInterval:

     class func POSTRequest(accessPoint: String, params: [String: String]?, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) {
    let url = URL(string: baseURLString)!
    .appendingPathComponent(accessPoint)

    let request = URLRequest(url: url, timeoutInterval: 10)

    URLSession.shared.dataTask(with: request, completionHandler: completionHandler)
    }

    Clearly, you may be doing something else in POSTRequest, so don't get lost in the details there. The key point is to build the timeoutInterval into the request.

  2. Then, make sure your UIAlertAction starts the request and simply puts all of the processing after the POSTRequest inside its completionHandler, thereby eliminating the need for any dispatch group waiting or anything like that:

     let goAheadAction = UIAlertAction(title: "Go Ahead", style: .default) { _ in
    let email = self.emailAddressTextField.text!
    let parameters = ["emailAddress": email]
    Server.POSTRequest(accessPoint: "/UserServices/forgotPassword", params: parameters) { data, response, error in
    DispatchQueue.main.async {
    guard error == nil else {
    let alert = UIAlertController(title: "Time Out", message: "Failed to request service, reason: \"Time Out\"", preferredStyle: .alert)
    alert.addAction( UIAlertAction(title: "Got it", style: .cancel))
    self.present(alert, animated: true)
    return
    }

    let alert = UIAlertController(title: "Email Sent", message: "We've sent an password reset link to \"\(email)\"", preferredStyle: .alert)
    alert.addAction(UIAlertAction(title: "Got it", style: .cancel))
    self.present(alert, animated: true)
    }
    }
    }

    This way, no threads are blocked.

setting wait time on dispatch_group

I hope, this example is 'self explanatory'. see, how timeout is defined!

import Foundation

let group = dispatch_group_create()
let queue = dispatch_queue_create("queue", DISPATCH_QUEUE_SERIAL)
dispatch_async(queue) { () -> Void in
dispatch_group_enter(group)
var i = 0
while i < 15 {
print(i)
i++
usleep(500000)
}
dispatch_group_leave(group)
}
//dispatch_group_wait(group, DISPATCH_TIME_FOREVER)
let t = dispatch_time( DISPATCH_TIME_NOW, 5000000000)
dispatch_group_wait(group, t)
print("stop")

/* prints
0
1
2
3
4
5
6
7
8
9
stop
*/

Declaration dispatch_time_t dispatch_time( dispatch_time_t when,
int64_t delta); Parameters when The dispatch_time_t value to use as
the basis for a new value. Pass DISPATCH_TIME_NOW to create a new time
value relative to now. delta The number of nanoseconds to add to the
time in the when parameter. Return Value A new dispatch_time_t.

Discussion The default clock is based on mach_absolute_time.

You can use simplified version with the same functionality

import Foundation

let group = dispatch_group_create()
let queue = dispatch_queue_create("queue", DISPATCH_QUEUE_SERIAL)
dispatch_group_async(group, queue) { () -> Void in
var i = 0
while i < 15 {
print(i)
i++
usleep(500000) // 0.5 sec delay
}
}
// wait time a little bit more than 1 second
let t = dispatch_time( DISPATCH_TIME_NOW, 1100000000)
dispatch_group_wait(group, t)
print("stop")

/* prints
0
1
2
stop
*/

with

dispatch_group_wait(group, DISPATCH_TIME_FOREVER)

the same code prints all 15 numbers (0...14)



Related Topics



Leave a reply



Submit