如何在异步方法中实现线程安全?

问题描述 投票:0回答:3

我正在尝试读取钥匙串,但文档警告我以下内容:

SecItemCopyMatching 会阻塞调用线程,因此如果从主线程调用,可能会导致应用程序的 UI 挂起。相反,从后台调度队列或异步函数调用 SecItemCopyMatching。

来源

所以我想写一个在后台运行的异步方法。

actor Keychain {
    public static let standard = Keychain()
    
    public enum Error: Swift.Error {
        case failed(String)
    }
    
    public func get(_ key: String) async throws -> Data? {
        let backgroundTask = Task(priority: .background) {
            var query: [String: Any] = [
                type(of: self).klass       : kSecClassGenericPassword,
                type(of: self).attrAccount : key,
                type(of: self).matchLimit  : kSecMatchLimitOne,
                type(of: self).returnData  : kCFBooleanTrue as CFBoolean
            ]
            
            var item: CFTypeRef?
            let status = SecItemCopyMatching(query as CFDictionary, &item)
            
            guard status == errSecSuccess else {
                if let errorMessage = SecCopyErrorMessageString(status, nil) {
                    throw Error.failed(String(errorMessage))
                } else {
                    throw Error.failed("unsupported")
                }
            }
            
            return item as? Data
        }
        return try await backgroundTask.value
    }
}

我的问题是..演员是否已经使其线程安全?

通常我会添加一个

NSLock
以确保安全。

public func get(_ key: String) async throws -> Data? {
    lock.lock()
    defer { lock.unlock() }
    
    (...)
    
    return try await task.value
}

但是现在我收到警告

Instance method 'lock' is unavailable from asynchronous contexts; Use async-safe scoped locking instead; this is an error in Swift 6

那么我如何才能实现这一目标?

swift asynchronous concurrency actor
3个回答
6
投票

是的,演员将使其线程安全。不需要锁之类的。请参阅 WWDC 2021 视频使用 Swift Actor 保护可变状态


顺便说一句,既然你已经在演员身上得到了这个,你真的不需要制作

get(_:)
一个
async
方法。 Actor 已经在后台线程上运行。因此,删除
async
限定符,然后删除
Task

actor Keychain {
    ...
    
    public func get(_ key: String) throws -> Data? {
        var query: [String: Any] = [
            type(of: self).klass       : kSecClassGenericPassword,
            type(of: self).attrAccount : key,
            type(of: self).matchLimit  : kSecMatchLimitOne,
            type(of: self).returnData  : kCFBooleanTrue as CFBoolean
        ]
        
        var item: CFTypeRef?
        let status = SecItemCopyMatching(query as CFDictionary, &item)
        
        guard status == errSecSuccess else {
            if let errorMessage = SecCopyErrorMessageString(status, nil) {
                throw Error.failed(String(errorMessage))
            } else {
                throw Error.failed("unsupported")
            }
        }
        
        return item as? Data
    }
}

顺便说一句,虽然在 Swift 并发中锁通常是不必要的,因为我们通常与 actor 实现类似的同步,但在某些边缘情况下我们可能会使用它们。在那些罕见的情况下,我们只需要小心使用它们。例如,编译器会警告我们,Swift 6 中可能不允许以下操作:

// Warning: Instance method 'lock' is unavailable from asynchronous contexts;
// Use async-safe scoped locking instead; this is an error in Swift 6

lock.lock()
someSynchronousStuff()
lock.unlock()

但是这是安全的,因为编译器可以推断锁的本地使用:

// OK

lock.withLock {
    someSynchronousStuff()
}

但是您不能从内部调用异步例程

withLock
(因为我们从不想要跨并发上下文边界使用锁:

// Error: Cannot pass function of type '() async -> ()' to parameter expecting
// synchronous function type

lock.withLock {
    await someSynchronousStuff()
}

例如,以下是 Xcode 15.3 产生的警告/错误:


0
投票

看起来您正在使用所有局部变量(没有实例或静态变量),所以应该没问题。


0
投票

为什么要把Keychain操作放在不需要的异步块中。没有它你就可以走了。钥匙串操作是线程安全的,这是由 Apple 保证的。请参阅此文档。 苹果

因此,不要使用异步块,并将所有 ui 更改放在主线程中(如果还没有)。 UI 更改需要在主线程中进行。

© www.soinside.com 2019 - 2024. All rights reserved.