#作業練習【ChaoCode】 Swift 中級篇 21:Actor 作業

Dong
23 min readDec 21, 2022

--

題目內容取自於 : ChaoCode 頻道

  1. Data Race 和 Reentrancy 是什麼?
Data Race:當有多個 thread 同時嘗試存取某個資料,並且其中一個操作是修改,導致資料不正確甚至錯誤的情況。
Reentrancy:表示一段程式碼被暫停後再重新進入不會影響結果。也就是等待期間不會發生影響這段程式碼的事情,導致判斷、運算結果錯誤的情況。
(解答)

2. 官方形容 Actor 是什麼?

大海中的島嶼。(解答)

3. Actor 幫助我們避免的問題是什麼?

Data Race。(解答)

4. Actor 內部和外部使用有什麼差別?

 內部使用都是同步語法;外部使用如果是「isolated」的屬性或方法都必須要 await。(解答)

5. isolated 是什麼意思?

表示獨立於某一個 actor,只有那一個 actor 可以和這個資料互動。(解答)

Notes:

Actor -

actor官方說明-

isolated & nonisolated-

外面 呼叫含有 isolated function 的時候,還是要加上await

比較-

第一種await執行完回傳不是固定的,第二種能夠確保func內是一起動作的

globalActor-

使用範例-

沒有使用 actor的情況

import _Concurrency

class BankAccount {

let name: String
var balance = 1000

init(_ name: String) { self.name = name }

// 提款
func withdraw(_ amount: Int) -> Int {
if amount > balance {
print("🙈 \(name) 存款只剩 \(balance) 元,無法提款。")
return 0
}
balance -= amount
print("💵 \(name) 提款 \(amount) 元,剩下 \(balance) 元。")
return amount
}

// 存款
func deposit(_ amount: Int) -> Int {
balance += amount
print("\(name) 存款 \(amount) 元,目前存款為 \(balance) 元。")
return balance
}

func printBlance() {
print("\(name) 的餘額為: \(balance) 元。")
}
}


let familyAccount = BankAccount("家庭帳戶")

print("創建了 \(familyAccount.name)。")
print("一開始有: \(familyAccount.balance) 元")

Task {
await withTaskGroup(of: Void.self) { group in

(0...3).forEach{ number in
group.addTask {
familyAccount.withdraw(200)
familyAccount.deposit(100)
}
}

await group.waitForAll() // 跑到有錯誤
familyAccount.printBlance()
}
}


/* 輸出結果:
創建了 家庭帳戶。
一開始有: 1000 元
💵 家庭帳戶 提款 200 元,剩下 800 元。
💵 家庭帳戶 提款 200 元,剩下 600 元。
家庭帳戶 存款 100 元,目前存款為 700 元。
💵 家庭帳戶 提款 200 元,剩下 500 元。
💵 家庭帳戶 提款 200 元,剩下 300 元。
家庭帳戶 存款 100 元,目前存款為 400 元。
家庭帳戶 存款 100 元,目前存款為 500 元。
家庭帳戶 存款 100 元,目前存款為 600 元。
家庭帳戶 的餘額為: 600 元。
*/

// 可以看出執行順序不一定會 提款 -> 存款 -> 提款 -> 存款...

解法(1) : 使用 actor 加上一個 同步的方法(提款+存款執行完後一起回傳)

import _Concurrency

// 將 class 改為 actor
actor BankAccount {

let name: String
var balance = 1000

init(_ name: String) { self.name = name }

func withdraw(_ amount: Int) -> Int {
if amount > balance {
print("🙈 \(name) 存款只剩 \(balance) 元,無法提款。")
return 0
}
balance -= amount
print("💵 \(name) 提款 \(amount) 元,剩下 \(balance) 元。")
return amount
}

func deposit(_ amount: Int) -> Int {
balance += amount
print("\(name) 存款 \(amount) 元,目前存款為 \(balance) 元。")
return balance
}

func printBlance() {
print("\(name) 的餘額為: \(balance) 元。")
}

// 新增 同步方法(外部要加 await)
func syncActions() {
print("- - - - - - - - - - - - Strat")
withdraw(200)
deposit(100)
}
}


