VC++ 线程同步 总结
注:所谓同步,并不是多个线程一起同时执行,而是他们协同步调,按预定的先后次序执行。
与线程相关的基本函数包括:
CreateThread:创建线程
CloseHandle:关闭线程句柄。注意,这只会使指定的线程句柄无效(减少该句柄的引用计数),启动句柄的检查操作,如果一个对象所关联的最后一个句柄被关闭了,那么这个对象会从系统中被删除。关闭句柄不会终止相关的线程。
线程是如何运行的呢?这又与你的CPU有关系了,如果你是一个单核CPU,那么系统会采用时间片轮询的方式运行每个线程;如果你是多核CPU,那么线程之间就有可能并发运行了。这样就会出现很多问题,比如两个线程同时访问一个全局变量之类的。它们需要线程的同步来解决。所谓同步,并不是多个线程一起同时执行,而是他们协同步调,按预定的先后次序执行。
Windows下线程同步的基本方法有3种:互斥对象、事件对象、关键代码段(临界区),下面一一介绍:
互斥对象属于内核对象,包含3个成员:
1.使用数量:记录了有多少个线程在调用该对象
2.一个线程ID:记录互斥对象维护的线程的ID
3.一个计数器:前线程调用该对象的次数
与之相关的函数包括:
创建互斥对象:CreateMutex
判断能否获得互斥对象:WaitForSingleObject
对于WaitForSingleObject,如果互斥对象为有信号状态,则获取成功,函数将互斥对象设置为无信号状态,程序将继续往下执行;如果互斥对象为无信号状态,则获取失败,线程会停留在这里等待。等待的时间可以由参数控制。
释放互斥对象:ReleaseMutex
当要保护的代码执行完毕后,通过它来释放互斥对象,使得互斥对象变为有信号状态,以便于其他线程可以获取这个互斥对象。注意,只有当某个线程拥有互斥对象时,才能够释放互斥对象,在其他线程调用这个函数不得达到释放的效果,这可以通过互斥对象的线程ID来判断。
#include <Windows.h>
#include <stdio.h> //线程函数声明
DWORD WINAPI Thread1Proc( LPVOID lpParameter);
DWORD WINAPI Thread2Proc( LPVOID lpParameter); //全局变量
int tickets = ;
HANDLE hMutex; int main()
{
HANDLE hThread1;
HANDLE hThread2;
//创建互斥对象
hMutex = CreateMutex( NULL, //默认安全级别
FALSE, //创建它的线程不拥有互斥对象
NULL); //没有名字
//创建线程1
hThread1 = CreateThread(NULL, //默认安全级别
, //默认栈大小
Thread1Proc,//线程函数
NULL, //函数没有参数
, //创建后直接运行
NULL); //线程标识,不需要 //创建线程2
hThread2 = CreateThread(NULL, //默认安全级别
, //默认栈大小
Thread2Proc,//线程函数
NULL, //函数没有参数
, //创建后直接运行
NULL); //线程标识,不需要 //主线程休眠4秒
Sleep();
//主线程休眠4秒
Sleep();
//关闭线程句柄
CloseHandle(hThread1);
CloseHandle(hThread2); //释放互斥对象
ReleaseMutex(hMutex);
return ;
} //线程1入口函数
DWORD WINAPI Thread1Proc( LPVOID lpParameter)
{
while(TRUE)
{
WaitForSingleObject(hMutex,INFINITE);
if(tickets > )
{
Sleep();
printf("thread1 sell ticket : %d\n",tickets--);
ReleaseMutex(hMutex);
}
else
{
ReleaseMutex(hMutex);
break;
}
} return ;
} //线程2入口函数
DWORD WINAPI Thread2Proc( LPVOID lpParameter)
{
while(TRUE)
{
WaitForSingleObject(hMutex,INFINITE);
if(tickets > )
{
Sleep();
printf("thread2 sell ticket : %d\n",tickets--);
ReleaseMutex(hMutex);
}
else
{
ReleaseMutex(hMutex);
break;
}
} return ;
}
使用互斥对象时需要小心:
调用假如一个线程本身已经拥有该互斥对象,则如果它继续调用WaitForSingleObject,则会增加互斥对象的引用计数,此时,你必须多次调用ReleaseMutex来释放互斥对象,以便让其他线程可以获取:
//创建互斥对象
hMutex = CreateMutex( NULL, //默认安全级别
TRUE, //创建它的线程拥有互斥对象
NULL); //没有名字
WaitForSingleObject(hMutex,INFINITE);
//释放互斥对象
ReleaseMutex(hMutex);
//释放互斥对象
ReleaseMutex(hMutex);
下面看事件对象,它也属于内核对象,包含3各成员:
1.使用计数
2.用于指明该事件是自动重置事件还是人工重置事件的布尔值
3.用于指明该事件处于已通知状态还是未通知状态。
自动重置和人工重置的事件对象有一个重要的区别:当人工重置的事件对象得到通知时,等待该事件对象的所有线程均变为可调度线程;而当一个自动重置的事件对象得到通知时,等待该事件对象的线程中只有一个线程变为可调度线程。
与事件对象相关的函数包括:
创建事件对象:CreateEvent
HANDLE CreateEvent( LPSECURITY_ATTRIBUTES lpEventAttributes, BOOL bManualReset, BOOL bInitialState,LPCTSTR lpName);
设置事件对象:SetEvent:将一个这件对象设为有信号状态
BOOL SetEvent( HANDLE hEvent );
重置事件对象状态:ResetEvent:将指定的事件对象设为无信号状态
BOOL ResetEvent( HANDLE hEvent );
下面仍然使用买火车票的例子:
#include <Windows.h>
#include <stdio.h> //线程函数声明
DWORD WINAPI Thread1Proc( LPVOID lpParameter);
DWORD WINAPI Thread2Proc( LPVOID lpParameter); //全局变量
int tickets = ;
HANDLE g_hEvent; int main()
{
HANDLE hThread1;
HANDLE hThread2;
//创建事件对象
g_hEvent = CreateEvent( NULL, //默认安全级别
TRUE, //人工重置
FALSE, //初始为无信号
NULL ); //没有名字
//创建线程1
hThread1 = CreateThread(NULL, //默认安全级别
, //默认栈大小
Thread1Proc,//线程函数
NULL, //函数没有参数
, //创建后直接运行
NULL); //线程标识,不需要 //创建线程2
hThread2 = CreateThread(NULL, //默认安全级别
, //默认栈大小
Thread2Proc,//线程函数
NULL, //函数没有参数
, //创建后直接运行
NULL); //线程标识,不需要 //主线程休眠4秒
Sleep();
//关闭线程句柄
//当不再引用这个句柄时,立即将其关闭,减少其引用计数
CloseHandle(hThread1);
CloseHandle(hThread2);
//关闭事件对象句柄
CloseHandle(g_hEvent);
return ;
} //线程1入口函数
DWORD WINAPI Thread1Proc( LPVOID lpParameter)
{
while(TRUE)
{
WaitForSingleObject(g_hEvent,INFINITE);
if(tickets > )
{
Sleep();
printf("thread1 sell ticket : %d\n",tickets--);
}
else
break;
} return ;
} //线程2入口函数
DWORD WINAPI Thread2Proc( LPVOID lpParameter)
{
while(TRUE)
{
WaitForSingleObject(g_hEvent,INFINITE);
if(tickets > )
{
Sleep();
printf("thread2 sell ticket : %d\n",tickets--);
}
else
break;
} return ;
}
程序运行后并没有出现两个线程买票的情况,而是等待了4秒之后直接退出了,这是什么原因呢?问题出在了我们创建的事件对象一开始就是无信号状态的,因此2个线程线程运行到WaitForSingleObject时就会一直等待,直到自己的时间片结束。所以什么也不会输出。
如果想让线程能够执行,可以在创建线程时将第3个参数设为TRUE,或者在创建完成后调用
SetEvent(g_hEvent);
程序的确可以实现买票了,但是有些时候,会打印出某个线程卖出第0张票的情况,这是因为当人工重置的事件对象得到通知时,等待该对象的所有线程均可变为可调度线程,两个线程同时运行,线程的同步失败了。
也许有人会想到,在线程得到CPU之后,能否使用ResetEvent是得线程将事件对象设为无信号状态,然后当所保护的代码运行完成后,再将事件对象设为有信号状态?我们可以试试:
//线程1入口函数
DWORD WINAPI Thread1Proc( LPVOID lpParameter)
{
while(TRUE)
{
WaitForSingleObject(g_hEvent,INFINITE);
ResetEvent(g_hEvent);
if(tickets > )
{
Sleep();
printf("thread1 sell ticket : %d\n",tickets--);
SetEvent(g_hEvent);
}
else
{
SetEvent(g_hEvent);
break;
}
} return ;
}
线程2的类似,这里就省略了。运行程序,发现依然会出现卖出第0张票的情况。这是为什么呢?我们仔细思考一下:单核CPU下,可能线程1执行完WaitForSingleObject,还没来得及执行ResetEvent时,就切换到线程2了,此时,由于线程1并没有执行ResetEvent,所以线程2也可以得到事件对象了。而在多CPU平台下,假如两个线程同时执行,则有可能都执行到本应被保护的代码区域。
所以,为了实现线程间的同步,不应该使用人工重置的事件对象,而应该使用自动重置的事件对象:
hThread2 = CreateThread(NULL,,Thread2Proc,NULL0,NULL);
并将原来写的ResetEvent和SetEvent全都注释起来。我们发现程序只打印了一次买票过程。我们分析一下原因:
当一个自动重置的事件得到通知后,等待该该事件的线程中只有一个变为可调度线程。在这里,线程1变为可调度线程后,操作系统将事件设为了无信号状态,当线程1休眠时,所以线程2只能等待,时间片结束以后,又轮到线程1运行,输出thread1 sell ticket :100。然后循环,又去WaitForSingleObject,而此时事件对象处于无信号状态,所以线程不能继续往下执行,只能一直等待,等到自己时间片结束,直到主线程睡醒了,结束整个程序。
正确的使用方法是:当访问完对保护的代码段后,立即调用SetEvent将其设为有信号状态。允许其他等待该对象的线程变为可调度状态:
DWORD WINAPI Thread1Proc( LPVOID lpParameter)
{
while(TRUE)
{
WaitForSingleObject(g_hEvent,INFINITE);
if(tickets > )
{
Sleep();
printf("thread1 sell ticket : %d\n",tickets--);
SetEvent(g_hEvent);
}
else
{
SetEvent(g_hEvent);
break;
}
} return ;
}
总结一下:事件对象要区分人工重置事件还是自动重置事件。如果是人工重置的事件对象得到通知,则等待该事件对象的所有线程均变为可调度线程;当一个自动重置的事件对象得到通知时,只有一个等待该事件对象的线程变为可调度线程,且操作系统会将该事件对象设为无信号状态。因此,当执行完受保护的代码后,需要调用SetEvent将事件对象设为有信号状态。
下面介绍另一种线程同步的方法:关键代码段。
关键代码段又称为临界区,工作在用户方式下。它是一小段代码,但是在代码执行之前,必须独占某些资源的访问权限。
我们先介绍与之先关的API函数:
使用InitializeCriticalSection初始化关键代码段
使用EnterCriticalSection进入关键代码段:
使用LeaveCriticalSection离开关键代码段:
使用DeleteCriticalSection删除关键代码段,释放资源
我们看一个例子:
#include <Windows.h>
#include <stdio.h> //线程函数声明
DWORD WINAPI Thread1Proc( LPVOID lpParameter);
DWORD WINAPI Thread2Proc( LPVOID lpParameter); //全局变量
int tickets = ;
CRITICAL_SECTION g_cs; int main()
{
HANDLE hThread1;
HANDLE hThread2;
//初始化关键代码段
InitializeCriticalSection(&g_cs);
//创建线程1
hThread1 = CreateThread(NULL, //默认安全级别
, //默认栈大小
Thread1Proc,//线程函数
NULL, //函数没有参数
, //创建后直接运行
NULL); //线程标识,不需要 //创建线程2
hThread2 = CreateThread(NULL, //默认安全级别
, //默认栈大小
Thread2Proc,//线程函数
NULL, //函数没有参数
, //创建后直接运行
NULL); //线程标识,不需要 //主线程休眠4秒
Sleep();
//关闭线程句柄
CloseHandle(hThread1);
CloseHandle(hThread2);
//关闭事件对象句柄
DeleteCriticalSection(&g_cs);
return ;
} //线程1入口函数
DWORD WINAPI Thread1Proc( LPVOID lpParameter)
{
while(TRUE)
{
//进入关键代码段前调用该函数判断否能得到临界区的使用权
EnterCriticalSection(&g_cs);
Sleep();
if(tickets > )
{
Sleep();
printf("thread1 sell ticket : %d\n",tickets--);
//访问结束后释放临界区对象的使用权
LeaveCriticalSection(&g_cs);
Sleep();
}
else
{
LeaveCriticalSection(&g_cs);
break;
}
} return ;
} //线程2入口函数
DWORD WINAPI Thread2Proc( LPVOID lpParameter)
{
while(TRUE)
{
//进入关键代码段前调用该函数判断否能得到临界区的使用权
EnterCriticalSection(&g_cs);
Sleep();
if(tickets > )
{
Sleep();
printf("thread2 sell ticket : %d\n",tickets--);
//访问结束后释放临界区对象的使用权
LeaveCriticalSection(&g_cs);
Sleep();
}
else
{
LeaveCriticalSection(&g_cs);
break;
}
} return ;
}
在这个例子中,通过在放弃临界区资源后,立即睡眠引起另一个线程被调用,导致两个线程交替售票。
下面看一个多线程程序中常犯的一个错误-线程死锁。死锁产生的原因,举个例子:线程1拥有临界区资源A,正在等待临界区资源B;而线程2拥有临界区资源B,正在等待临界区资源A。它俩各不相让,结果谁也执行不了。我们看看程序:
#include <Windows.h>
#include <stdio.h> //线程函数声明
DWORD WINAPI Thread1Proc( LPVOID lpParameter);
DWORD WINAPI Thread2Proc( LPVOID lpParameter); //全局变量
int tickets = ;
CRITICAL_SECTION g_csA;
CRITICAL_SECTION g_csB;
int main()
{
HANDLE hThread1;
HANDLE hThread2;
//初始化关键代码段
InitializeCriticalSection(&g_csA);
InitializeCriticalSection(&g_csB);
//创建线程1
hThread1 = CreateThread(NULL, //默认安全级别
, //默认栈大小
Thread1Proc,//线程函数
NULL, //函数没有参数
, //创建后直接运行
NULL); //线程标识,不需要 //创建线程2
hThread2 = CreateThread(NULL, //默认安全级别
, //默认栈大小
Thread2Proc,//线程函数
NULL, //函数没有参数
, //创建后直接运行
NULL); //线程标识,不需要
//关闭线程句柄
//当不再引用这个句柄时,立即将其关闭,减少其引用计数
CloseHandle(hThread1);
CloseHandle(hThread2); //主线程休眠4秒
Sleep(); //关闭事件对象句柄
DeleteCriticalSection(&g_csA);
DeleteCriticalSection(&g_csB);
return ;
} //线程1入口函数
DWORD WINAPI Thread1Proc( LPVOID lpParameter)
{
while(TRUE)
{
EnterCriticalSection(&g_csA);
Sleep();
EnterCriticalSection(&g_csB);
if(tickets > )
{
Sleep();
printf("thread1 sell ticket : %d\n",tickets--);
LeaveCriticalSection(&g_csB);
LeaveCriticalSection(&g_csA);
Sleep();
}
else
{
LeaveCriticalSection(&g_csB);
LeaveCriticalSection(&g_csA);
break;
}
} return ;
} //线程2入口函数
DWORD WINAPI Thread2Proc( LPVOID lpParameter)
{
while(TRUE)
{
EnterCriticalSection(&g_csB);
Sleep();
EnterCriticalSection(&g_csA);
if(tickets > )
{
Sleep();
printf("thread2 sell ticket : %d\n",tickets--);
LeaveCriticalSection(&g_csA);
LeaveCriticalSection(&g_csB);
Sleep();
}
else
{
LeaveCriticalSection(&g_csA);
LeaveCriticalSection(&g_csB);
break;
}
} return ;
}
在程序中,创建了两个临界区对象g_csA和g_csB。线程1中先尝试获取g_csA,获取成功后休眠,线程2尝试获取g_csB,成功后休眠,切换回线程1,然后线程1试图获取g_csB,因为g_csB已经被线程2获取,所以它线程1的获取不会成功,一直等待,直到自己的时间片结束后,转到线程2,线程2获取g_csB后,试图获取g_csA,当然也不会成功,转回线程1……这样交替等待,直到主线程睡醒,执行完毕,程序结束。
VC++ 线程同步 总结的更多相关文章
- 转:VC++线程同步-事件对象
这是整理孙鑫VC得到的关于线程同步方面的笔记. n 事件对象也属于内核对象,包含一个使用计数,一个用于指明该事件是一个自动重置的事件还是一个人工重置的事件的布尔值,另一个用于指明该事件处于 ...
- VC++线程同步之临界区(CriticalSection)
1.相关文件和接口 #include <windows.h> CRITICAL_SECTION cs;//定义临界区对象 InitializeCriticalSection(&cs ...
- windows核心编程---第八章 使用内核对象进行线程同步
使用内核对象进行线程同步. 前面我们介绍了用户模式下线程同步的几种方式.在用户模式下进行线程同步的最大好处就是速度非常快.因此当需要使用线程同步时用户模式下的线程同步是首选. 但是用户模式下的线程同步 ...
- Windows线程同步(下)
线程同步三:事件 CreateEvent:Creates or opens a named or unnamed event object. HANDLE WINAPI CreateEvent( _I ...
- 老版VC++线程池
在一般的设计中,当需要一个线程时,就创建一个,但是当线程过多时可能会影响系统的整体效率,这个性能的下降主要体现在:当线程过多时在线程间来回切换需要花费时间,而频繁的创建和销毁线程也需要花费额外的机器指 ...
- VC线程同步方法
VC MFC中线程同步对象的区别 临界区 CCriticalSection,在用户模式工作,适用于保护线程间共享资源,一个线程可以多次Lock不会出错.不支持在多进程之间工作.互斥量 CM ...
- 《Windows核心编程系列》八谈谈用内核对象进行线程同步
使用内核对象进行线程同步. 前面我们介绍了用户模式下线程同步的几种方式.在用户模式下进行线程同步的最大好处就是速度非常快.因此当需要使用线程同步时用户模式下的线程同步是首选. 但是用户模式下的线程同步 ...
- [ 高并发]Java高并发编程系列第二篇--线程同步
高并发,听起来高大上的一个词汇,在身处于互联网潮的社会大趋势下,高并发赋予了更多的传奇色彩.首先,我们可以看到很多招聘中,会提到有高并发项目者优先.高并发,意味着,你的前雇主,有很大的业务层面的需求, ...
- C#多线程之线程同步篇3
在上一篇C#多线程之线程同步篇2中,我们主要学习了AutoResetEvent构造.ManualResetEventSlim构造和CountdownEvent构造,在这一篇中,我们将学习Barrier ...
随机推荐
- ios学习笔记(二)第一个应用程序--Hello World
原文地址:http://blog.csdn.net/shangyuan21/article/details/18416537 上一篇文章,Windows7上使用VMWare搭建iPhone开发环境介绍 ...
- dbms_sql包的用法
http://blog.itpub.net/20948385/viewspace-691398 对于一般的select操作,如果使用动态的sql语句则需要进行以下几个步骤: open cursor ...
- 8007003Windows Update遇到未知错误
如果在检查更新时收到 Windows Update 错误 80070003,则需要删除 Windows 用于标识计算机更新的临时文件.若要删除临时文件,请停止 Windows Update 服务,删除 ...
- SQL优化SQL tuning
1. 索引不合适,走主键进行了key lookup查找 说明索引没有覆盖到where条件 或者 orderby 或者 group by的列
- React组件开发
目录: 属性:props 内联样式 状态记忆 : state 生命周期 访问DOM 表单输入 承接快速入门篇:http://www.cnblogs.com/jasonnode/p/4444504.ht ...
- Hadoop之TaskAttemptContext类和TaskAttemptID类
先来看看TaskAttemptContext的类图 : Figure1:TaskAttemptContext类图 用户向Hadoop提交Job(作业),Job在JobTracker对象的控制下执行.J ...
- selenium webdriver设置超时
webdriver类中有三个和时间相关的方法: 1.pageLoadTimeout 2.setScriptTimeout 3.implicitlyWait pageLoadTimeout from s ...
- java总结第二次(剩余内容)//类和对象1
7.成员变量和局部变量 成员变量:在类中定义,用来描述对象将要有什么 局部变量:在类的方法中定义,在方法中保存临时数据 区别:作用域不同 局部变量的作用域仅限于定义它的方法 成员变量的作用域在整个类内 ...
- Java总结第一次//有些图片未显示,文章包含基础java语言及各种语句
一.java入门 1.Java入门学习框架: 2.常用的DOS命令: dir(directory) : 列出当前目录下的文件以及文件夹 md(make directory) : 创建目录 r ...
- E2PROM与Flash的引脚图
E2PROM(24C02):