簡體   English   中英

結構(不變性)與線程安全有何關系

[英]How is struct (immutability) related to thread safety

我讀過一篇關於Swift中不變性/結構的文章 有一部分說:

在函數式編程中,副作用通常被認為是不好的,因為它們可能以意想不到的方式影響您的代碼。 例如,如果在多個位置引用了一個對象,則每個更改都會自動在每個位置發生。 正如我們在簡介中所看到的,在處理多線程代碼時,這很容易導致錯誤:因為您可以從另一個線程修改您剛剛檢查的對象,所以所有假設都可能無效。

使用Swift結構,變異不會有相同的問題。 該結構的突變是局部副作用,僅適用於當前的結構變量。 因為每個struct變量都是唯一的(或者換句話說:每個struct值都只有一個所有者),所以幾乎不可能以這種方式引入錯誤。 除非您是跨線程引用全局結構變量,否則就是這樣。

在銀行帳戶示例中,有時必須更改或替換帳戶對象。 使用struct不足以保護帳戶免受多線程更新帳戶的困擾。 如果我們使用一些鎖或隊列,我們​​也可以使用class實例代替struct ,對嗎?

我嘗試編寫一些不確定導致競態條件的代碼。 主要思想是:兩個線程都首先獲取一些信息( Account的實例),然后根據獲取的內容覆蓋保存信息的變量。

struct Account {
    var balance = 0

    mutating func increase() {
        balance += 1
    }
}

class ViewController: NSViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        // method1()
        method2()
    }

    func method1() {
        var a = Account(balance: 3) {
            didSet {
                print("Account changed")
            }
        }

        // https://stackoverflow.com/questions/45912528/is-dispatchqueue-globalqos-userinteractive-async-same-as-dispatchqueue-main
        let queue = DispatchQueue.global()

        queue.async {
            a.increase()
            print("global: \(a.balance)")
        }

        DispatchQueue.main.async {
            a.increase()
            print("main: \(a.balance)")
        }
    }

    func method2() {
        var a = Account(balance: 3) {
            didSet {
                print("Account changed")
            }
        }

        func updateAccount(account : Account) {
            a = account
        }

        let queue = DispatchQueue.global()

        queue.async { [a] in
            var temp = a
            temp.increase()
            updateAccount(account: temp) // write back
            print("global: \(temp.balance)")
        }

        DispatchQueue.main.async { [a] in
            var temp = a
            temp.increase()
            updateAccount(account: temp)
            print("main: \(temp.balance)")
        }
    }
}

如果我們不修改變量a (或者只是使用let將其設為常數), 那么對於這樣一個簡單的結構,就線程安全而言,使用struct代替class的好處是什么? 不過, 可能還有其他好處

在現實世界中, a應在隊列中進行修改,或者通過一些鎖保護,但哪里的想法“不變性與線程安全幫助”從何而來?

在上面的示例中,兩個線程是否仍有可能獲得相同的值,從而產生錯誤的結果?

引用StackExchange上的答案

不變性讓您一件事:您可以自由讀取不可變對象,而不必擔心其下方的狀態發生變化

那么好處是只讀的嗎?

Java中也存在類似的問題,例如, 不變性可以確保線程安全嗎? 不可變對象是線程安全的,但是為什么呢?

我也在Java上找到了一篇有關該主題的好文章

其他資源:

我不是100%肯定我了解您要問的問題,但是由於我最近一直在研究Swift結構,因此我還是會搖擺不定,覺得我為您提供了一個可靠的答案。 關於Swift中的值類型,這里似乎有些混亂。

首先,您的代碼不會導致爭用條件,因為正如您在注釋中指出的那樣,結構是值類型,因此是按拷貝分配的(也就是,當分配給新變量時,將創建值類型的深層副本)。 這里的每個線程(隊列)將具有分配給temp的結構自己的線程本地副本,默認情況下,該副本可防止出現競爭狀況(這有助於結構的線程安全)。 永遠不會從外部線程訪問(或訪問)它們。

即使我們調用了mutating方法,我們也無法真正在Swift中對結構進行突變,因為創建了一個新方法,對嗎?

不一定正確。 從Swift文檔:

但是,如果需要在特定方法中修改結構或枚舉的屬性,則可以選擇對該方法進行行為更改。 然后,該方法可以從方法內部改變(即更改)其屬性,並在方法結束時將其所做的任何更改寫回到原始結構 該方法還可以為其隱式的self屬性分配一個全新的實例,並且該新實例將在方法結束時替換現有實例。

我的意思是有時實際上會對原始結構的值進行更改,並且在某些情況下可以為self分配一個全新的實例。 這些都不能為多線程提供直接的“好處”,而僅僅是Swift中值類型的工作方式。 實際上,“好處”來自“選擇加入”到易變的行為。

“不變性有助於線程安全”的思想從何而來?

同樣,價值類型強調價值高於身份的重要性,因此我們應該能夠對其價值做出一些可靠的假設。 如果值類型默認情況下是可變的,則對值(及其值)的預測/假設將變得更加困難。 如果某些東西是不可變的,則推理和做出假設變得容易得多。 我們可以假定一個不變值在傳遞給它的任何線程中都是相同的。

如果我們必須“啟用”可變性,則可以更輕松地查明值類型的更改來自何處,並有助於減少因意外更改值而產生的錯誤。 此外,由於我們確切知道哪些方法可以進行更改,並且(希望)知道變異方法將在什么條件/情況下運行,因此它仍然相對容易地推理和對值進行預測。

這與我能想到的其他語言相反,例如C ++,在這種語言中,您暗示方法無法通過在方法標題的末尾添加關鍵字const來更改成員的值。

如果我們不修改變量a (或者, 僅使用let使其成為常量 ), 那么就這樣的簡單結構而言,就線程安全而言,使用struct代替類有什么好處?

如果我們要使用一類,即使是在您所描述的那種簡單情況下,那么即使使用let分配也不足以防止產生副作用。 它仍然是脆弱的,因為它是引用類型。 即使在同一線程內,對類類型的let分配也不會停止副作用。

class StringSim{
var heldString:String

init(held: String){
    heldString = held
}

func display(){
    print(heldString)
}

func append(str: String){
    heldString.append(str)
}
}
let myStr = StringSim(held:"Hello, playground")
var mySecStr = myStr
mySecStr.append(str: "foo")
myStr.display()
mySecStr.display()

此代碼產生以下輸出:

 Hello, playgroundfoo
 Hello, playgroundfoo

將上面的代碼與使用Strings(Swift Standard結構類型)的類似操作進行比較,您將看到結果的差異。

TL; DR可以很容易地預測不會改變或只能在特定情況下改變的值。

如果有任何更好的了解的人想糾正我的答案或指出我的錯誤,請放心。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM