C 在pthread之间同步简单标志是否需要互斥?

C 在pthread之间同步简单标志是否需要互斥?,c,synchronization,pthreads,C,Synchronization,Pthreads,假设我有几个工作线程,如下所示: while (1) { do_something(); if (flag_isset()) do_something_else(); } volatile int global_flag; pthread_mutex_t flag_mutex; void flag_set() { pthread_mutex_lock(flag_mutex); global_flag = 1; pthread_mutex_unloc

假设我有几个工作线程,如下所示:

while (1) {
    do_something();

    if (flag_isset())
        do_something_else();
}
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;
}
我们有两个助手函数用于检查和设置标志:

void flag_set()   { global_flag = 1; }
void flag_clear() { global_flag = 0; }
int  flag_isset() { return global_flag; }
因此,线程在忙循环中不断调用
do\u something()
,如果其他线程设置了
global\u标志
,线程也会调用
do\u something()
(例如,当从另一个线程设置标志时,可以输出进度或调试信息)

我的问题是:我是否需要做一些特殊的事情来同步对全局_标志的访问?如果是,以便携方式进行同步的最小工作量是多少?

我读了很多文章试图弄明白这一点,但我仍然不太确定正确的答案。。。我认为这是以下情况之一:

 for (int i = 0; i < 1000000000; i++) {
   flag_set(); // assume this is inlined
   local_counter += i;
 }
答:无需同步,因为设置或清除标志不会创建竞争条件: 我们只需要将标志定义为volatile,以确保每次检查时都能从共享内存中读取:

volatile int global_flag;
它可能不会立即传播到其他CPU核,但迟早会传播到其他CPU核

B:需要完全同步以确保在线程之间传播对标志的更改: 在一个CPU内核中设置共享标志不一定会让另一个内核看到它。我们需要使用互斥来确保通过使其他CPU上相应的缓存线无效来传播标志更改。代码如下所示:

while (1) {
    do_something();

    if (flag_isset())
        do_something_else();
}
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;
}
C:需要同步以确保在线程之间传播对标志的更改: 这与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上可见


有一个有点相关的问题:……但它并没有真正回答这个问题。

对于您发布的示例,只要

  • 获取和设置标志只需要一条CPU指令
  • do_something_else()不依赖于该例程执行期间设置的标志
  • 如果获取和/或设置标志需要多条CPU指令,则必须使用某种形式的锁定

    如果do_something_else()依赖于该例程执行期间设置的标志,则必须像案例C一样锁定,但在调用标志_isset()之前必须锁定互斥体


    希望这有帮助。

    将传入作业分配给工作线程不需要锁定。典型的例子是webserver,其中请求由主线程捕获,该主线程选择一个辅助线程。我想用一些伪码解释一下

    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
    
    主要任务{
    //永远
    while(true)
    //等待工作
    while(x!=null){
    睡眠(一些);
    x=抓取作业();
    }
    //选择工人
    bool-found=false;
    对于(n=0;n
    因此,如果有一个标志,一方设置为A,另一方设置为B和C(主任务将其设置为DO_it_PLS,工作人员将其设置为BUSY and AVAILABLE),则没有确认。比如,当老师给学生布置不同的任务时,用“现实生活”的例子来演示。老师选择一个学生,给他/她一个任务。然后,老师寻找下一个可用的学生。当一名学生准备好后,他/她会回到可用的学生池中


    更新:请澄清,只有一个main()线程和几个可配置数量的辅助线程。由于main()只运行一个实例,因此无需同步辅助进程的选择和启动。

    最小工作量是一个明确的内存障碍。语法取决于编译器;在GCC上,您可以执行以下操作:

    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;
     }
    
    在线程B上:

      a = flag_isset_A();
      b = flag_isset_B();
      assert(a || b); // can be false!
    
    一些CPU架构允许对这些写入进行重新排序;您可能会看到两个标志都为false(即,先移动了写入标志)。如果标志保护(例如)有效的指针,则可能会出现问题。内存障碍迫使对写操作进行排序,以防止出现这些问题

  • 还请注意,在某些CPU上,可以使用“获取-释放”屏障语义来进一步减少开销。然而,x86上不存在这种区别,需要在GCC上进行内联汇编


    关于什么是记忆障碍以及为什么需要记忆障碍,请参阅。最后,请注意,这段代码对于单个标志来说已经足够了,但是如果您还想与任何其他值同步,那么必须非常小心地处理。锁通常是最简单的方法。

    您必须不引起数据争用情况。这是一种未定义的行为,编译器可以做任何事情

    关于该主题的幽默博客:

    情况1:标志上没有同步
    while(weArentBoredLoopingYet())
        doSomethingVeryExpensive();
    flag_set();
    flag_clear()