gpt4 book ai didi

c - 是否需要互斥锁来同步 pthread 之间的简单标志?

转载 作者:太空狗 更新时间:2023-10-29 16:32:35 25 4
gpt4 key购买 nike

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

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 的访问?如果是,以可移植方式进行同步的最少工作量到底是多少?

我试图通过阅读许多文章来解决这个问题,但我仍然不太确定正确答案......我认为它是以下之一:

A:不需要同步,因为设置或清除标志不会产生竞争条件:

我们只需要将标志定义为 volatile 以确保每次检查时它确实是从共享内存中读取的:

volatile int global_flag;

它可能不会立即传播到其他 CPU 内核,但迟早会传播,保证。

B:需要完全同步以确保对标志的更改在线程之间传播:

在一个 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;
}

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 上可见?

有一个有点相关的问题:Does guarding a variable with a pthread mutex guarantee it's also not cached? ...但它并没有真正回答这个问题。

最佳答案

“最小工作量”是一个明确的内存障碍。语法取决于您的编译器;在 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;
}

这些内存障碍实现了两个重要目标:

  1. 他们强制编译器刷新。考虑如下循环:

     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();

    插入屏障会强制编译器立即写回变量。

  2. 它们强制 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 中找到关于什么是内存屏障以及为什么需要它们的很好的概述。 .最后,请注意,此代码足以用于单个标志,但如果您还想与任何其他值同步,则必须非常小心。锁通常是最简单的做事方式。

关于c - 是否需要互斥锁来同步 pthread 之间的简单标志?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/7223164/

25 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com