let familyAccount = BankAccount("家庭帳戶")

print("創建了 \(familyAccount.name)。")


Task {
print("一開始有: \(await familyAccount.balance) 元") // 等待 actor 執行回傳
await withTaskGroup(of: Void.self) { group in

(0...3).forEach{ number in
group.addTask {
await familyAccount.syncActions() // 外部加 await
}
}

await group.waitForAll() // 跑到有錯誤
await familyAccount.printBlance()
}
}

/* 輸出結果:
創建了 家庭帳戶。
一開始有: 1000 元
- - - - - - - - - - - - Strat
💵 家庭帳戶 提款 200 元,剩下 800 元。
家庭帳戶 存款 100 元,目前存款為 900 元。
- - - - - - - - - - - - Strat
💵 家庭帳戶 提款 200 元,剩下 700 元。
家庭帳戶 存款 100 元,目前存款為 800 元。
- - - - - - - - - - - - Strat
💵 家庭帳戶 提款 200 元,剩下 600 元。
家庭帳戶 存款 100 元,目前存款為 700 元。
- - - - - - - - - - - - Strat
💵 家庭帳戶 提款 200 元,剩下 500 元。
家庭帳戶 存款 100 元,目前存款為 600 元。
家庭帳戶 的餘額為: 600 元。
*/

// 成功地將 提款 + 存款變成一組同步回傳

syncActions方法 拉到外面,添加一個參數account 類型為 BankAccount,使用 isolated,讓方法被丟進來的 actor 執行

import _Concurrency

actor BankAccount {

let name: String
var balance = 1000

init(_ name: String) { self.name = name }

func withdraw(_ amount: Int) -> Int {
if amount > balance {
print("🙈 \(name) 存款只剩 \(balance) 元,無法提款。")
return 0
}
balance -= amount
print("💵 \(name) 提款 \(amount) 元,剩下 \(balance) 元。")
return amount
}

func deposit(_ amount: Int) -> Int {
balance += amount
print("\(name) 存款 \(amount) 元,目前存款為 \(balance) 元。")
return balance
}

func printBlance() {
print("\(name) 的餘額為: \(balance) 元。")
}

}

// 整個 function內 會被丟進來的 actor 執行
func syncActions(account: isolated BankAccount) {
print("- - - - - - - - - - - - Strat")
account.withdraw(200)
account.deposit(100)
}


let familyAccount = BankAccount("家庭帳戶")

print("創建了 \(familyAccount.name)。")


Task {
print("一開始有: \(await familyAccount.balance) 元") // 等待 actor 執行回傳
await withTaskGroup(of: Void.self) { group in

(0...3).forEach{ number in
group.addTask {
await syncActions(account: familyAccount) // 獨立 讓 familyAccount(actor) 執行,加上await等待actor執行完回傳
}
}

await group.waitForAll() // 跑到有錯誤
await familyAccount.printBlance()
}
}

/* 執行結果:
創建了 家庭帳戶。
一開始有: 1000 元
- - - - - - - - - - - - Strat
💵 家庭帳戶 提款 200 元,剩下 800 元。
家庭帳戶 存款 100 元,目前存款為 900 元。
- - - - - - - - - - - - Strat
💵 家庭帳戶 提款 200 元,剩下 700 元。
家庭帳戶 存款 100 元,目前存款為 800 元。
- - - - - - - - - - - - Strat
💵 家庭帳戶 提款 200 元,剩下 600 元。
家庭帳戶 存款 100 元,目前存款為 700 元。
- - - - - - - - - - - - Strat
💵 家庭帳戶 提款 200 元,剩下 500 元。
家庭帳戶 存款 100 元,目前存款為 600 元。
家庭帳戶 的餘額為: 600 元。
*/

// 和解法(1)一樣的結果

用 nonisolated 搭配 protocol -

// CustomStringConvertible、Hashable 為例


