【问题标题】:ensure reading a struct as a whole确保阅读整个结构
【发布时间】:2016-12-04 23:53:57
【问题描述】:

我在 2 个线程之间有一个共享结构变量:

struct {
  long a;
  long b;
  long c;
} myStruct;
struct myStruct A;

A 的所有 3 个字段都初始化为零。然后第一个线程将更新它们:

A.a = 1;
A.b = 2;
A.c = 3;

第二个线程将从它读取。我要确保的是第二个线程将作为一个整体读取 A,无论是旧值 {0,0,0},还是新值 {1,2,3},而不是像 {1,2,0 这样的损坏}。 该结构不适合 64 位,所以我不能使用 gcc 的内置原子,我也不想使用互斥锁,所以我想出了 2 个保护标志:

struct {
  long a;
  long b;
  long c;
  volatile int beginCount, endCount;
} A;

那么第一个线程将:

A.beginCount++;
A.a = 1;
A.b = 2;
A.c = 3;
A.endCount++;

并且 2nd 将循环直到它得到一个一致的结构:

int begin, end;
myStruct tmp;
do {
  begin = A.beginCount;
  end = A.endCount;
  tmp = A;
} while (!(begin == A.beginCount && end == A.endCount && A.beginCount == A.endCount))
// now tmp will be either {0,0,0} or {1,2,3}

那两个守卫标志够吗?如果不是,请指出可能破坏它的线程调度的特定组合。

编辑1:我不想使用互斥锁的原因是第一个线程具有高优先级,它不应该等待任何东西。如果第 2 个线程在读取时第 1 个线程要写入,那么第 1 个线程仍然会写入,并且第 2 个线程必须重做读取,直到它获得一致的值。我们不能用互斥锁做到这一点,至少我不知道。

编辑 2:关于环境:这段代码在多处理器系统上运行,我为每个线程专用了 1 个完整的 cpu 核心。

编辑 3:我知道没有互斥锁或原子的同步非常棘手。我已经列出了我能想到的所有组合,但找不到任何破解密码的组合。所以,请不要只说它不起作用,如果你指出它什么时候会坏,我将非常感激。

【问题讨论】:

  • 不,他们不是。两个线程可以同时读取beginCount,然后添加一个,并保存结果。现在您的临界区中有两个线程,但 beginCount 是 1。您需要使用原子或互斥体来解决这个问题。
  • 只有第一个线程修改共享结构,第二个线程只读取它

标签: c multithreading gcc consistency


【解决方案1】:

我也不想使用互斥锁

在单处理器系统上,如果第一个线程在写入时被抢占,读取线程将花费其时间片不必要地旋转。在这种情况下,您确实需要互斥锁。

Linux futexes 和 Windows 的 CriticalSections 在非争用情况下和多处理器系统上都不会进行上下文切换,在让步之前旋转一段时间。

为什么要重新实现完全相同的机制?

【讨论】:

    【解决方案2】:

    绝对没有可移植的方式来做你想做的事。一些非常高端的系统具有事务内存,可能能够完成您想要的事情,但无论如何使用事务内存的正常模式是编写带有锁的代码并依赖锁实现来使用事务。

    只需使用互斥锁来保护读取和写入。没有其他方法可以使您的代码正确,但是有很多方法可以使它“看起来对测试正确”,直到它违反不变量并在几个月后崩溃或在稍微不同的环境/ cpu 上运行并开始每次崩溃你运行它。

    【讨论】:

    • 您可以使用互斥锁。您的优先级反转问题正是优先级继承互斥锁的用途。
    • 如果第一个线程当前什么都不做,那么第二个线程可以很好地获取锁,然后执行它的读取工作。然后当第二个线程正在读取(并持有锁)时,如果第一个线程想要写入,它必须等到第二个线程释放锁。我认为第一个线程没有任何方法可以“强制”第二个线程释放锁。
    • 当然不能强制第二个线程释放锁。它可以做的是以高优先级运行第二个线程,直到锁被释放,然后立即将其放回正常的低优先级。这就是优先级继承的工作原理。这些是已解决的问题。查找它们并使用解决方案,而不是试图发明不安全和无效的黑客。
    【解决方案3】:

    我的第一个建议是你真的应该使用互斥锁来实现它(确保每个线程持有互斥锁的时间尽可能短),看看你是否真的遇到了任何问题。您很可能会发现使用互斥体就可以了,而且不需要更多。这样做的好处是可以移植到任何硬件、易于理解和调试。

    也就是说,如果您坚持不使用互斥锁,那么唯一的其他选择就是使用原子变量。由于原子变量是字大小的,因此您无法使整个结构成为原子的,但您可以通过实例化结构数组来伪造它(数组的必要大小取决于您打算更新的频率struct),然后使用原子整数作为数组中“当前有效读取”和“可以写入写入”结构的索引。从数组中读取结构的当前值很简单——你只需要从数组中的“当前有效读取”索引中读取,保证不会被写入——但是写入一个新值是更精细;您需要原子地增加“可以写入以进行写入”索引(并在必要时将其环绕以避免索引数组的末尾,并检查溢出条件,如果这样做之后,可以写入index 等于读取索引)。然后将你的新结构写入ok-for-writing 索引指定的槽中。然后你必须做一个原子的比较和设置操作来设置读取索引等于可以写入的索引;如果比较和设置操作失败,您需要重新启动整个操作,因为另一个线程击败您进行更新。再次重复整个 set() 过程,直到 compare-and-set 操作成功。

    (如果这一切听起来令人怀疑且容易出错,那是因为它确实如此。它可以正确实现,但很容易几乎完全正确地实现它,并且最终得到的代码在 99.999% 的时间都可以工作并且然后在另外 0.0001% 的时间里做一些令人遗憾和不可重现的事情。考虑一下自己被警告过 :))

    【讨论】:

    • 我不希望第一个线程等待任何东西,只是将数据写入结构。如果有任何事情必须等待和重复,它是第二个线程。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-02-12
    相关资源
    最近更新 更多