【问题标题】:Do I need to add thread locking to simple variables? [duplicate]我需要将线程锁定添加到简单变量吗? [复制]
【发布时间】:2017-07-04 08:39:44
【问题描述】:

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

enum State: String {
  case one
  case two
}

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

【问题讨论】:

标签: ios swift multithreading thread-safety


【解决方案1】:

是的,你知道。

想象一下两个线程试图将 1 加到 someValue 的情况。一个线程通过以下方式执行此操作:

  1. someValue读入寄存器
  2. 加1
  3. 回信someValue

如果两个线程在执行操作 3 之前执行操作 1,与一个线程在另一个线程执行操作 1 之前执行所有三个操作的情况不同,您将得到不同的答案。

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

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

  1. 修改someValue
  2. 测试新值
  3. 相应地设置state

以上三个操作必须看起来是原子的,或者如果在操作1之后但在操作3之前检查对象,它将处于不一致的状态。

【讨论】:

  • 除非我弄错了,即使读取/写入单个属性也不一定是原子的,因此如果另一个线程同时写入,一个线程可能会读取垃圾。
  • @MartinR 我不知道。我的想法是,对于 64 位数据总线,Double 将在一个内存写入周期中写入,但不知道enum 的值。但是,对于 CPU 缓存、编译器优化和乱序执行等问题,我认为这个问题没有实际意义。无论如何,你需要某种形式的同步。
  • @MartinR:只要没有人尝试更改数据(在 Intel 和 ARM 处理器上),从多个线程读取相同的数据就可以了。边读边写是个问题,因为读者可能读取新旧数据的混合体,这完全是垃圾。
  • “边读边写是一个问题,因为读者可能会阅读新旧数据的混合,这将是完全垃圾”不认为这是可能的,感谢您的澄清
  • @weuhi 对于大多数 16 位处理器上的 32 位值或大多数 32 或 16 位处理器上的 64 位值当然是可能的,因为它们可能会被读取第一部分之间的上下文切换中断值和值的下一部分。我不知道 32 位 iOS 是否曾经在硬件上运行过。
【解决方案2】:

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

【讨论】:

  • 说线程 A 每 X 毫秒写入 someValue 并且有多个线程想要读取 someValue。
  • 在 C++ 中,我知道我可以在不加锁的情况下做到这一点。将原始值写入内存会写入整个内容,如果正确对齐,则不会先更改某些字节。您描述了一个常见的有用案例。记录你的假设!
【解决方案3】:

JeremyP 所说的,但您还需要考虑更高级别:您的“状态”和“某些值”可能是相关的。因此,如果我更改状态,那么 someValue,在我更改“状态”之后整个对象的内容可能是垃圾,因为新状态与旧的 someValue 不匹配。

简单的解决方案是在谷歌上搜索如何在 Swift 中执行“@synchronized”,或者分派到主线程,或者分派到串行队列。

【讨论】:

  • "你还需要考虑更高的级别"。我在回答的第二部分做了:)
【解决方案4】:

为了模拟你的问题,我追踪了以下代码sn-p(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() 已经被调用了 3 次,但是在我的机器(模拟器)上依次运行应用程序 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

这代表了预期的结果。

希望对你有帮助。

【讨论】:

    【解决方案5】:

    严格来说,不(尽管“是”也是一个有效的答案,并且对于一般情况来说可能是更正确的答案)。

    根据您的需要/想要什么,您可以很好地使用原子操作,例如使用函数,例如OSAtomicIncrement32OSAtomicCompareAndSwapPtr 没有锁定

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

    还请注意,即使您需要锁定或使用原子操作为了程序的正确性,您偶尔也可以很好地“逃脱”而不这样做。这是因为在大多数平台上,对正确对齐的内存地址的普通加载和存储无论如何都是原子的。
    但是,不要动心,这并不像听起来那么好,而且确实根本不是一件好事——依靠事情会正常工作(即使你“测试”了,它无论如何都可以正常工作)创建一种在开发过程中运行良好的程序,然后在发布后一个月导致一千张支持票,并且没有明显迹象表明出了什么问题。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2023-03-24
      • 1970-01-01
      相关资源
      最近更新 更多