鉴于this post中的代码,仅使用Semaphore
和atomic<>
来实施mutex
。
我很好奇,因为count
已被updateMutex
看守,atomic<>
是否必要?
struct Semaphore {
int size;
atomic<int> count;
mutex updateMutex;
Semaphore(int n) : size(n) { count.store(0); }
void aquire() {
while (1) {
while (count >= size) {}
updateMutex.lock();
if (count >= size) {
updateMutex.unlock();
continue;
}
++count;
updateMutex.unlock();
break;
}
}
void release() {
updateMutex.lock();
if (count > 0) {
--count;
} // else log err
updateMutex.unlock();
}
};
如果没有atomic
,我认为构造函数会出现同步问题。如果其他线程在构造之后立即使用它,则可能无法看到计数分配。
如果是,那么size
呢?是否还需要atomic<>
保护?
或者atomic<>
完全没用,因为无论其他线程何时使用size
和count
,它们都将可见。
谢谢!
答案 0 :(得分:7)
我认为count
成为atomic<int>
的真正原因是读在<{1}} 之外 aquire()
- 此行中的受保护区域:
mutex
如果没有while (count >= size) {}
,则允许编译假设读取一次就足够了,并且不会轮询它以获取来自其他线程的更改值。
答案 1 :(得分:6)
提出了多个问题。所有都要求理解基础概念:如果一个对象由至少一个由另一个线程访问(读取或写入)的线程写入,并且写入和访问不是,那么您有一个数据竞争同步。数据竞赛的正式定义见1.10 [intro.multithread]第21段:
程序的执行包含数据竞争,如果它在不同的线程中包含两个冲突的动作,其中至少有一个不是原子的,并且在另一个之前都不会发生。 [...]
包含数据竞争的程序具有未定义的行为,即程序需要确保它是免费的数据竞争。现在回答不同的问题:
是否有必要在构造函数中使用同步?
这取决于对象是否可以在不同的线程中同时访问,同时它仍在构建中。我可以想象并发访问正在构建的对象的唯一情况是在静态初始化期间,其中多个线程已经开始访问共享对象。由于对全局对象的构造顺序的弱约束,我无法想象无论如何都将使用全局对象,并且实现将函数本地static
对象的构造同步。否则,我希望使用适当同步的机制跨线程共享对象的引用。也就是说,我会设计系统,使构造函数不需要同步。
已经锁定了。这是否意味着count
不必是原子。
由于在获取锁之前在count
函数中访问acquire()
,因此它将是对另一个线程写入的对象的非同步访问,即,您将进行数据竞争,因此,未定义的行为。 count
必须是原子的。
size
是否也需要同步。
size
成员仅在Semaphore
的构造函数中进行了修改,通过实际使其成为const
成员来强制实施该成员是合理的。假设在构造期间不同时访问对象(参见上面的1.),访问size
时不存在数据竞争的可能性。
请注意,您不应该毫无保留地使用互斥锁的lock()
和unlock()
成员。相反,您应该使用std::lock_guard<std::mutex>
或std::unique_lock<std::mutex>
,可能使用辅助块。这两个类保证始终释放获取的锁。我还质疑,等待信号量获取锁定的忙是否是正确的方法。
答案 2 :(得分:3)
是。存在理论风险:
count = 0;
acquire()
或release()
,在另一个CPU上运行的其他线程不会观察到构造函数中的。发生这种情况的可能性很小,因为为了使用信号量对象,构造函数必须完成,并且不知何故另一个线程需要获取该对象。
这就是说,count
占用的内存的另一个CPU视图不会在CPU之间同步,而另一个可以读取旧的(例如未初始化的)值。
默认情况下,在此使用std::atomic<int>
会在负载周围(在这种情况下通过重载运算符)和存储生成内存屏障。默认情况下,这是非常保守的。
您也可以在构造函数中锁定和解锁互斥锁以获得相同的效果 - 但这更加昂贵。
必须要说的是,这是一种非常讨厌的实现计数信号量的方式 - 但它毕竟是一个面试问题,因此有很多方面。