extension BankAccount: CustomStringConvertible, Hashable {
// 加上 nonisolated 後 會檢查裡面是否都是 nonisolated 的 (沒有用到 isolated 的東西)
nonisolated var description: String {
name
}

// 靜態方法是獨立於 actor 的存在,所以本來就是 nonisolated 的
static func == (lhs: BankAccount, rhs: BankAccount) -> Bool {
lhs.name == rhs.name
}

// 加上 nonisolated 會檢查裡面是否都是 nonisolated 的 (沒有用到 isolated 的東西)
nonisolated func hash(into hasher: inout Hasher) {
hasher.combine(name)
}
}

兩個Actor互動情況-

(1) 使用 class 模擬一個 互相給錢的情況 (還沒使用actor)

import _Concurrency

class 玩家 {
var name: String
var money: Int

func give(_ other: 玩家, amount: Int) {
self.money -= amount
other.money += amount

print(">>> 交易後資產: \(name) \(money) 元 ; \(other.name) \(other.money) 元。")
}

init(name: String, money: Int) {
self.name = name
self.money = money
}
}

let player = 玩家(name: "DONG", money: 100)
let rock = 玩家(name: "🗿", money: 0)

Task{
await withTaskGroup(of: Void.self) { group in
(0..<15).forEach{ _ in

group.addTask {
player.give(rock, amount: 50)
}

group.addTask {
rock.give(player, amount: 50)
}
}

await group.waitForAll()
print("剩下的總金額: \(player.money + rock.money)")
}
}

/* 輸出結果:
>>> 交易後資產: DONG 50 元 ; 🗿 50 元。
>>> 交易後資產: 🗿 0 元 ; DONG 100 元。
>>> 交易後資產: DONG 50 元 ; 🗿 50 元。
>>> 交易後資產: 🗿 0 元 ; DONG 100 元。
>>> 交易後資產: 🗿 -50 元 ; DONG 150 元。
>>> 交易後資產: DONG 100 元 ; 🗿 0 元。
>>> 交易後資產: DONG 50 元 ; 🗿 0 元。
>>> 交易後資產: 🗿 -50 元 ; DONG 100 元。
>>> 交易後資產: DONG 50 元 ; 🗿 0 元。
>>> 交易後資產: 🗿 -50 元 ; DONG 100 元。
>>> 交易後資產: 🗿 -50 元 ; DONG 100 元。
>>> 交易後資產: 🗿 -100 元 ; DONG 150 元。
>>> 交易後資產: DONG 50 元 ; 🗿 50 元。
>>> 交易後資產: DONG 50 元 ; 🗿 0 元。
>>> 交易後資產: 🗿 -50 元 ; DONG 100 元。
>>> 交易後資產: DONG 100 元 ; 🗿 -50 元。
剩下的總金額: 50
*/

// 互相給對方 50 元,最終金額與想要的執行結果不同

(2) 使用 actor , 避免Data Race

(最終執行完金額正確,過程中的await的顯示結果會很奇怪)

import _Concurrency

// class 改為 actor
actor 玩家 {
var name: String
var money: Int

// 因為給對方$$會更改到對方的金額,而2個不同的actor不能直接更改對方 所以使用function,呼叫這個 function的時候也要加上 await
func addMoney(_ amount: Int) {
money += amount
}

func give(_ other: 玩家, amount: Int) async {
self.money -= amount
// other.money += amount
// 將添加錢改用 func 處理,因為要await,所以give這個function也要加上 async
await other.addMoney(amount)

// name、money 都是 await var,等待 actor 處理完回傳
print(">>> 交易後資產: \(name) \(money) 元 ; \(await other.name) \(await other.money) 元。")
}

init(name: String, money: Int) {
self.name = name
self.money = money
}
}

let player = 玩家(name: "DONG", money: 100)
let rock = 玩家(name: "🗿", money: 0)

Task{
await withTaskGroup(of: Void.self) { group in
(0..<1500).forEach{ _ in

group.addTask {
await player.give(rock, amount: 50)
}

group.addTask {
await rock.give(player, amount: 50)
}
}

await group.waitForAll()
print("剩下的總金額: \((await player.money) + (await rock.money))") // 等待顯示
}
}




