我是否需要为简单变量添加线程锁定?

时间:2017-07-04 08:39:45

标签: ios swift multithreading thread-safety

假设我有一个对象,多个线程可以读取/写入statesomeValue变量。如果这些变量是int,double,enums等类型,我是否需要添加锁定?

enum State: String {
  case one
  case two
}

class Object {
  var state: State
  var someValue: Double
}

5 个答案:

答案 0 :(得分:6)

是的,你这样做。

想象一下两个线程试图将1添加到someValue的情况。一个线程通过以下方式完成:

  1. someValue读入注册
  2. 添加1
  3. 写回someValue
  4. 如果两个线程在执行操作3之前执行操作1,那么与另一个线程执行操作1之前一个线程执行所有三个操作相比,您将获得不同的答案。

    还有一些更微妙的问题,因为优化编译器可能不会在一段时间内将修改后的值写回寄存器 - 如果有的话。此外,现代CPU具有多个核心,每个核心都有自己的缓存。 CPU将值写回内存并不能保证它能够立即进入内存。它可能只是核心的缓存。您需要所谓的内存屏障,以确保将所有内容整齐地写回主内存。

    在更大的范围内,您需要锁定以确保类中变量之间的一致性。因此,如果该状态旨在表示someValue的某些属性,例如它是否为整数,您需要锁定以确保每个人始终具有一致的视图,即

    1. 修改someValue
    2. 测试新值
    3. 相应地设置state
    4. 上述三个操作必须看似原子,或者如果在操作1之后但在操作3之前检查对象,则它将处于不一致状态。

答案 1 :(得分:4)

“需要锁定”需要根据您的预期安全起见。如果您需要以协调的方式更新多个值,则当然需要锁定。如果在多个线程上执行读/修改/写入,则需要锁定或使用可以记录另一个线程中断的特殊推测代码。对于单个值的简单使用,您可以使用特殊的原子操作。有时只设置一个值不需要锁定,但这取决于具体情况。

答案 2 :(得分:2)

JeremyP说的是什么,但你还需要考虑更高的层次:你的状态"和" someValue"可能是相关的。因此,如果我改变状态,然后更改someValue,整个对象的内容就在我改变" state"之后。可能是垃圾,因为新的状态与旧的someValue不匹配。

简单的解决方案是谷歌搜索如何做" @ synchronized"在Swift中,或调度到主线程,或调度到串行队列。

答案 3 :(得分:1)

为了模拟您的问题,我追踪了以下代码段(iOS App环境):

import UIKit

func delay (
    _ seconds: Double,
    queue: DispatchQueue = DispatchQueue.main,
    after: @escaping ()->()) {

    let time = DispatchTime.now() + Double(Int64(seconds * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC)
    queue.asyncAfter(deadline: time, execute: after)
}

class ViewController: UIViewController {
    var myValue = 0

    override func viewDidLoad() {
        super.viewDidLoad()

        addOneThousand()
        addOneThousand()
        addOneThousand()

        // calling this is just for logging the value after a delay
        // just for making sure that all threads execution is completed...
        delay(3.0) {
            print(self.myValue)
        }
    }

    func addOneThousand() {
        DispatchQueue(label: "com.myapp.myqueue").async {
            for _ in 0...999 {
                self.myValue += 1
            }

            print(self.myValue)
        }
    }
}

首先看一下,期望是:myValue的值应该是3000,因为addOneThousand()被调用了三次,但是在我的机器(模拟器)上运行应用程序10次之后 - 顺序,输出是:

1582 1582 1582 1582

3000 3000 3000 3000

2523 2523 2523 2523

2591 2591 2591 2591

1689 1689 1689 1689

1556 1556 1556 1556

1991 1991年 1991年 1991

1914 1914年 1914年 1914年

2416 2416 2416 2416

1889 1889年 1889年 1889年

最重要的是每个结果的第四个值(等待延迟后的输出)是大多数时间是意外的(不是3000)。如果我没有误会,我认为我们在这里所面临的是race condition

针对这种情况的适当解决方案是让线程的执行序列化;在修改addOneThousand()sync而不是async之后。您可能想要检查this answer):

func addOneThousand() {
    DispatchQueue(label: "com.myapp.myqueue").sync {
        for _ in 0...999 {
            self.myValue += 1
        }

        print(self.myValue)
    }
}

10次连续运行的输出是:

1000 2000 3000 3000

1000 2000 3000 3000

1000 2000 3000 3000

1000 2000 3000 3000

1000 2000 3000 3000

1000 2000 3000 3000

1000 2000 3000 3000

1000 2000 3000 3000

1000 2000 3000 3000

1000 2000 3000 3000

这代表了预期的结果。

我希望它有所帮助。

答案 4 :(得分:0)

严格地说,没有(虽然“是”也是一个有效的答案,对于一般情况可以说是更正确的答案。)

根据您的需要/需要,您可以使用诸如此类的函数来使用原子操作。 OSAtomicIncrement32OSAtomicCompareAndSwapPtr 未锁定

但是,请注意,即使一个操作是原子操作,两个单独原子操作,连续操作也完全是 原子。因此,例如,如果你想要一致地同时更新statesomeValue,那么如果正确性很重要,那么没有锁定是绝对不可能的(除非巧合,它们足够小以致你可以作弊并将它们挤压成一个更大的原子类型。)

另请注意,即使您需要锁定或使用原子操作以获得程序正确性,您也可以偶尔“远离”而不会这样做。这是因为在大多数平台上,正常对齐的内存地址的普通加载和存储无论如何都是原子的 然而,不要受到诱惑,这不是听起来那么好,实际上根本不是一件好事 - 依靠事情才会起作用(即使你“测试过”,并且一切正常)它创造了这种程序在开发过程中运行良好,然后在发货后一个月内生成一千张支持票,并且没有明显迹象表明出了什么问题。