You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 17:17:00