How to create and use an actor in Swift 如何在 Swift 中创建和使用 actor

How to create and use an actor in Swift 如何在 Swift 中创建和使用 actor

温馨提示:本文最后更新于2025-02-10 16:41:12,某些文章具有时效性,若有错误或已失效,请在下方留言

Creating and using an actor in Swift takes two steps: create the type using actor rather than class or struct, then use await when accessing its properties or methods externally. Swift takes care of everything else for us, including ensuring that properties and methods must be accessed safely.
在 Swift 中创建和使用 actor 需要两个步骤:使用 actor 而不是 class 或 struct 创建类型,然后在外部访问其属性或方法时使用 await。Swift 会为我们处理其他所有事情,包括确保必须安全地访问属性和方法。

Let’s look at a simple example: authenticating a user using a remote server. We’d start by creating our actor:
让我们看一个简单的示例:使用远程服务器对用户进行身份验证。我们首先创建我们的 actor:

actor AuthenticationManager {
    var token: String?

    var isAuthenticated: Bool {
        token != nil
    }

    func authenticate(username: String, password: String) async throws {
        // Simulate authenticating a user.
        let url = URL(string: "https://hws.dev/authenticate")!
        let (data, _) = try await URLSession.shared.data(from: url)
        token = String(decoding: data, as: UTF8.self)
    }
}

And now we could create an instance of that actor and use it in two concurrent tasks, like this:
现在我们可以创建该 actor 的实例,并在两个并发任务中使用它,如下所示:

actor AuthenticationManager {
    var token: String?

    var isAuthenticated: Bool {
        token != nil
    }

    func authenticate(username: String, password: String) async throws {
        // Simulate authenticating a user.
        let url = URL(string: "https://hws.dev/authenticate")!
        let (data, _) = try await URLSession.shared.data(from: url)
        token = String(decoding: data, as: UTF8.self)
    }
}

let manager = AuthenticationManager()

let first = Task {
    do {
        // Attempt to log in.
        try await manager.authenticate(username: "twostraws", password: "samoyed123")

        // Get the user's authentication token.
        if let token = await manager.token {
            print("User token: \(token)")
        }
    } catch {
        print("Authentication failed: \(error.localizedDescription)")
    }
}

let second = Task {
    // Attempt to access the authentication status elsewhere.
    let authenticated = await manager.isAuthenticated
    print("Second task check: \(authenticated)")
}

await first.value
await second.value
下载图标
how-to-create-and-use-an-actor-in-swift-1.zip
zip文件
50.4K

We don’t know or care in what order those tasks run, or even if they run in parallel – using an actor makes sure they can be accessed in two separate tasks, because it automatically ensures access to properties like token and isAuthenticated are handled separately.
我们不知道也不关心这些任务以什么顺序运行,甚至不知道它们是否并行运行——使用 actor 可以确保它们可以在两个单独的任务中访问,因为它会自动确保对 token 和 isAuthenticated 等属性的访问被单独处理。

If we didn’t have an actor here – if we had used class AuthenticationManager, for example – then we would need to solve those two problems ourselves. It’s not hard, at least not in a simple way, but it’s error-prone and boring to do, so it’s great to be able to hand this work over to the Swift compiler to do for us.
如果我们这里没有 actor (例如,如果我们使用了 AuthenticationManager 类),那么我们需要自己解决这两个问题。这并不,至少不是以简单的方式,但它很容易出错且做起来很无聊,所以能够将这项工作交给 Swift 编译器为我们做真是太好了。

The canonical example of why data races are problematic – the one that is often taught in computer science degrees – is about bank accounts, because here data races can result in serious real-world problems.
数据竞争之所以存在问题的典型例子(计算机科学学位中经常教授的例子)是关于银行账户的,因为在这里,数据竞争会导致严重的现实问题。

To see why, here’s an example BankAccount class that handles sending and receiving money:
为了了解原因,下面是一个处理汇款和收款的示例 BankAccount 类:

class BankAccount {
    var balance: Decimal

    init(initialBalance: Decimal) {
        balance = initialBalance
    }

    func deposit(amount: Decimal) {
        balance = balance + amount
    }

    func transfer(amount: Decimal, to other: BankAccount) {
        // Check that we have enough money to pay
        guard balance >= amount else { return }

        // Subtract it from our balance
        balance = balance - amount

        // Send it to the other account
        other.deposit(amount: amount)
    }
}

