让我们假设我有一些工作线程如下:
while (1) {
do_something();
if (flag_isset())
do_something_else();
}
我们有几个帮助函数来检查和设置标志:
void flag_set() { global_flag = 1; }
void flag_clear() { global_flag = 0; }
int flag_isset() { return global_flag; }
因此,线程继续在忙循环中调用do_something()
,并且在某些其他线程集global_flag
的情况下,线程也调用do_something_else()
(例如,可以输出进度或调试信息)通过从另一个线程设置标志来请求。
我的问题是:我是否需要做一些特殊的事情才能同步访问global_flag?如果是,那么以便携方式进行同步的最小工作究竟是什么?
我试图通过阅读许多文章来解决这个问题,但我仍然不太确定正确的答案......我认为它是以下之一:
我们只需要将标志定义为volatile
,以确保每次检查时都从共享内存中读取它:
volatile int global_flag;
它可能不会立即传播到其他CPU核心,但迟早会保证。
在一个CPU内核中设置共享标志不一定会让另一个内核看到它。我们需要使用互斥锁来确保通过使其他CPU上的相应缓存行无效来传播标志更改。代码如下:
volatile int global_flag;
pthread_mutex_t flag_mutex;
void flag_set() { pthread_mutex_lock(flag_mutex); global_flag = 1; pthread_mutex_unlock(flag_mutex); }
void flag_clear() { pthread_mutex_lock(flag_mutex); global_flag = 0; pthread_mutex_unlock(flag_mutex); }
int flag_isset()
{
int rc;
pthread_mutex_lock(flag_mutex);
rc = global_flag;
pthread_mutex_unlock(flag_mutex);
return rc;
}
这与 B 相同,但我们不是在两侧使用互斥锁(读写器),而是仅在写入端设置它。因为逻辑不需要同步。我们只需要在更改标志时同步(使其他缓存无效):
volatile int global_flag;
pthread_mutex_t flag_mutex;
void flag_set() { pthread_mutex_lock(flag_mutex); global_flag = 1; pthread_mutex_unlock(flag_mutex); }
void flag_clear() { pthread_mutex_lock(flag_mutex); global_flag = 0; pthread_mutex_unlock(flag_mutex); }
int flag_isset() { return global_flag; }
当我们知道很少更改标志时,这将避免连续锁定和解锁互斥锁。我们只是使用Pthreads互斥锁的副作用来确保传播更改。
我认为A和B是明显的选择,B更安全。但C怎么样?
如果C没问题,还有其他方法可以强制标志更改在所有CPU上都可见吗?
有一个相关的问题:Does guarding a variable with a pthread mutex guarantee it's also not cached? ......但它并没有真正回答这个问题。
答案 0 :(得分:12)
“最低工作量”是明确的记忆障碍。语法取决于您的编译器;在海湾合作委员会,你可以做:
void flag_set() {
global_flag = 1;
__sync_synchronize(global_flag);
}
void flag_clear() {
global_flag = 0;
__sync_synchronize(global_flag);
}
int flag_isset() {
int val;
// Prevent the read from migrating backwards
__sync_synchronize(global_flag);
val = global_flag;
// and prevent it from being propagated forwards as well
__sync_synchronize(global_flag);
return val;
}
这些记忆障碍实现了两个重要目标:
他们强制编译器刷新。考虑如下循环:
for (int i = 0; i < 1000000000; i++) {
flag_set(); // assume this is inlined
local_counter += i;
}
如果没有障碍,编译器可能会选择将其优化为:
for (int i = 0; i < 1000000000; i++) {
local_counter += i;
}
flag_set();
插入屏障会强制编译器立即写回变量。
它们强制CPU命令其写入和读取。这不是一个单一标志的问题 - 大多数CPU架构将最终看到一个没有CPU级别障碍的标志。但是订单可能会改变。如果我们有两个标志,并在线程A:
// start with only flag A set
flag_set_B();
flag_clear_A();
在主题B上:
a = flag_isset_A();
b = flag_isset_B();
assert(a || b); // can be false!
某些CPU架构允许重新排序这些写入;你可能会看到两个标志都是假的(即标志A写入先被移动)。如果标志保护指针有效,则这可能是一个问题。内存障碍迫使写入顺序以防止这些问题。
另请注意,在某些CPU上,可以使用“获取释放”屏障语义来进一步减少开销。然而,在x86上不存在这种区别,并且需要在GCC上进行内联汇编。
可以在the Linux kernel documentation directory中找到有关内存障碍及其需要原因的详细概述。最后,请注意,此代码足以支持单个标志,但如果您想同步任何其他值,则必须非常小心。锁通常是最简单的做事方式。
答案 1 :(得分:3)
您必须不会导致数据争用案例。它是未定义的行为,允许编译器做任何事情以及它所喜欢的一切。
关于此主题的幽默博客:http://software.intel.com/en-us/blogs/2013/01/06/benign-data-races-what-could-possibly-go-wrong
案例1:标志上没有同步,因此允许任何事情发生。例如,允许编译器转
flag_set();
while(weArentBoredLoopingYet())
doSomethingVeryExpensive();
flag_clear()
到
while(weArentBoredLoopingYet())
doSomethingVeryExpensive();
flag_set();
flag_clear()
注意:这种比赛实际上很受欢迎。你的millage可能会有所不同。一方面,pthread_call_once的事实上的实现涉及这样的数据竞争。另一方面,它是未定义的行为。在gcc的大多数版本中,你可以侥幸使用它,因为在很多情况下gcc选择不行使其优化权限,但它不是“规范”代码。
B:完全同步是正确的呼叫。这就是你必须要做的事情。
C:如果您可以证明在写入时没有人想要阅读它,那么只有编写器上的同步可以工作。数据竞争的官方定义(来自C ++ 11规范)是一个线程写入变量,而另一个线程可以同时读取或写入相同的变量。如果你的读者和作家都一次跑,你仍然有一个种族案例。但是,如果你能证明作者写了一次,有一些同步,然后读者都读了,那么读者不需要同步。
对于缓存,规则是互斥锁定/解锁与锁定/解锁相同互斥锁的所有线程同步。这意味着你不会看到任何不寻常的缓存效果(虽然在引擎盖下,你的处理器可以做一些壮观的事情来让它运行得更快......它只是让它看起来没有做任何特别的事情)。但是,如果您不进行同步,则无法保证其他线程没有更改以推送您所需的内容!
所有这一切,问题是你真的愿意依赖编译器特定的行为。如果要编写正确的代码,则需要进行适当的同步。如果你愿意依靠编译器对你很友好,那么你就可以逍遥法外。
如果您使用的是C ++ 11,那么简单的答案就是使用atomic_flag,它可以完全满足您的需求,并且在大多数情况下可以正确地为您进行同步。
答案 2 :(得分:0)
对于你发布的例子,案例A就足够了......
如果获取和/或设置标志需要多条CPU指令,则必须使用某种形式的锁定。
如果do_something_else()依赖于在执行该例程期间设置的标志,则必须按照情况C锁定,但必须在调用flag_isset()之前锁定互斥锁。
希望这有帮助。
答案 3 :(得分:-1)
将传入作业分配给工作线程不需要锁定。典型的例子是webserver,其中请求由主线程捕获,并且该主线程选择一个worker。我正在尝试用一些伪造的代码解释它。
main task {
// do forever
while (true)
// wait for job
while (x != null) {
sleep(some);
x = grabTheJob();
}
// select worker
bool found = false;
for (n = 0; n < NUM_OF_WORKERS; n++)
if (workerList[n].getFlag() != AVAILABLE) continue;
workerList[n].setJob(x);
workerList[n].setFlag(DO_IT_PLS);
found = true;
}
if (!found) panic("no free worker task! ouch!");
} // while forever
} // main task
worker task {
while (true) {
while (getFlag() != DO_IT_PLS) sleep(some);
setFlag(BUSY_DOING_THE_TASK);
/// do it really
setFlag(AVAILABLE);
} // while forever
} // worker task
因此,如果有一个标志,一方设置为A,另一方设置为B和C(主任务将其设置为DO_IT_PLS,并且工作人员将其设置为BUSY和AVAILABLE),则没有标志。当教师向学生提供不同的任务时,可以用“现实生活”的例子来玩。老师选择一个学生,给他/她一个任务。然后,老师寻找下一个可用的学生。当学生准备好后,他/她会回到可用学生的池中。
UPDATE :简单来说,只有一个main()线程和几个 - 可配置数量的 - 工作线程。由于main()只运行一个实例,因此无需同步worker的选择和launc。