C语言 设置共享变量时,我应该使用关键部分还是内存屏障?



假设我有以下代码:

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

试图巧妙地使用无锁线程模型(障碍、原子、互锁操作等(来保护多个变量而不是标准锁定机制只会导致错误。

您需要用锁(关键部分(保护所有变量(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) {}循环使用条件变量刻录 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);   

在您的具体示例中,您完全需要发布-获取排序

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,在线程 B 中成为可见的副作用,即一次 原子加载完成,线程B保证看到一切 (number1, number2(线程A写入内存。

您还可以将标志定义为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);
}

/挥发性:毫秒

选择Microsoft扩展的易失性语义,这些语义添加了超出 ISO 标准C++语言的内存排序保证。 在易失性访问上保证获取/释放语义。 但是,此选项还会强制编译器生成硬件 内存障碍,这可能会在 ARM 和其他方面增加大量开销 弱内存排序体系结构。如果编译器以任何 平台,除了 ARM,这是易失性的默认解释。

但无论如何while (flag == 0) ;不是一个好的解决方案(自旋锁(。 这里可以使用设置/等待事件,条件变量,将消息发送/发布到具体线程或IOCP。 取决于具体任务

如果你将你的标志声明为volatile LONG那么你可以这样做:

InterlockedExchange (&flag, 1);

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

在实时代码(例如低延迟音频处理(中,如果优先级较低的线程声明优先级,则使用 CRITICAL_SECTION 可能会导致优先级反转。 我怀疑这是否是一个问题,但我遇到过这种情况。

最新更新