swift 如何在调度信号量内部更改外部的值(已编辑)

4ioopgfo  于 2023-01-08  发布在  Swift
关注(0)|答案(1)|浏览(129)

我有一个问题,我会尽可能清楚地告诉你:
我需要一个对象的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指出它。

tmb3ates

tmb3ates1#

让我们看一下您的第一个async调用:

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()
}

我猜内部闭包,也就是传递给saveImagesToFireBaseStorage的闭包,也是异步调用的,如果我猜对了,那么saveImagesToFireBaseStorage几乎立即返回,执行signal,但是内部闭包还没有运行,所以新值还没有设置,然后经过一段延迟,内部闭包最终被调用,但这是在依赖于page.picURL1的"外部"代码已经运行之后,因此page.picURL1最终是在之后设置的。
所以你需要在内部闭包中调用signal,但是你仍然需要处理内部闭包没有被调用的情况,我的想法是这样的:

newQueue.async {
    if let picURL1 = model.picURL1 {
        self.saveImagesToFireBaseStorage(pictureURL: picURL1) { urlString in
            let url = urlString 
            page.picURL1 = url    
            print(page.picURL1!)
            semaphore.signal() // <--- ADDED THIS
        }
        /*
        If saveImagesToFireBaseStorage might not call the closure,
        such as on error, you need to detect that and call signal here
        in the case as well, or perhaps in some closure that's called in 
        the failure case.  That depends on your design.
        */
    }
    else { semaphore.signal() } // <--- MOVED INTO `else` block
}

您的第二个async需要进行类似的修改。
我注意到你没有在第二个async之后调用wait,第二个async设置了page.picURL2,所以你有2个wait调用,但是有3个signal调用,这不会影响page.picURL1在第一个async中是否设置正确。但这确实意味着semaphore在代码示例的末尾将具有不平衡的等待和信号,并且wait在第三个async之后的阻塞行为可能与您所期望的不同。
如果您的项目可以选择使用asyncawait关键字进行重构,则可以通过更易于维护的方式解决问题,因为这样可以完全消除对信号量的需要。
另外,如果我关于异步调用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的不同名称的属性(picURL1picURL2),同时仍然合并重复的代码。
下面是重构后的savePage代码:

func savePage(model: PageModel, savingHandler: @escaping (Bool) -> Void)
{
    // some  code .....
    
    var page = model
    
    let saveImageDone = DispatchSemaphore(value: 0)
    let waitTimeOut = DispatchTimeInterval.microseconds(500)

    func saveModelImageToFireBaseStorage(
        from urlPath: WritableKeyPath<PageModel, String?>)
    {
        if let picURL = model[keyPath: urlPath]
        {
            saveImagesToFireBaseStorage(pictureURL: picURL)
            {
                page[keyPath: urlPath] = $0
                print("page.\(urlPath) = \(page[keyPath: urlPath]!)")
                saveImageDone.signal()
            }
            
            if .timedOut == saveImageDone.wait(timeout: .now() + waitTimeOut) {
                print("saveImagesToFireBaseStorage timed out!")
            }
        }
    }
    
    newQueue.async
    {
        saveModelImageToFireBaseStorage(from: \.picURL1)
        saveModelImageToFireBaseStorage(from: \.picURL2)
        print(page.picURL1!)
        
        do {
            try self.pageDocumentRef.setData(from: page)
            
            // Assume handler might do UI stuff, so it needs to execute
            // on main
            
            DispatchQueue.main.async { savingHandler(true) }
        } catch let error {
            print("Error writing city to Firestore: \(error)")
            
            // Should savingHandler(false) be called here?
        }
    }
}

值得注意的是,savePage不会阻塞被调用的线程,我认为是DispatchQueue.main。我假设在调用savePage之后顺序调用的任何代码(如果有的话)都不依赖于调用savePage的结果。任何依赖于它的代码都应该在它的savingHandler中。
说到savingHandler,我必须假设它可能会更新UI,并且由于它将被调用的点位于newQueue.async的闭包中,因此必须在DispatchQueue.main上显式调用它,所以我这样做了。

相关问题