我在2个线程之间有一个共享的struct变量:
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++;
和第二个将循环,直到它得到一致的结构:
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个整个cpu核心。
编辑3:我知道没有互斥或原子的同步是非常棘手的。我已经列出了我能想到的所有组合,并找不到任何破坏代码的组合。所以,请不要告诉它它不会起作用,如果你指出什么时候它会破裂,我将非常感激。
答案 0 :(得分:3)
我不想使用互斥锁
在单处理器系统上,如果第一个线程在写入时被抢占,则读取线程将花费其时间片不必要地旋转。在这种情况下,你确实需要一个互斥锁。
Linux futexes和Windows' CriticalSections在非争用情况和多处理器系统中没有上下文切换,在屈服之前旋转一段时间。
为什么重新实现完全相同的机制?
答案 1 :(得分:1)
绝对没有可移植的方式来做你想要的。一些非常高端的系统具有可以实现您想要的事务内存,但是使用事务内存的正常模式无论如何都是使用锁编写代码并依赖锁实现来使用事务。
只需使用互斥锁即可保护读写操作。没有其他方法可以使你的代码正确,但是有很多方法可以使它“在测试中看起来是正确的”,直到它违反了一个不变量并在几个月后崩溃或者在一个稍微不同的环境/ cpu上运行并且每次都开始崩溃你跑了。
答案 2 :(得分:0)
我的第一个建议是你真的应该使用互斥锁来实现它(确保每个线程尽可能少地保存互斥锁)并查看你是否遇到任何问题。很可能你会发现使用互斥锁工作得很好,而且不需要更多。这样做具有可移植到任何硬件,易于理解和易于调试的优点。
也就是说,如果坚持不使用互斥锁,那么唯一的另一个选择就是使用原子变量。由于原子变量是字大小的,你不能使整个结构成为原子,但你可以通过实例化一个结构数组来伪造它(数组的必要大小将取决于你打算的频率更新结构),然后使用原子整数作为索引进入"目前有效用于阅读"并且"可以写作写作"数组中的结构。从数组中读取结构的当前值非常简单 - 您只需要读取"当前对读取有效"数组中的索引,保证不写入 - 但写一个新值更精细;你需要以原子方式增加"可以写入写作" index(并在必要时将其包装起来,以避免索引数组的末尾,并在执行此操作后,如果执行此操作,写入的写入索引等于读取索引),则检查溢出条件。然后将新结构写入okay-for-writing索引指定的槽中。然后你必须进行原子比较和设置操作,将read-from索引设置为等于写入索引;如果比较和设置操作失败,则需要重新启动整个操作,因为另一个线程会使您更新。再次重复整个set()过程,直到比较和设置操作成功。
(如果这一切听起来很可疑且容易出错,那就是因为它。它可以正确实现,但很容易实现它几乎正确,并最终得到代码在99.999%的时间内工作,然后在0.0001%的其他时间做一些令人遗憾和不可重复的事情。考虑自己警告:))