i have a question, and I will tell you as clear as possible:
I need to get an object to my func, create a variable version of it, change some property values one by one, and use new version of it for saving to the cloud. My problem is, when I declare my variable, if I change property values inside my Dispatch Semaphore, the variable outside not changing somehow, there is some problem that I could not understand. Here is the code :
func savePage(model: PageModel, savingHandler: @escaping (Bool) -> Void) {
// some code .....
var page = model // (1) I created a variable from function arg
let newQueue = DispatchQueue(label: "image upload queue")
let semaphore = DispatchSemaphore(value: 0)
newQueue.async {
if let picURL1 = model.picURL1 {
self.saveImagesToFireBaseStorage(pictureURL: picURL1) { urlString in
let url = urlString // (2) urlString value true and exist
page.picURL1 = url // (3) modified the new created "page" object
print(page.picURL1!) // (4) it works, object prints modified
}
}
semaphore.signal()
}
semaphore.wait()
newQueue.async {
if let picURL2 = model.picURL2 {
self.saveImagesToFireBaseStorage(pictureURL: picURL2) { urlString in
let url = urlString
page.picURL2 = url
}
}
semaphore.signal()
}
semaphore.wait()
print(page.picURL1!) //(5) "page" object has the old value?
newQueue.async {
print(page.picURL1!) //(6) "page" object has the old value?
do {
try pageDocumentRef.setData(from: page)
savingHandler(true)
} catch let error {
print("Error writing city to Firestore: \(error)")
}
semaphore.signal()
}
semaphore.wait()
}
I should upload some pictures to the cloud and get their urls, so I can create updated version of the object and save onto the old version on the cloud. But "page" object doesn't change somehow. When inside the semaphore, it prints right value, when outside, or inside another async semaphore block, it prints old value. I am new to the concurrency and could not find a way.
What I tried before :
- Using Operation Queue and adding blocks as dependencies.
- Creating queue as DispatchQueue.global()
What is the thing I am missing here?
Edit : I added semaphore.wait() after second async call. It actually was in my code but I accidentally deleted it while pasting to the question, thanks to Chip Jarred for pointing it.
UPDATE
This is how I changed the code with async - await:
func savePage(model: PageModel, savingHandler: @escaping () -> Void) {
// Some code
Task {
do {
let page = model
let updatedPage = try await updatePageWithNewImageURLS(page)
// some code
} catch {
// some code
}
}
// Some code
}
private func saveImagesToFireBaseStorage(pictureURL : String?) async -> String? {
//some code
return downloadURL.absoluteString
}
private func updatePageWithNewImageURLS(_ page : PageModel) async throws -> PageModel {
let picUrl1 = await saveImagesToFireBaseStorage(pictureURL: page.picURL1)
let picUrl2 = await saveImagesToFireBaseStorage(pictureURL: page.picURL2)
let picUrl3 = await saveImagesToFireBaseStorage(pictureURL: page.picURL3)
let newPage = page
return try await addNewUrlstoPage(newPage, url1: picUrl1, url2: picUrl2, url3: picUrl3)
}
private func addNewUrlstoPage(_ page : PageModel, url1: String?, url2 : String?, url3 :String?) async throws -> PageModel {
var newPage = page
if let url1 = url1 { newPage.picURL1 = url1 }
if let url2 = url2 { newPage.picURL2 = url2 }
if let url3 = url3 { newPage.picURL3 = url3 }
return newPage
}
So one async function gets a new photo url, for an old url, second async function runs multiple of this first function inside to get new urls for all three urls of the object, then calls a third async function a create and return an updated object with the new values.
This is my first time using async - await.
Let's look at your first
async
call:I would guess that the inner closure, that is the one passed to
saveImagesToFireBaseStorage
, is also called asynchronously. If I'm right about that, thensaveImagesToFireBaseStorage
returns almost immediately, executes thesignal
, but the inner closure has not run yet, so the new value isn't yet set. Then after some latency, the inner closure is finally called, but that's after your "outer" code that depends onpage.picURL1
has already been run, sopage.picURL1
ends up being set afterwards.So you need to call signal in the inner closure, but you still have to handle the case where the inner closure isn't called. I'm thinking something like this:
Your second
async
would need to be modified similarly.I notice that you're not calling
wait
after the secondasync
, the one that setspage.picURL2
. So you have 2 calls towait
, but 3 calls tosignal
. That wouldn't affect whetherpage.picURL1
is set properly in the firstasync
, but it does mean thatsemaphore
will have unbalanced waits and signals at the end of your code example, and the blocking behavior ofwait
after the thirdasync
may not be as you expect it to be.If it's an option for your project, refactoring to use
async
andawait
keywords would resolve the problem in a way that's easier to maintain, because it would remove the need for the semaphore entirely.Also, if my premise that
saveImagesToFireBaseStorage
is called asynchronously is correct, you don't really need theasync
calls at all, unless there is more code in their closures that isn't shown.Update
In comments, it was revealed the using the solution above caused the app to "freeze". This suggests that
saveImagesToFireBaseStorage
calls its completion handler on the same queue thatsavePage(model:savingHandler)
is called on, and it's almost certainlyDispatchQueue.main
. The problem is thatDispatchQueue.main
is a serial queue (as isnewQueue
), which means it won't execute any tasks until the next iteration of its run loop, but it never gets to do that, because it callssemaphore.wait()
, which blocks waiting for the completion handler forsaveImagesToFireBaseStorage
to callsemaphore.signal
. By waiting it prevents the very thing its waiting for from ever executing.You say in comments that using
async
/await
solved the problem. That's probably the cleanest way, for lots of reasons, not the least of which is that you get compile time checks for a lot of potential problems.In the mean time, I came up with this solution using
DispatchSemaphore
. I'll put it here, in case it helps someone.First I moved the creation of
newQueue
outside ofsavePage
. Creating a dispatch queue is kind of heavy-weight operation, so you should create whatever queues you need once, and then reuse them. I assume that it's a global variable or instance property of whatever object ownssavePage
.The second thing is that
savePage
doesn't block anymore, but we still want the sequential behavior, preferably without going to completion handler hell (deeply nested completion handlers).I refactored the code that calls
saveImagesToFireBaseStorage
into a local function, and made it behave synchronously by using aDispatchSemaphore
to block until it's completion handler is called, but only in that local function. I do create theDispatchSemaphore
outside of that function so that I can reuse the same instance for both invocations, but I treat it as though it were a local variable in the nested function.I also have to use a time-out for the
wait
, because I don't know if I can assume that the completion handler forsaveImagesToFireBaseStorage
will always be called. Are there failure conditions in which it wouldn't be? The time-out value is almost certainly wrong, and should be considered a place-holder for the real value. You'd need to determine the maximum latency you want to allow based on your knowledge of your app and its working environment (servers, networks, etc...).The local function uses a key path to allow setting differently named properties of
PageModel
(picURL1
vspicURL2
), while still consolidating the duplicated code.Here's the refactored
savePage
code:It's important to note that
savePage
does not block the thread that's called on, which I believe to beDispatchQueue.main
. I assume that any code that is sequentially called after a call tosavePage
, if any, does not depend on the results of callingsavePage
. Any that does depend on it should be in itssavingHandler
.And speaking of
savingHandler
, I have to assume that it might update the UI, and since the point where it would be called is in a closure fornewQueue.async
it has to be explicitly called onDispatchQueue.main
, so I do that.