一.原子锁
原子锁主要解决的问题是多线程在操作符方面的问题。
- 相关问题:
多个线程对同一个数据进行原子操作时,会产生结果丢失,比如++运算符
我们来写一段代码看看多线程在操作同一个数据的时候出现的问题:
#include <stdio.h> #include <windows.h> DWORD WINAPI ThreadProc1(LPVOID lpParameter); DWORD WINAPI ThreadProc2(LPVOID lpParameter); int g_value = 0; int main() { DWORD nID = 0; HANDLE hThread1 = CreateThread(NULL, 0, ThreadProc1, NULL, 0, &nID); HANDLE hThread2 = CreateThread(NULL, 0, ThreadProc2, NULL, 0, &nID); WaitForMultipleObjects(2, hThread, TRUE, INFINITE); printf("%d\n", g_value); return 0; } DWORD WINAPI ThreadProc1(LPVOID lpParameter) { for (int i = 0; i < 100000000; i++) { g_value++; } return 0; } DWORD WINAPI ThreadProc2(LPVOID lpParameter) { for (int i = 0; i < 100000000; i++) { g_value++; } return 0; }
- 代码解释
我们创建两个线程,同时对全局变量g_value进行自增操作,两个线程分别自增100000000次,那么最后结果就应该是200000000,我们来看看执行结果:
我们发现,最后结果并不是200000000,那么是为什么呢?我们来分析一下错误:
当线程A执行g_value++时,如果线程切换正好是在线程A将结果保存到g_value之前,线程B继续执行g_value++,那么当线程A再次被切换回来之后,会继续上一步的操作,继续将值保存到g_value中,线程B的计算结果被覆盖
通俗点来说,就是线程A计算好了g_value的结果,但是还没有保存到g_value,这时候线程切换到了B线程,线程B完成了计算,并且成功保存,当返回到A线程的时候,A线程会继续上一步的保存操作,那么B线程的计算结果就被覆盖掉了。
那么如何来解决这样的问题呢?那就要用到我们的线程同步技术—原子锁了:
- 原子锁函数:
InterlockedIncrement()
InterlockedDecrement()
InterlockedCompareExcahnge()
InterlockedExchange()
我们在上文中提到,原子锁主要针对的是运算符的问题,每一种运算符都有原子锁函数
我们来看看使用效果:这里以++运算符为例:
#include <stdio.h> #include <windows.h> DWORD WINAPI ThreadProc1(LPVOID lpParameter); DWORD WINAPI ThreadProc2(LPVOID lpParameter); DWORD g_value = 0; int main() { DWORD nID = 0; HANDLE hThread[2] = { 0 }; hThread[0] = CreateThread(NULL, 0, ThreadProc1, NULL, 0, &nID); hThread[1] = CreateThread(NULL, 0, ThreadProc2, NULL, 0, &nID); WaitForMultipleObjects(2, hThread, TRUE, INFINITE); printf("%d\n", g_value); return 0; } DWORD WINAPI ThreadProc1(LPVOID lpParameter) { for (int i = 0; i < 100000000; i++) { InterlockedIncrement(&g_value); } return 0; } DWORD WINAPI ThreadProc2(LPVOID lpParameter) { for (int i = 0; i < 100000000; i++) { InterlockedIncrement(&g_value); } return 0; }
我们来看看执行效果:
我们可以发现,当我们使用原子锁的时候,两个线程操作同一个数据,就不会出现结果丢失的问题了。但是我们也不难发现,执行结果慢了很多,这是因为执行过程中多了很多等待事件,这个等待我们在互斥中会讲到。
原子锁的实现:直接对数据所在的内存操作,并且在任何一个瞬间,只能有一个线程访问
二.互斥体
- 相关问题
跟原子锁一样,都是解决多线程下资源的共享使用,但是与原子锁不同的是,互斥体解决的是代码资源的共享使用。
- 互斥体的使用:
- 创建互斥体:
使用CreateMutex
函数:
MSDN官方文档解释
HMODLE CreateMutex( LPSECURITY_ATTRIBUTES lpMutexAttributes, //安全性 BOOL bInitialOwner, //初始拥有者 LPCSTR lpName //为互斥命名 );
参数bInitialOwner介绍:
如果此值为 TRUE ,并且调用方创建了互斥体,则调用线程获取互斥体对象的初始所有权。 否则,调用线程不会获取互斥体的所有权。
互斥体特性介绍:
- 在任何一个时间点上,只能由一个线程拥有互斥体
- 当前任何一个线程不拥有互斥体是,互斥体句柄有信号
- 谁先等候互斥体,谁先获取
- 等候互斥体:
上一篇介绍过了,使用等候句柄函数。WaitFor...
- 释放互斥体
BOOL ReleaseMutex( HANDLE hMutex //handle of Mutex );
- 关闭互斥体
使用CloseHandle
函数
我们来看看使用互斥体来解决我们在多线程中遇到的问题:
#include <stdio.h> #include <windows.h> DWORD WINAPI ThreadProc1(LPVOID lpParameter); DWORD WINAPI ThreadProc2(LPVOID lpParameter); DWORD g_value = 0; HANDLE hMutex = NULL; //用于接收互斥体句柄 int main() { DWORD nID = 0; HANDLE hThread[2] = { 0 }; //hMutex = CreateMutex(NULL, FALSE, NULL); hThread[0] = CreateThread(NULL, 0, ThreadProc1, NULL, 0, &nID); hThread[1] = CreateThread(NULL, 0, ThreadProc2, NULL, 0, &nID); WaitForMultipleObjects(2, hThread, TRUE, INFINITE); printf("%d\n", g_value); CloseHandle(hMutex); return 0; } DWORD WINAPI ThreadProc1(LPVOID lpParameter) { char a[] = "********"; while (1) { //WaitForSingleObject(hMutex, INFINITE); for (int i = 0; i < strlen(a); i++) { printf("%c", a[i]); Sleep(125); } printf("\n"); //ReleaseMutex(hMutex); } return 0; } DWORD WINAPI ThreadProc2(LPVOID lpParameter) { char b[] = "--------"; while (1) { //WaitForSingleObject(hMutex, INFINITE); for (int i = 0; i < strlen(b); i++) { printf("%c", b[i]); Sleep(125); } //ReleaseMutex(hMutex); printf("\n"); } return 0; }
我们来看看不适用互斥体技术的时候的输出:
我们再来看看使用了互斥体之后:
#include <stdio.h> #include <windows.h> DWORD WINAPI ThreadProc1(LPVOID lpParameter); DWORD WINAPI ThreadProc2(LPVOID lpParameter); DWORD g_value = 0; HANDLE hMutex = NULL; //用于接收互斥体句柄 int main() { DWORD nID = 0; HANDLE hThread[2] = { 0 }; hMutex = CreateMutex(NULL, FALSE, NULL); hThread[0] = CreateThread(NULL, 0, ThreadProc1, NULL, 0, &nID); hThread[1] = CreateThread(NULL, 0, ThreadProc2, NULL, 0, &nID); WaitForMultipleObjects(2, hThread, TRUE, INFINITE); printf("%d\n", g_value); CloseHandle(hMutex); return 0; } DWORD WINAPI ThreadProc1(LPVOID lpParameter) { char a[] = "********"; while (1) { WaitForSingleObject(hMutex, INFINITE); for (int i = 0; i < strlen(a); i++) { printf("%c", a[i]); Sleep(125); } printf("\n"); ReleaseMutex(hMutex); } return 0; } DWORD WINAPI ThreadProc2(LPVOID lpParameter) { char b[] = "--------"; while (1) { WaitForSingleObject(hMutex, INFINITE); for (int i = 0; i < strlen(b); i++) { printf("%c", b[i]); Sleep(125); } ReleaseMutex(hMutex); printf("\n"); } return 0; }
我们可以发现使用互斥体之后,对代码段进行了枷锁。
我们来大致讲解一下互斥体的实现吧:
我们在主进程中创建了互斥体,并且互斥体不归之进程所有,两个线程谁先等待互斥体句柄,谁就拥有了互斥体,那么当线程跳转到另一个线程之后,发现被锁定在了另一个线程,那么线程就会被阻塞,直到线程再次跳转到另一个线程,执行完之后,互斥体被释放,这时候跳转到这个线程,在这个线程中再进行加锁,这个线程执行完之后,再锁定到另一个线程,这样就实现了加锁技术。