我有一个问题,我会尽可能清楚地告诉你:
我需要一个对象的func,创建一个变量版本,一个一个地修改一些属性值,然后用新的版本保存到云。我的问题是,当我声明变量时,如果我修改了Dispatch信号量内部的属性值,而外部的变量却没有改变,那么就会出现一些我无法理解的问题。下面是代码:
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()
}
我应该上传一些图片到云,并获得他们的网址,这样我就可以创建对象的更新版本,并保存到云上的旧版本。但“页面”对象并没有改变。当在信号量内,它打印正确的值,当外部,或在另一个异步信号量块内,它打印旧值。我是新的并发,无法找到一种方法。
我之前尝试过:
- 使用操作队列并将块添加为相关性。
- 正在将队列创建为DispatchQueue.global()
我错过了什么?
编辑:我在第二次异步调用后添加了信号量.wait()。它实际上在我的代码中,但我在粘贴到问题时不小心删除了它,感谢Chip Jarred指出它。
1条答案
按热度按时间tmb3ates1#
让我们看一下您的第一个
async
调用:我猜内部闭包,也就是传递给
saveImagesToFireBaseStorage
的闭包,也是异步调用的,如果我猜对了,那么saveImagesToFireBaseStorage
几乎立即返回,执行signal
,但是内部闭包还没有运行,所以新值还没有设置,然后经过一段延迟,内部闭包最终被调用,但这是在依赖于page.picURL1
的"外部"代码已经运行之后,因此page.picURL1
最终是在之后设置的。所以你需要在内部闭包中调用signal,但是你仍然需要处理内部闭包没有被调用的情况,我的想法是这样的:
您的第二个
async
需要进行类似的修改。我注意到你没有在第二个
async
之后调用wait
,第二个async
设置了page.picURL2
,所以你有2个wait
调用,但是有3个signal
调用,这不会影响page.picURL1
在第一个async
中是否设置正确。但这确实意味着semaphore
在代码示例的末尾将具有不平衡的等待和信号,并且wait
在第三个async
之后的阻塞行为可能与您所期望的不同。如果您的项目可以选择使用
async
和await
关键字进行重构,则可以通过更易于维护的方式解决问题,因为这样可以完全消除对信号量的需要。另外,如果我关于异步调用
saveImagesToFireBaseStorage
的假设是正确的,那么您实际上根本不需要async
调用,除非它们的闭包中还有更多未显示的代码。更新
在评论中,我们发现使用上面的解决方案会导致应用程序"冻结"。这表明
saveImagesToFireBaseStorage
在调用savePage(model:savingHandler)
的同一个队列上调用它的完成处理程序,而且几乎可以肯定是DispatchQueue.main
。问题是DispatchQueue.main
是一个 * 串行 * 队列(就像newQueue
一样),这意味着它在run循环的下一次迭代之前不会执行任何任务,但它永远不会执行,因为它调用semaphore.wait()
,阻塞等待saveImagesToFireBaseStorage
的完成处理程序调用semaphore.signal
。通过等待,它阻止了它所等待的东西的执行。你在评论中说使用
async
/await
解决了这个问题,这可能是最干净的方法,原因有很多,其中一个重要的原因是你可以在编译时检查很多潜在的问题。与此同时,我使用
DispatchSemaphore
提出了这个解决方案。我将把它放在这里,以防它对某人有所帮助。首先,我将
newQueue
的创建从savePage
中移出。创建调度队列是一种繁重的操作,因此您应该一次性创建所需的队列,然后重用它们。我假设它是一个全局变量或拥有savePage
的任何对象的示例属性。第二件事是
savePage
不再阻塞了,但是我们仍然想要顺序行为,最好不要进入完成处理程序的地狱(深度嵌套的完成处理程序)。我将调用
saveImagesToFireBaseStorage
的代码重构为一个本地函数,并通过使用DispatchSemaphore
阻塞直到调用其完成处理程序来使其行为同步,但仅限于该本地函数。我确实在该函数外部创建了DispatchSemaphore
,以便可以在两次调用中重用同一个示例。但我把它当作嵌套函数中的局部变量。我还必须为
wait
使用一个time-out,因为我不知道是否可以假定saveImagesToFireBaseStorage
的完成处理程序总是被调用。是否存在不会被调用的故障条件?超时值几乎肯定是错误的。并且应该被视为实际值的占位符。我们需要根据您对应用及其工作环境(服务器、网络等)的了解,确定您希望允许的最大延迟。local函数使用一个键路径来允许设置
PageModel
的不同名称的属性(picURL1
与picURL2
),同时仍然合并重复的代码。下面是重构后的
savePage
代码:值得注意的是,
savePage
不会阻塞被调用的线程,我认为是DispatchQueue.main
。我假设在调用savePage
之后顺序调用的任何代码(如果有的话)都不依赖于调用savePage
的结果。任何依赖于它的代码都应该在它的savingHandler
中。说到
savingHandler
,我必须假设它可能会更新UI,并且由于它将被调用的点位于newQueue.async
的闭包中,因此必须在DispatchQueue.main
上显式调用它,所以我这样做了。