C语言 在设置共享变量时,我应该使用临界区还是内存屏障?

bvuwiixz  于 2023-06-05  发布在  其他
关注(0)|答案(3)|浏览(138)

假设我有以下代码:

/* Global Variables */

int flag = 0;
int number1;
int number2;

//------------------------------------

/* Thread A */

number1 = 12345;
number2 = 678910;
flag = 1;

//------------------------------------

/* Thread B */

while (flag == 0) {}
printf("%d", number1);
printf("%d", number2);

线程A中,代码可以不按顺序执行,例如:

/* Thread A */

flag = 1;
number1 = 12345;
number2 = 678910;

为了防止这种情况,我应该使用记忆屏障。
但我不确定是否应该使用常规的内存屏障,例如:

/* Thread A */

number1 = 12345;
number2 = 678910;
MEMORY_BARRIER_GOES_HERE
flag = 1;

或者我是否应该使用关键部分,例如:

/* Thread A */

number1 = 12345;
number2 = 678910;
EnterCriticalSection(&cs);
flag = 1;
LeaveCriticalSection(&cs);
0qx6xfy6

0qx6xfy61#

试图聪明地使用无锁线程模型(屏障,原子,互锁操作等)来保护多个变量而不是标准的锁定机制只会导致错误。
您需要用锁保护所有变量(number1number2flag)(关键部分)
线程A:

EnterCriticalSection(&cs);
    flag = 1;
    number1 = 12345;
    number2 = 678910;
LeaveCriticalSection(&cs);

线程B:

while (1)
{
    int n1, n2;

    EnterCriticalSection(&cs);
    if (flag)
    {
        n1 = number1;
        n2 = number2;
        break;
    }
    LeaveCriticalSection(&cs);
}
printf("%d", n1);
printf("%d", n2);

此外,在Windows上,您可以避免整个while (flag == 0) {}循环使用Conditional Variable来烧毁CPU核心。从连续轮询机制切换到基于通知的机制将产生比尝试执行棘手的互锁操作好得多的性能结果。

更好:

线程A:

EnterCriticalSection(&cs);
    flag = 1;
    number1 = 12345;
    number2 = 678910;
LeaveCriticalSection(&cs);
WakeAllConditionVariable(&conditional_variable);

线程B:

EnterCriticalSection(&cs);

while (flag == 0)
{
    // This will atomically Leave the CS and block until the conditional_variable is fired by the other thread

    SleepConditionVariableCS(&conditional_variable, &cs, INFINITE);

    // After it returns, it will re-enter the CS.
}

n1 = number1;
n2 = number2;
LeaveCriticalSection(&cs);
printf("%d", n1);
printf("%d", n2);
jhdbpxl9

jhdbpxl92#

在具体示例中,您需要的正是 * 发布-获取排序 *

int number1, number2, flag = 0;

/* Thread A */

number1 = 12345;
number2 = 678910;
//--------------
atomic_store_explicit(&flag, 1, memory_order_release);

/* Thread B */
if (atomic_load_explicit(&flag, memory_order_acquire) != 0)
{
    //--------------
    printf("%d", number1);
    printf("%d", number2);
}

如果线程 A 中的原子存储被标记为memory_order_release,并且线程 B 中的原子加载来自同一变量(flag)标记为memory_order_acquire,所有内存写入(非原子的和松弛的原子的)发生-从线程A的Angular 来看在原子存储之前,在线程B中变成可见的副作用,也就是说,一旦原子加载完成线程B保证看到线程A写入内存的所有内容(number1, number2)。
你也可以将flag定义为volatile int flag,并使用/volatile:msCL.exe 选项:

int number1, number2;
volatile int flag = 0;

/* Thread A */

number1 = 12345;
number2 = 678910;
//--------------
flag = 1;

/* Thread B */
if (flag)
{
    //--------------
    printf("%d", number1);
    printf("%d", number2);
}

/volatile:ms

选择Microsoft扩展的volatile语义,它在ISO标准 C++ 之外添加了内存排序保证。在易失性访问上保证获取/释放语义。但是,此选项也会强制编译器生成硬件内存屏障,这可能会在ARM和其他弱内存排序架构上增加大量开销。如果编译器针对除ARM以外的任何平台,则这是volatile的默认解释。
但无论如何while (flag == 0) ;不是好的解决方案(自旋锁)。这里可以使用设置/等待事件、条件变量、向具体线程或IOCP发送/发布消息。取决于具体任务

w8ntj3qf

w8ntj3qf3#

如果你声明你的标志为volatile LONG,那么你可以这样做:
InterlockedExchange (&flag, 1);
这会产生一个完全的内存屏障,参见MSDN。考虑到这是一个C问题,这似乎是一个很好的方法。
在真实的代码中(例如低延迟音频处理)可能导致priority inversion,如果一个较低优先级的线程曾经要求它。我怀疑这是否是一个问题,但我已经发生在我身上。

相关问题