題目內容取自於 : ChaoCode 頻道
- 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-
比較-
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() 不是在主執行序上執行