/* 輸出結果:
>>> 交易後資產: DONG 50 元 ; 🗿 0 元。
>>> 交易後資產: 🗿 -50 元 ; DONG 100 元。
>>> 交易後資產: DONG 100 元 ; 🗿 -150 元。
>>> 交易後資產: 🗿 -50 元 ; DONG 100 元。
>>> 交易後資產: DONG 100 元 ; 🗿 -100 元。
>>> 交易後資產: 🗿 0 元 ; DONG -200 元。
>>> 交易後資產: 🗿 -100 元 ; DONG 100 元。
>>> 交易後資產: DONG -300 元 ; 🗿 0 元。
>>> 交易後資產: 🗿 -50 元 ; DONG 100 元。
>>> 交易後資產: 🗿 0 元 ; DONG 100 元。
>>> 交易後資產: DONG -350 元 ; 🗿 0 元。
>>> 交易後資產: DONG -350 元 ; 🗿 0 元。
>>> 交易後資產: 🗿 0 元 ; DONG -50 元。
>>> 交易後資產: DONG -300 元 ; 🗿 0 元。
.....
......
.......
........
>>> 交易後資產: DONG 100 元 ; 🗿 0 元。
>>> 交易後資產: DONG 100 元 ; 🗿 0 元。
>>> 交易後資產: DONG 100 元 ; 🗿 0 元。
>>> 交易後資產: DONG 100 元 ; 🗿 0 元。
>>> 交易後資產: DONG 100 元 ; 🗿 0 元。
>>> 交易後資產: DONG 100 元 ; 🗿 0 元。
>>> 交易後資產: DONG 100 元 ; 🗿 0 元。
剩下的總金額: 100
*/

// 從執行結果來看actor只能避免,沒辦法確保Reentrancy的發生,
// 設計時要先考慮 Reentrancy 會不會影響到功能

GlobalActor使用情境-

(1) 將錢的部分使用MainActor處理(還沒使用GlobalActor)

import _ConCurrency


class 玩家 {
var name: String
@MainActor var money: Int

@MainActor
func give(_ other: 玩家, amount: Int) {
self.money -= amount
other.money += amount

print(">>> 交易後資產: \(name) \(money) 元 ; \(other.name) \(other.money) 元。")
}

init(name: String, money: Int) {
self.name = name
self.money = money
}
}

let player = 玩家(name: "DONG", money: 100)
let rock = 玩家(name: "🗿", money: 0)

Task{
await withTaskGroup(of: Void.self) { group in
(0..<1000).forEach{ _ in

group.addTask {
await player.give(rock, amount: 50)
}

group.addTask {
await rock.give(player, amount: 50)
}
}

await group.waitForAll()
print("剩下的總金額: \((await player.money) + (await rock.money))")
}
}


/* 輸出結果:
>>> 交易後資產: DONG 50 元 ; 🗿 50 元。
>>> 交易後資產: 🗿 0 元 ; DONG 100 元。
>>> 交易後資產: DONG 50 元 ; 🗿 50 元。
>>> 交易後資產: 🗿 0 元 ; DONG 100 元。
>>> 交易後資產: DONG 50 元 ; 🗿 50 元。
>>> 交易後資產: 🗿 0 元 ; DONG 100 元。
>>> 交易後資產: DONG 50 元 ; 🗿 50 元。
>>> 交易後資產: 🗿 0 元 ; DONG 100 元。
>>> 交易後資產: DONG 50 元 ; 🗿 50 元。
>>> 交易後資產: DONG 0 元 ; 🗿 100 元。
.....
......
.......
........

>>> 交易後資產: 🗿 0 元 ; DONG 100 元。
>>> 交易後資產: DONG 50 元 ; 🗿 50 元。
>>> 交易後資產: 🗿 0 元 ; DONG 100 元。
>>> 交易後資產: DONG 50 元 ; 🗿 50 元。
>>> 交易後資產: 🗿 0 元 ; DONG 100 元。
>>> 交易後資產: DONG 50 元 ; 🗿 50 元。
>>> 交易後資產: 🗿 0 元 ; DONG 100 元。
剩下的總金額: 100
*/

