想要了解线程的同步,唯一的途径就是实际使用
原子访问
原子是物理中最小的单位,原子操作也就是CPU内部执行的最小单位,不可以再被分割
long g_x = 0;
DWORD WINPAI Thread1(LPVOID pvParam)
{
g_x++;
return 0;
}
DWORD WINPAI Thread2(LPVOID pvParam)
{
g_x++;
return 0;
}
反汇编代码:
//Thread1
MOV EAX,[g_x]
INC EAX
MOV [g_x],EAX
//Thread2
MOV EAX,[g_x]
INC EAX
MOV [g_x],EAX
//这样执行没有任何毛病,但是发生如下:
MOV EAX,[g_x] //Thread1
INC EAX //Thread1
MOV EAX,[g_x] //Thread2
INC EAX //Thread2
MOV [g_x],EAX //Thread2
MOV [g_x],EAX //Thread1
//最终两次自增运算,正确结果为2,错误为1
//两个线程发生错乱,导致数据发生错误
//因为++自增运算在C语言看来是原子操作,翻译为汇编后是3次原子操作,所以存在错误
所以必须进行控制(Interlocked系列函数):
InterlockedExchangeAdd(
PLONG volatile plAddend, //控制的变量
LONG lIncrement); //变动值
long g_x = 0;
DWORD WINPAI Thread1(LPVOID pvParam)
{
InterlockedExchangeAdd(&g_x,1);
return 0;
}
DWORD WINPAI Thread2(LPVOID pvParam)
{
InterlockedExchangeAdd(&g_x,1);
return 0;
}
//自减时: InterlockedExchangeAdd(&g_x,-1);
关键段
关键段是一小段代码,它在执行前需要独占对一些共享资源的访问权。
举个例子:比如在飞机上,厕所里有一个马桶,所以同一时刻只允许一个人(线程)在卫生间(关键段)内使用马桶(被保护的资源)。当厕所显示“无人”时,进去上厕所标记为“有人”,上完后标记为“无人”。
CRITICAL_SECTION g_cs;
long g_x = 0;
DWORD WINPAI Thread1(LPVOID pvParam)
{
EnterCriticalSection(&g_cs);
g_x ++;
printf("%d\n",g_x);
LeaveCriticalSection(&g_cs);
return 0;
}
DWORD WINPAI Thread2(LPVOID pvParam)
{
EnterCriticalSection(&g_cs);
g_x ++;
printf("%d\n",g_x);
LeaveCriticalSection(&g_cs);
return 0;
}
void main()
{
InitializeCriticalSection(PCRITICAL_SECTION pcs); //初始化临界区
DeleteCriticalSection(PCRITICAL_SECTION pcs); //销毁临界区
}
CRITICAL_SECTION g_cs:是一个数据结构,把所有要进行共享的资源放在EnterCriticalSection(&g_cs)和LeaveCriticalSection(&g_cs)之间,调用时传入g_cs的地址。
存在问题:当厕所上完后没有进行标记“无人”,之后的所有人都无法上厕所。如果有人憋不住,强制进行上厕所,一定会把厕所门给弄坏的。
关键段和自旋锁
当线程试图进入一个关键段,但关键段被另一个线程占用,函数会立即把调用线程切换到等待状态,这意味着线程从用户模式转到内核模式,这个切换开销非常大(大约1000个CPU周期)。
为了提高关键段的性能,在关键段中加入自旋锁,当等待时会用一个自旋锁不断循环尝试进入关键段,只有在尝试失败后,线程才会切换到内核模式并进入等待状态。
BOOL InitializeCriticalSectionAndSpinCount(
PCRITICAL_SECTION pcs, //关键段结构地址
DWORD dwSpinCount); //旋转循环的次数
DWORD SetCriticalSectionSpinCount(
PCRITICAL_SECTION pcs, //关键段结构地址
DWORD dwSpinCount); //旋转循环的次数
//主机只有一个处理器时,函数会忽略dwSpinCount参数