我们在上一章节中讲解了关于Windows的线程基础,相信大家已经对线程有了基本的概念。这一章节中,我们来讲讲线程同步技术,包括加锁技术(原子锁和互斥体)和事件,信号量。
文章目录
- 一.原子锁
- 二.互斥体
- 三.事件
- 四.信号量
原子锁主要解决的问题是多线程在操作符方面的问题。
多个线程对同一个数据进行原子操作时,会产生结果丢失,比如++运算符
我们来写一段代码看看多线程在操作同一个数据的时候出现的问题:
#include
#include
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;
}
我们来分析一下错误:
当线程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
#include
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
函数:HMODLE CreateMutex(
LPSECURITY_ATTRIBUTES lpMutexAttributes, //安全性
BOOL bInitialOwner, //初始拥有者
LPCSTR lpName //为互斥命名
);
参数bInitialOwner介绍:
如果此值为 TRUE ,并且调用方创建了互斥体,则调用线程获取互斥体对象的初始所有权。 否则,调用线程不会获取互斥体的所有权。
互斥体特性介绍:
- 在任何一个时间点上,只能由一个线程拥有互斥体
- 当前任何一个线程不拥有互斥体是,互斥体句柄有信号
- 谁先等候互斥体,谁先获取
WaitFor...
BOOL ReleaseMutex(
HANDLE hMutex //handle of Mutex
);
CloseHandle
函数#include
#include
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
#include
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;
}
我们可以发现使用互斥体之后,对代码段进行了枷锁。
我们来大致讲解一下互斥体的实现吧:
我们在主进程中创建了互斥体,并且互斥体不归之进程所有,两个线程谁先等待互斥体句柄,谁就拥有了互斥体,那么当线程跳转到另一个线程之后,发现被锁定在了另一个线程,那么线程就会被阻塞,直到线程再次跳转到另一个线程,执行完之后,互斥体被释放,这时候跳转到这个线程,在这个线程中再进行加锁,这个线程执行完之后,再锁定到另一个线程,这样就实现了加锁技术。
前两个技术都属于加锁技术,即两个线程互斥的时候使用,那么线程也会有协调工作的时候,这时候就需要用到我们的事件和信号量了。
相关问题:
多线程协调工作的时候的通知问题
事件的使用
CreatEvent
函数,MSDN官方解释HANDLE CreateEvent(
LPSECURITY_ATTRIBUTES lpEventAttributes, //安全属性
BOOL bManualReset, //事件重置/复位方式
BOOL bInitialState, //事件初始状态
LPCSTR lpName //为事件命名
);
- bManualReset为事件重置/复位方式,如果该参数被设置为TRUE那么就需要我们来手动重置事件对象,如果该参数被设置为FALSE,那么操纵系统会帮我们完成事件的重置和复位。
- bInitialState:该参数指定了当创建事件后,该事件句柄是否处于有消息状态
WaitFor......
函数BOOLSetEvent(
HANDLE hEvent
);
BOOL ResetEvent(
HANDLE hEvent
);
CloseHandle
函数#include
#include
DWORD WINAPI ThreadProc1(LPVOID lpParameter);
DWORD WINAPI ThreadProc2(LPVOID lpParameter);
DWORD g_value = 0;
HANDLE hEvent = NULL; //用于接收事件句柄
int main() {
DWORD nID = 0;
HANDLE hThread[2] = { 0 };
hEvent = CreateEvent(NULL,FALSE,0,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(hEvent);
return 0;
}
DWORD WINAPI ThreadProc1(LPVOID lpParameter) {
char a[] = "********";
while (1) {
WaitForSingleObject(hEvent, INFINITE);
for (int i = 0; i < strlen(a); i++) {
printf("%c", a[i]);
}
printf("\n");
ResetEvent(hEvent);
}
return 0;
}
DWORD WINAPI ThreadProc2(LPVOID lpParameter) {
while (1) {
Sleep(1000);
SetEvent(hEvent);
}
return 0;
}
这是一个很典型的相互协调工作的双线程,我们在A线程中没有设定时间间隔,但是在B线程中设定了事件间隔,我们能够很明显地感受到输出是有时间间隔的:
CreateSemaphore
函数:HANDLE CreateSemaphore(
LPSECURITY_ATTRIBUIES lpSemaphoreAttributes, //安全属性
LONG lInitialCount, //初始化信号量数量
LONG lMaximumCount, //信号量的最大值
LPSTSTR lpName //为信号量命名
);创建成功返回信号量句柄
WaitFor...
函数ReleaseSemaphore()
函数BOOL ReleaseSemaphore(
HANDLE hSeamephore, //信号量句柄
LONG lReleaseSemaphore, //信号量将增加的量
LPONG lpPreviousCount //指向一个变量的指针,用于记录信号量的上一个计数
);
CloseHandle()
函数#include
#include
DWORD WINAPI ThreadProc1(LPVOID lpParameter);
DWORD WINAPI ThreadProc2(LPVOID lpParameter);
HANDLE hSemaphore = NULL; //用于接收事件句柄
int main() {
DWORD nID = 0;
HANDLE hThread[2] = { 0 };
hSemaphore = CreateSemaphore(NULL, 3, 10, NULL);
hThread[0] = CreateThread(NULL, 0, ThreadProc1, NULL, 0, &nID);
while (getchar()=='\n') {
ReleaseSemaphore(hSemaphore, 5, NULL);
}
CloseHandle(hSemaphore);
return 0;
}
DWORD WINAPI ThreadProc1(LPVOID lpParameter) {
char a[] = "********";
while (1) {
WaitForSingleObject(hSemaphore, INFINITE);
for (int i = 0; i < strlen(a); i++) {
printf("%c", a[i]);
}
printf("\n");
}
return 0;
}
我们设置了计数器为3的信号量,我们发现程序最开始只会输出三行,每当我们按一次回车键,就将信号量计数值值为5:
本篇文章的分享就到这里,如果大家发现有错误之处,还请大家指出来,我会非常虚心地学习。希望我们共同进步!!!