【问题标题】:Real dangers of 2+ threads writing/reading a variable2+ 线程写入/读取变量的真正危险
【发布时间】:2011-05-08 12:00:14
【问题描述】:

同时读/写单个变量的真正危险是什么?

如果我在while循环中使用一个线程写入一个变量,另一个线程读取该变量,并且如果在写入时读取变量并使用旧值并没有危险,这里还有什么危险?

同时读取/写入是否会导致线程崩溃或发生精确同时读取/写入时在低级别会发生什么?

【问题讨论】:

  • “使用旧值”不是一个相当重要的问题吗?如果您不关心其中一个变量的值,您到底在计算什么? :D
  • 你甚至不能可靠地count了,你有什么算法可以在变量发生不可预测的变化时存活下来?不,即使是随机数生成器也不符合条件。
  • 我正在使用 while 条件和额外的迭代进行循环,或者如果 while 条件接收到垃圾数据,它将传递 == 0 并且无论如何循环都会退出。它是一个在while循环中循环的看门狗线程,检查线程是否已死。当线程函数完成时,它设置变量 = 1 并且 while 循环退出。

标签: c pthreads


【解决方案1】:

如果两个线程在没有适当同步的情况下访问一个变量,并且这些访问中至少有一个是写入,那么您就会出现数据竞争和未定义的行为。

未定义行为的表现方式完全取决于实现。在大多数现代架构上,您不会从硬件中得到陷阱或异常或任何东西,它会读取 something,或存储 something。问题是,它不一定会按照您的预期读取或写入。

例如如果两个线程递增一个变量,您可能会错过计数,如我在 devx 的文章中所述:http://www.devx.com/cplus/Article/42725

对于单个写入器和单个读取器,最常见的结果是读取器看到一个陈旧的值,但如果更新需要多个周期,或者变量被拆分,您也可能会看到部分更新的值缓存行。然后会发生什么取决于你用它做什么——如果它是一个指针并且你得到一个部分更新的值,那么它可能不是一个有效的指针,也不会指向你想要的,然后你可能会由于取消引用无效的指针值而导致任何类型的损坏或错误。如果错误的指针值恰好指向内存映射的 I/O 寄存器,这可能包括格式化您的硬盘或其他不良后果......

