youareawaitress
youareawaitress

Reputation: 407

How does the semaphore keep async loop in order?

I've set up this script to loop through a bunch of data in the background and I've successfully set up a semaphore to keep everything (the array that will populate the table) in order but I cannot exactly understand how or why the semaphore keeps the array in order. The dispatchGroup is entered, the loop stops and waits until the image is downloaded, once the image is gotten the dispatchSemaphore is set to 1 and immediately the dispatchGroup is exited and the semaphore set back to 0. The semaphore is toggled so fast from 0 to 1 that I don't understand how it keeps the array in order.

let dispatchQueue = DispatchQueue(label: "someTask")
let dispatchGroup = DispatchGroup()
let dispatchSemaphore = DispatchSemaphore(value: 0)

dispatchQueue.async {

    for doc in snapshot.documents {

        // create data object for array

        dispatchGroup.enter()

        // get image with asynchronous completion handler
        Storage.storage().reference(forURL: imageId).getData(maxSize: 1048576, completion: { (data, error) in

            defer {
                dispatchSemaphore.signal()
                dispatchGroup.leave()
            }

            if let imageData = data,
                error == nil {
                // add image to data object
                // append to array
            }

        })

        dispatchSemaphore.wait()

    }

    // do some extra stuff in background after loop is done

}

dispatchGroup.notify(queue: dispatchQueue) {

    DispatchQueue.main.async {
        self.tableView.reloadData()
    }

}

Upvotes: 4

Views: 1675

Answers (2)

Alexander
Alexander

Reputation: 63272

The DispatchGroup isn't really doing anything here. You have mutual exclusion granted by the DispatchSemaphor, and the ordering is simply provided by the iteration order of snapshot.documents

Upvotes: 0

anon
anon

Reputation:

The solution is in your comment get image with asynchronous completion handler. Without the semaphore all image downloads would be started at the same time and race for completion, so the image that downloads fastest would be added to the array first.

So after you start your download you immediately wait on your semaphore. This will block until it is signaled in the callback closure from the getData method. Only then the loop can continue to the next document and download it. This way you download one file after another and block the current thread while the downloads are running.

Using a serial queue is not an option here, since this would only cause the downloads to start serially, but you can’t affect the order in which they finish.

This is a rather inefficient though. Your network layer probably can run faster if you give it multiple requests at the same time (think of parallel downloads and HTTP pipelining). Also you're 'wasting' a thread which could do some different work in the meantime. If there is more work to do at the same time GCD will spawn another thread which wastes memory and other resources.

A better pattern would be to skip the semaphore, let the downloads run in parallel and store the image directly at the correct index in your array. This of course means you have to prepare an array of the appropriate size beforehand, and you have to think of a placeholder for missing or failed images. Optionals would do the trick nicely:

var images: [UIImage?] = Array(repeating: nil, count: snapshot.documents.count)

for (index, doc) in snapshot.documents.enumerated() {

    // create data object for array

    dispatchGroup.enter()

    // get image with asynchronous completion handler
    Storage.storage().reference(forURL: imageId).getData(maxSize: 1048576) { data, error in

        defer {
            dispatchGroup.leave()
        }

        if let imageData = data,
            error == nil {
            // add image to data object
            images[index] = image
        }
    }
}

Upvotes: 5

Related Questions