线程安全的Swift单例,使用async/await和异步初始化。

huangapple go评论69阅读模式
英文:

thread safe swift singleton with async/ await and async init

问题

Here's the translated code portion as requested:

假设我有一个Swift类,写成这样,如何确保对shared()函数的访问是线程安全的?
class Singleton {
    static private var _shared: Singleton?
    static func shared() async -> Singleton {
        if let shared = _shared {
            return shared
        }
        let shared = await Singleton()
        _shared = shared
        return shared
    }
    
    private init() async {
        // Some mandatory async work
    }
}
我知道我们可以使用actors来处理实例,但如何处理静态成员?

我尝试过像这样做
@globalActor
struct SingletonActor {
    public actor SingletonActor {}
    public static let shared = SingletonActor()
}

然后将Singleton类标记为@SingletonActor,如下所示
@SingletonActor
class Singleton { ... }

但似乎也不起作用,我做错了什么?

EDIT: 根据评论的实际用例示例

基本上,我想确保对Firestore的所有访问都具有有效的身份验证。因此,我有一个AuthRules actor来验证当前用户。这在FirestoreServer actor的init中调用,以确保所有函数都已等待有效的身份验证。以下是实际代码,使用我不安全的静态函数访问方法。

actor AuthRules {
    
    static func shared() async -> AuthRules {
        if let shared = _shared {
            return shared
        }
        let shared = await AuthRules()
        return shared
    }
    static private var _shared: AuthRules?
    
    private init() async {
        await validateAnonymous()
    }
    
    private func signInAnonymously() async {
        do {
            print("AuthBackend.signInAnonymously: Attempting to sign in user anonymously")
            let result = try await Auth.auth().signInAnonymously()
            print("AuthBackend.signInAnonymously: Auth signed in as \(result.user.uid)")
        } catch(let error) {
            print("AuthBackend.signInAnonymously: Unable to sign in user anonymously \(error.localizedDescription)")
        }
    }
    
    private func validateAnonymous() async {
        if let currentUser = Auth.auth().currentUser, currentUser.isAnonymous {
            print("AuthBackend.validateAnonymous: Auth signed in as \(currentUser.uid)")
        } else {
            await signInAnonymously()
        }
    }
}

actor FirestoreServer {
    
    static func server() async -> FirestoreServer {
        if let server = _server {
            return server
        }
        let server = await FirestoreServer()
        return server
    }
    static private var _server: FirestoreServer?
    
    private let db = Firestore.firestore()
    private let authRules: AuthRules
    
    private init() async {
        authRules = await AuthRules.shared()
    }
    
    /* several async functions that communicate with firestore through db */
}

如果您有其他翻译需求,请告诉我。

英文:

Suppose I have a swift class written like this, how can I ensure the access to the shared() function is thread-safe?

class Singleton {
    static private var _shared: Singleton?
    static func shared() async -> Singleton {
        if let shared = _shared {
            return shared
        }
        let shared = await Singleton()
        _shared = shared
        return shared
    }
    
    private init() async {
        // Some mandatory async work
    }
}

I know that we can use actors for instances but how do I do this for static members?

I tried to do something like this

@globalActor
struct SingletonActor {
    public actor SingletonActor {}
    public static let shared = SingletonActor()
}

and then marked the Singleton class as @SingletonActor like

@SingletonActor
class Singleton { ... }

but that doesn't seem to be working either, what am I doing wrong?

EDIT: Practical use case example per comments

Essentially I want to ensure that all access to firestore first has valid authentication. So I have an AuthRules actor that validates the current user. This is called in the init of the FirestoreServer actor so all functions are ensured to have awaited for valid auth. Here is the practical code, using my unsafe static function access approach.

actor AuthRules {
    
    static func shared() async -> AuthRules {
        if let shared = _shared {
            return shared
        }
        let shared = await AuthRules()
        return shared
    }
    static private var _shared: AuthRules?
    
    private init() async {
        await validateAnonymous()
    }
    
    private func signInAnonymously() async {
        do {
            print("AuthBackend.signInAnonymously: Attempting to sign in user anonymously")
            let result = try await Auth.auth().signInAnonymously()
            print("AuthBackend.signInAnonymously: Auth signed in as \(result.user.uid)")
        } catch(let error) {
            print("AuthBackend.signInAnonymously: Unable to sign in user anonymously \(error.localizedDescription)")
        }
    }
    
    private func validateAnonymous() async {
        if let currentUser = Auth.auth().currentUser, currentUser.isAnonymous {
            print("AuthBackend.validateAnonymous: Auth signed in as \(currentUser.uid)")
        } else {
            await signInAnonymously()
        }
    }
}

actor FirestoreServer {
    
    static func server() async -> FirestoreServer {
        if let server = _server {
            return server
        }
        let server = await FirestoreServer()
        return server
    }
    static private var _server: FirestoreServer?
    
    private let db = Firestore.firestore()
    private let authRules: AuthRules
    
    private init() async {
        authRules = await AuthRules.shared()
    }
    
    /* several async functions that communicate with firestore through db */
}

答案1

得分: 1

第一个示例(具有异步方法初始化程序和“shared()”的异步呈现的class)绝对不是线程安全的。而且属性包装器方法也行不通。

如果你真的需要具有异步初始化程序的单例,我猜你可以这样做:

actor Foo {
    static private var task: Task<Foo, Never>?
    
    static func shared() async -> Foo {
        if let task {
            return await task.value
        }
        let task = Task { await Foo() }
        self.task = task
        return await task.value
    }
    
    private init() async {
        // 一些异步的事情,使用`await`
    }
}

毋庸置疑,这显然缺乏标准单例模式的优雅和简洁:

final class Foo {
    static let shared = Foo()
    
    private init() {
        // 标准同步初始化程序
    }
}

第一个片段背后的想法是使用actor来消除数据竞争,但由于actor是可重入的,我们await了原始实例化的Task

它能工作,但我很难想象在实际应用中我何时会想使用这样繁琐的东西。

英文:

The first example (a class with async method initializer and async rendition of shared()) is definitely not thread safe. And the property wrapper approach won’t work, either.

If you really needed to have a singleton with an async initializer, I guess you could do something like:

actor Foo {
    static private var task: Task&lt;Foo, Never&gt;?
    
    static func shared() async -&gt; Foo {
        if let task {
            return await task.value
        }
        let task = Task { await Foo() }
        self.task = task
        return await task.value
    }
    
    private init() async {
        // something asynchronous, with an `await`
    }
}

Needless to say, this obviously lacks the elegance and simplicity of the standard singleton pattern:

final class Foo {
    static let shared = Foo()
    
    private init() {
        // standard synchronous initializer
    }
}

The idea behind the first snippet is to use a actor to eliminate data races, but because actors are reentrant, we await a Task for the original instantiation.

It works, but I am hard pressed to think of a practical example where I would ever want to use something as cumbersome as this.

huangapple
  • 本文由 发表于 2023年5月7日 07:04:55
  • 转载请务必保留本文链接:https://go.coder-hub.com/76191553.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定