【讨论】:

    【解决方案2】:

    通常你会得到意想不到的结果。维基百科定义了两种不同的比赛条件:

    当内部变量的更改顺序决定了状态机最终进入的最终状态时,就会发生关键竞争。

    当内部变量更改的顺序没有改变最终状态时,就会发生非关键竞争。换句话说,当移动到所需状态时发生非关键竞争意味着必须一次更改多个内部状态变量,但无论这些内部状态变量以何种顺序更改,结果状态都是相同的。

    所以输出不会总是一团糟,这取决于代码。 始终处理比赛条件以供以后的代码扩展和防止可能的错误是一种很好的做法。没有什么比无法信任自己的数据更令人烦恼的了。

    【讨论】:

    • 你也可以使用原子操作。互斥锁很可能是矫枉过正。
    • @DeadMG:我会留下原子操作,因为当 OP 需要互斥锁时(即做,然后做对,然后快速做,互斥锁做对,原子操作快速完成)。
    • 我将措辞改为更笼统的“处理比赛条件”。
    【解决方案3】:

    两个线程读取相同的值完全没有问题。

    当一个线程写入一个非原子变量而另一个线程读取它时,问题就开始了。那么读取的结果是不确定的。因为线程可能随时被抢占(停止)。只有对原子变量的操作才能保证是不可破坏的。原子操作通常写入int 类型变量。

    如果您有两个线程访问相同的数据,最好的做法是 + 通常不可避免地使用锁定(互斥锁、信号量)。

    马里奥

    【讨论】:

    • @Helium3:这取决于 CPU。此外,变量应该对齐。
    【解决方案4】:

    取决于平台。例如,在 Win32 上,对齐 32 位值的读写操作是原子的——也就是说,你不能半读一个新值而半读一个旧值,如果你写,那么当有人来读, 他们要么获得完整的新值,要么获得旧值。当然,并非所有价值观或所有平台都如此。

    【讨论】:

    • @Helium3:这种行为实际上是针对 x86 的。
    【解决方案5】:

    将发生的最坏情况取决于实施。有很多完全独立的 pthread 实现,运行在不同的系统和硬件上,我怀疑有人知道所有这些的一切。

    如果p 不是指向易失性的指针,那么我认为允许 使用符合Posix 实现的编译器:

    while (*p == 0) {}
    exit(0);
    

    *p 进行一次检查,然后是一个无限循环,根本不会查看*p 的值。实际上,它不会,所以这是一个问题,即您是要按照标准进行编程,还是要按照您正在使用的实现的未记录观察到的行为进行编程。后者通常适用于简单的情况,然后您在代码上构建,直到您做了足够复杂的事情以至于它意外地不起作用。

    实际上,在没有一致内存缓存的多 CPU 系统上,可能需要很长时间,while 循环才能看到来自不同 CPU 的更改,因为如果没有内存屏障,它可能永远不会更新它的主内存缓存视图。但是英特尔有连贯的缓存,所以你个人很可能不会看到任何足够长的延迟来关心。如果某个可怜的傻瓜试图在更奇特的架构上运行您的代码,他们最终可能不得不修复它。

    回到理论上,您描述的设置可能会导致崩溃。想象一个假设的架构,其中:

    • p 指向非原子类型,例如典型 32 位架构上的 long long
    • 该系统上的long long 具有陷阱表示,例如因为它具有用作奇偶校验的填充位。
    • 发生读取时,对*p 的写入已完成一半
    • 半写已更新值的部分位,但尚未更新奇偶校验位。

    砰,未定义的行为,你读到了一个陷阱表示。可能是 Posix 禁止了 C 标准允许的某些陷阱表示,在这种情况下,long long 可能不是 *p 类型的有效示例,但我希望您能找到允许陷阱表示的类型。

    【讨论】:

    • 太棒了。它目前适用于我的 mac intel 64 位,但它将部署在运行 ubuntu 的熊猫板(双核 ARM)上。
    • @Helium3:啊,双核ARM,可能没有连贯缓存。我不是最新的 ARM,但几年前我确信有一些关于,所以值得检查。
    • 变量是一个普通的 int 值。没有指针或除 int 以外的任何东西。
    • @Helium3,“目前有效”...您确定您已经进行了足够的测试以捕获十亿分之一的案例吗?为此使用原子交换指令,您将轻松找到适用于不同 CPU 架构的代码。只有这样你才有保证。
    • @Helium3:让工作线程发布一个看门狗线程等待的信号量可能会更有效。那么你就没有看门狗线程忙循环(甚至睡眠循环)。
    【解决方案6】:

    如果正在写入和从中读取的变量无法更新或自动读取,则读取器可能会获取损坏的“部分更新”值。

    【讨论】:

    • 当例如检查如下 while(intValue == 0) 并且写入 int 值的线程正忙于写入 intValue = 1;
    • @Andrei:不。只是一个普通的 int。
    • 然后发布代码:) 如果没有 volatile 或内存屏障,我怀疑读取器线程会因为缓存而看到写入线程的输出。
    • 当我现在运行它时它会看到它。我只是不想要诸如线程崩溃之类的意外问题。这个问题更多的是关于线程和系统的内部工作
    【解决方案7】:
    • 您可以看到部分更新(例如,您可能会看到一个 long long 变量,其中一半来自新值,另一半来自旧值)。
    • 在使用内存屏障(pthread_mutex_unlock() 包含隐式内存屏障)之前,您无法保证看到新值。

    【讨论】:

      【解决方案8】:

      结果未定义。

      考虑这段代码:

      global int counter = 0;
      
      
      tread()
      {
         for(i=0;i<10;i++)
         {
             counter=counter+1;
         }
      }
      

      问题是如果你有 N 个线程,结果可能是 10 到 N*10 之间的任何值。 这是因为它可能会发生所有踏板读取相同的值增加它然后写回值+1。但是您问是否可以使程序或硬件崩溃。
      这取决于。在大多数情况下,错误的结果是无用的。

      要解决这个锁定问题,您需要互斥锁或信号量。

      互斥锁是代码的锁。在大写的情况下,您将锁定部分代码

      counter = counter+1;
      

      信号量是变量的锁

      counter 
      

      解决相同类型的问题基本相同。

      在您的胎面库中检查此工具。

      http://en.wikipedia.org/wiki/Mutual_exclusion

      【讨论】:

        猜你喜欢
        • 2015-04-19
        • 1970-01-01
        • 2021-08-23
        • 2021-08-16
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多