let firstAccount = BankAccount(initialBalance: 500)
let secondAccount = BankAccount(initialBalance: 0)
firstAccount.transfer(amount: 500, to: secondAccount)
下载图标
how-to-create-and-use-an-actor-in-swift-2.zip
zip文件
50.1K

That’s a class, so Swift won’t do anything to stop us from accessing the same piece of data multiple times. So, what could actually happen here?
这是一个类,所以 Swift 不会做任何事情来阻止我们多次访问同一条数据。那么,这里到底会发生什么呢?

Well, in the worst case two parallel calls to transfer() would be called on the same BankAccount instance, and the following would occur:
好吧,在最坏的情况下,将在同一个 BankAccount 实例上调用对 transfer() 的两个并行调用,并且会发生以下情况:

  1. The first would check whether the balance was sufficient for the transfer. It is, so the code would continue.
    第一个将检查余额是否足以进行转账。是的,所以代码会继续。
  2. The second would also check whether the balance was sufficient for the transfer. It still is, so the code would continue.
    第二个还将检查余额是否足以进行转账。它仍然是,因此代码将继续。
  3. The first would then subtract the amount from the balance, and deposit it in the other account.
    然后,第一个将从余额中减去金额,并将其存入另一个账户。
  4. The second would then subtract the amount from the balance, and deposit it in the other account.
    然后第二个将从余额中减去金额,并将其存入另一个账户。

Do you see the problem there? Well, what happens if the account we’re transferring from contains $100, and we’re asked to transfer $80 to the other account? If we follow the steps above, both calls to transfer() will happen in parallel and see that there was enough for the transfer to take place, then both will transfer the money across.
您看到那里的问题了吗?那么,如果我们要转账的账户包含 100 美元,并且我们被要求将 80 美元转移到另一个账户,会发生什么情况?如果我们按照上述步骤作,对 transfer() 的两个调用将并行进行,并且看到有足够的资金进行转账,那么两个调用都将转移资金。

The end result is that our check for sufficient funds wasn’t useful, and one account ends up with -$60 – something that might incur fees, or perhaps not even be allowed depending on the type of account they have.
最终结果是,我们对足够资金的检查没有用,一个账户最终得到 -60 美元——这可能会产生费用,或者甚至可能不被允许,具体取决于他们拥有的账户类型。

If we switch this type to be an actor, that problem goes away. This means using actor BankAccount rather than class BankAccount, but also using async and await because we can’t directly call deposit() on the other bank account and instead need to post the request as a message to be executed later.
如果我们将此类型切换为 actor,则问题就会消失。这意味着使用 actor BankAccount 而不是 BankAccount 类,但也要使用 async 和 await,因为我们不能直接在另一个银行账户上调用 deposit(),而是需要将请求作为消息发布,以便稍后执行。

Here’s how that looks:  这是它的样子:

actor BankAccount {
    var balance: Decimal

    init(initialBalance: Decimal) {
        balance = initialBalance
    }

    func deposit(amount: Decimal) {
        balance = balance + amount
    }

    func transfer(amount: Decimal, to other: BankAccount) async {
        // Check that we have enough money to pay
        guard balance > amount else { return }

        // Subtract it from our balance
        balance = balance - amount

        // Send it to the other account
        await other.deposit(amount: amount)
    }
}

let firstAccount = BankAccount(initialBalance: 500)
let secondAccount = BankAccount(initialBalance: 0)
await firstAccount.transfer(amount: 500, to: secondAccount)
下载图标
how-to-create-and-use-an-actor-in-swift-3.zip
zip文件
50.2K

With that change, our bank accounts can no longer fall into negative values by accident, which avoids a potentially nasty result.
通过这一变化,我们的银行账户不会再意外地陷入负值,从而避免了潜在的令人讨厌的结果。

In other places, actors can prevent bizarre results that ought to be impossible. For example, what would happen if our example was a basketball team rather than a bank account, and we were transferring players rather than money?
在其他地方,actor 可以防止本应不可能的奇怪结果。例如,如果我们的示例是篮球队而不是银行账户,并且我们转移的是球员而不是金钱,会发生什么情况?

Without actors we could end up in the situation where we transfer the same player twice – Team A would end up without them, and Team B would have them twice!
如果没有 Actor,我们最终可能会遇到这样一种情况:我们转移了两次相同的玩家 – 团队 A 最终会没有他们,而团队 B 会有他们两次!

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享