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

时间:2018-05-20 05:28:59

标签: c multithreading winapi

说我有以下代码:

/* 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);

3 个答案:

答案 0 :(得分:2)

尝试使用无锁线程模型(障碍,原子,互锁操作等等)来保护智能而不是单个变量而不是标准锁定机制只会导致错误。

您需要使用锁定保护所有变量(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);   

答案 1 :(得分:0)

在您的具体示例中,您需要完全Release-Acquire ordering

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并且   标记了来自同一变量(flag)的线程 B 中的原子加载   memory_order_acquire,所有内存写入(非原子和放松   原子)发生在原子商店之前的观点   线程A,在线程B中成为可见的副作用,即一次   原子加载完成后,线程B保证看到一切   (number1, number2)线程A写入内存。

您也可以将标记定义为volatile int flag并使用/volatile:ms CL.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扩展易失性语义,它增加了超出ISO标准C ++语言的内存排序保证。   在易失性访问中保证获取/释放语义。   但是,此选项还会强制编译器生成硬件   内存障碍,可能会增加ARM和其他的开销   弱内存排序架构。如果编译器针对任何目标   平台除了ARM,这是volatile的默认解释。

但无论如何while (flag == 0) ;不是一个好的解决方案(自旋锁)。这里可以使用set / wait on事件,条件变量,发送/发送消息到具体线程或iocp。依赖具体任务

答案 2 :(得分:-1)

如果您将自己的旗帜宣布为LONG,那么您可以这样做:

InterlockedExchange (&flag, 1);

这会产生完整的内存屏障,请参阅MSDN。鉴于这被标记为C问题,这似乎是一个很好的方法。

使用CRITICAL_SECTION的实时代码(例如,低延迟音频处理)可以导致priority inversion如果较低优先级的线程声称它。我怀疑这是否是一个问题,但我发生了这件事。