Swift 6中在nonisolated NSFilePresenter方法内调用async函数的方案
解决Swift 6中非Sendable类在NSFilePresenter方法中调用async函数的问题
问题背景
有一个非Sendable的类Foo,必须遵循NSFilePresenter协议(协议要求方法为nonisolated的@objc函数),需要调用异步的load()函数,但在Swift 6中遇到了并发安全相关的编译错误:
- 直接调用
load()提示“'async' call in a function that does not support concurrency” - 使用普通
Task调用提示数据竞争风险 - 指定
@MainActor的Task提示“Task or actor isolated value cannot be sent”
原始代码
// @MainActor is not an option here ! class Foo : NSObject { private var operationQueue: OperationQueue? // some other mutable properties, no way to make Foo conform to Sendable // this must stay async because I have to wait for a file coordinator, code not shown for brevity public func load() async throws -> sending String { let loadurl = URL(string: "bla")! var errorMain: NSError? let text = try await withCheckedThrowingContinuation { continuation in fileCoordinator.coordinate(readingItemAt: loadurl, options:.withoutChanges, error: &errorMain) { (readurl) in do { let text = try String(contentsOf: readurl) continuation.resume(returning: text) } catch { continuation.resume(throwing: MyError.internalError("file coordinator read operation failed")) } } if errorMain != nil { continuation.resume(throwing: MyError.internalError(errorMain!.localizedDescription)) } } return(text) } } extension Foo: NSFilePresenter { nonisolated var presentedItemURL: URL? { return URL(string: "Bla") } nonisolated var presentedItemOperationQueue: OperationQueue { if operationQueue == nil { operationQueue = OperationQueue() } return operationQueue! // unsafe, but not the problem here } nonisolated func presentedItemDidMove(to newURL: URL) { // Question: How to load() safely ? load() // ERROR: 'async' call in a function that does not support concurrency Task { // ERROR: Passing closure as a 'sending' parameter risks causing data races between code in the current task and concurrent execution of the closure await load() } Task { @MainActor in // ERROR: Task or actor isolated value cannot be sent await load() } } }
解决建议
1. 修复operationQueue的线程安全初始化
首先把operationQueue改成线程安全的懒加载串行队列,确保所有对类可变属性的访问都在同一个串行队列中执行,从根源避免数据竞争:
class Foo : NSObject { // 改成懒加载的串行队列,线程安全且确保串行执行 private lazy var operationQueue: OperationQueue = { let queue = OperationQueue() queue.maxConcurrentOperationCount = 1 // 串行执行,避免多线程访问属性冲突 queue.name = "com.yourdomain.Foo.FilePresenterQueue" return queue }() // ... 其他属性 }
2. 调整load()方法的并发逻辑
移除返回类型中多余的sending修饰符,同时确保load()内部的文件协调操作和属性访问都在operationQueue上执行(如果fileCoordinator是类属性,必须保证访问它的线程安全):
public func load() async throws -> String { let loadurl = URL(string: "bla")! return try await withCheckedThrowingContinuation { continuation in self.operationQueue.addOperation { var errorMain: NSError? self.fileCoordinator.coordinate(readingItemAt: loadurl, options: .withoutChanges, error: &errorMain) { readurl in do { let text = try String(contentsOf: readurl) continuation.resume(returning: text) } catch { continuation.resume(throwing: MyError.internalError("file coordinator read operation failed")) } } if let error = errorMain { continuation.resume(throwing: MyError.internalError(error.localizedDescription)) } } } }
3. 在presentedItemDidMove中安全调用load()
利用NSFilePresenter的presentedItemOperationQueue(已经是串行队列),使用Task并标记闭包为@unchecked Sendable(因为我们已经确保所有对self的访问都在同一个串行队列,不会有数据竞争),同时用weak self避免循环引用:
nonisolated func presentedItemDidMove(to newURL: URL) { Task { [weak self] @unchecked Sendable in guard let self = self else { return } do { try await self.load() // 加载成功后的逻辑 } catch { // 处理加载失败的错误 print("Load failed after file move: \(error.localizedDescription)") } } }
关键说明
@unchecked Sendable的使用是安全的:因为所有对Foo实例属性的访问都在operationQueue这个串行队列中执行,无论是NSFilePresenter的回调还是load()方法的逻辑,都不会出现多线程竞争。- 必须保持
presentedItemOperationQueue的串行性:这是NSFilePresenter协议的隐含要求,确保所有文件相关的回调都按顺序执行,同时也保证了类属性访问的线程安全。
内容的提问来源于stack exchange,提问作者Chris
相关产品推荐
相关产品推荐