// 可以發現不會再有誇張金額

(2) 將MainActor 改為 TradingActor (globalActor)

這樣就不會佔用到主執行緒

import _Concurrency

@globalActor
actor TradingActor {
static var shared = TradingActor()

}


class 玩家 {
var name: String
@TradingActor var money: Int

@TradingActor
func give(_ other: 玩家, amount: Int) {
self.money -= amount
other.money += amount

print(">>> 交易後資產: \(name) \(money) 元 ; \(other.name) \(other.money) 元。")
}

init(name: String, money: Int) {
self.name = name
self.money = money
}
}

let player = 玩家(name: "DONG", money: 100)
let rock = 玩家(name: "🗿", money: 0)

Task{
await withTaskGroup(of: Void.self) { group in
(0..<1000).forEach{ _ in

group.addTask {
await player.give(rock, amount: 50)
}

group.addTask {
await rock.give(player, amount: 50)
}
}

await group.waitForAll()
print("剩下的總金額: \((await player.money) + (await rock.money))")
}
}

// 輸出結果與上面的MainActor相同
// 使用TradingActor(globalActor),把 MainActor 留給 UI更新的動作上

補充(延續範例添加股票買賣的情況):

Task-

Task 可以繼承優先度、local變數和Actor。

Global Actor: 預設直接繼承

Actor: 如果有用到 local 變數才繼承。

不想繼承的話要使用Task.detached。

範例:

(1)用forEach 跑一個計數器,並在每次迴圈完 +1完休息1秒


import _Concurrency
import Foundation

func goSleep(){
sleep(1)
print("睡飽了")
}


class Counter {
var number = 0

func increace() {
number += 1
print("計數器更新為 \(number)")
}
}

let counter = Counter()
for _ in 1...10 {
Task { @MainActor in
counter.increace()
goSleep()
}
}

/* 輸出結果:
計數器更新為 1
睡飽了
計數器更新為 2
睡飽了
計數器更新為 3
睡飽了
計數器更新為 4
睡飽了
計數器更新為 5
睡飽了
計數器更新為 6
睡飽了
計數器更新為 7
睡飽了
計數器更新為 8
睡飽了
計數器更新為 9
睡飽了
計數器更新為 10
睡飽了
*/


// 如果把這段 Code
for _ in 1...10 {
Task { @MainActor in
counter.increace()
goSleep()
}
}

// 改成 👇🏻
for _ in 1...10 {
Task { @MainActor in
counter.increace()
// 這邊的 Task 繼承上面的 @MainActor
Task{
goSleep()
}
}
}

/* 執行結果:
計數器更新為 1
計數器更新為 2
計數器更新為 3
計數器更新為 4
計數器更新為 5
計數器更新為 6
計數器更新為 7
計數器更新為 8
計數器更新為 9
計數器更新為 10
睡飽了
睡飽了
睡飽了
睡飽了
睡飽了
睡飽了
睡飽了
睡飽了
睡飽了
睡飽了
*/

(2) 使用Task.detached 放棄繼承

import _Concurrency
import Foundation

func goSleep(){
sleep(1)
print("睡飽了")
}


class Counter {
var number = 0

func increace() {
number += 1
print("計數器更新為 \(number)")
}
}

let counter = Counter()
for _ in 1...10 {
Task { @MainActor in
counter.increace()
Task.detached {
goSleep()
}
}
}


/* 輸出結果:
計數器更新為 1
計數器更新為 2
計數器更新為 3
計數器更新為 4
計數器更新為 5
計數器更新為 6
計數器更新為 7
計數器更新為 8
計數器更新為 9
計數器更新為 10
睡飽了
睡飽了
睡飽了
睡飽了
睡飽了
睡飽了
睡飽了
睡飽了
睡飽了
睡飽了
*/


// 速度會比上面一個繼承MainActor的範例速度還快
// 因為現在 goSleep() 不是在主執行序上執行

--

--