结构(不变性)与线程安全有何关系

swift multithreading struct thread-safety immutability

1101 观看

1回复

735 作者的声誉

我读过一篇关于Swift中不变性/结构的文章。有一部分说:

在函数式编程中,副作用通常被认为是不好的,因为它们可能以意想不到的方式影响您的代码。例如,如果在多个位置引用了一个对象,则每个更改都会自动在每个位置发生。正如我们在简介中所看到的,在处理多线程代码时,这很容易导致错误:因为您可以从另一个线程修改您刚刚检查的对象,所以所有假设可能都是无效的。

使用Swift结构,变异不会有相同的问题。该结构的突变是局部副作用,并且仅适用于当前的结构变量。因为每个struct变量都是唯一的(或者换句话说:每个struct值都有一个所有者),所以几乎不可能以这种方式引入错误。除非您是跨线程引用全局结构变量,否则就是这样。

在银行帐户示例中,有时必须更改或替换帐户对象。使用a 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上找到了一篇有关该主题的好文章

其他资源:

作者: LShi 的来源 发布者: 2017 年 9 月 15 日

回应 1


2

47 作者的声誉

我不是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可以 更轻松地预测不会改变或只能在特定情况下改变的值。

如果有任何更好的了解的人想纠正我的答案或指出我的错误,请放心。

作者: Jake 发布者: 2018 年 7 月 23 日
32x32