我原本以为在C++中有专门的创建多线程的函数或方法,现在才知道C++标准中还没有涉及到多线程,虽然没有标准的C++多线程,但是许多支持多线程的操作系统都提供了多线程库实现编写多线程程序。每一套操作系统都有自己的一套多线程类库,不过造成多线程编程没有标准可循,也就是没有移植性。不过现在好像有一个Boost线程库,Boost是由C++标准委员会类库工作组成员发起,致力于为C++开发新的类库的组织。现在它已经有近2000名成员。许多库都可以在Boost源码的发布版本中找到。为了使这些类库是线程安全的(thread-safe),Boost线程库被创建了,Boost线程就是面向标准的C++线程库,可以解决平台型问题,不受特定操作系统的限定,不过在这里我并不考虑Boost线程库,因为我是windows开发,使用windows提供的线程库开发程序就OK了,接下来也都是对面向windows线程库的多线程程序开发知识总结。有关线程的知识在本文下方有介绍。
在系统中当使用CreateProcess调用时,系统将创建一个进程和一个主线程,以下函数都是在主线程的基础上创建一个新的线程,即辅助线程(我是这么理解的)。
当一个线程被创建时,系统会为他创建一个内核对象和一个栈;这几乎是创建一个线程所占用的所有系统资源。
1).线程内核对象操作系统用它来管理线程,来存放线程的统计信息
2).线程栈,用于维护线程执行时所需要的所有函数参数和局部变量
什么是核心对象
核心对象是用于管理进程、线程和文件等诸多种类的大量资源,他的所属权是系统内核,并不属于某个进程。内核对象是一个数据结构,而非可执行代码的地址,当然也不是某一资源(比如互斥量)本身。它只是一个数据结构,包含了一些成员变量,而这些变量记录着对应资源相关的信息(比如对象的引用计数)。我们可以通过不同的函数名称来创建核心对象,比如:CreateProcess、CreateThread、CreateFile、CreateFileMapping、CreateSemaphore等函数。每个内核对象都包含一个使用计数(usage count),使用计数是所有内核对象都有的一个数据成员。初次使用一个内核对象时,其使用计数被设为1,当有其他进程使用这个内核对象时使用计数便加1,当该进程终止时使用计数便减1,当使用计数为0时,系统会自动将该内核对象销毁。当一个进程内部创建了一个内核对象,另外一个进程使用了这个内核对象,那么该内核对象的使用计数便为2,那么创建这个内核对象的进程结束时,这个内核对象的使用计数便减为1,不是0,不会销毁,只有当使用计数变为0时,该内核对象才会被销毁,所以内核对象的生命周期可能长与创建它的那个进程。那么我们创建或者调用了一个内核对象后,如何关闭这个内核对象那?无论以什么样的方式创建内核对象,我们都需要调用CloseHandle向系统表明我们已经结束使用该对象了,此时该内核对象的使用计数便会减1,直至使用计数为零才会被消除。假如我们使用完一个内核对象后忘记使用CloseHandle来关闭,那么可能会造成内存泄露,不过当使用该内核对象的进程终止时,系统会自动释放该进程所使用的所有资源,包括内核对象,所以由内核对象忘记关闭造成内存泄露的情况只会出现在进程运行期间,即运行期间的内存泄露。
现在有个问题,什么问题那?就是在一个进程中创建一个新的线程时其内核对象初始值不是1,而是2,这是怎么回事?
原因是这样的,当新起一个线程时,系统为他创建了一个内核对象,其使用计数为1,可是这个内核对象还需要挂入它所属进程的内核对象句柄表中,形成API可以访问的句柄资源,所以其引用计数便变成了2.当该进程在创建完这个线程后对其不关心的话,可以直接调用closehandle。
那么这个进程内核句柄表又是什么东东,先创建的线程的内核对象为什么要插入到内核句柄表中那?
进程的内核句柄表是系统在进程初始化时自动为其分配的,且只用于内核对象,初始时这个表是空的;当进程中新建一个内核对象时,不管是用哪个函数创建的这个内核对象,系统内核会为这个内核分配一个内存块,然后扫描本进程的内核句柄表,在查找到的一个空白位置进行设置,线程内核之所以插入到这里,是为了可以访问控制。
创建内核对象的函数都有返回值,返回值是一个和进程相关的句柄。这个句柄可以由进程中的所有线程使用。这样在同一个进程中多个线程可以很容易的互相通信,也可以很好实现线程间的同步。
CreateTread是微软提供的windows API创建新线程的函数,原型如下:
CreateThread创建一个新线程的大致步骤如下:
不过一般不推荐使用这个函数来创建线程,因为容易造成内存泄露,其中一个原因是当使用C/C++运行库(CRT)中的一个需要_tiddata结构的函数时,CRT会主动为该线程分配并初始化一个_tiddata块,此时假如在结束线程时不适用_endthreadex来终止线程的话,这个新建的数据块就不会销毁,从而导致内存泄露(对于一个使用CreateThread函数来创建线程,谁会使用_endthreadex那?),具体原因请参考《windows核心编程》的6.7.1节,在这里就不说明了,知道有这个问题就行了。
使用CreateThread方法来创建线程时,强制结束线程时需要使用CloseHandle来关闭内核对象,只有当线程自己运行完毕结束时才不需要调用。
CloseHandle可以关闭多种类型的对象,比如文件对象等,这里使用这个函数来关闭线程对象。调用时,hObject为待关闭的线程对象的句柄。
这四个都是C/C++运行库中的函数,而不是windows API 中的函数,不过这个运行库也是微软的VisualC++编译器提供的,如果不使用Microsoft的VisualC++编译器,你的编译器供应商有它自己的CreateThread替代函数。不管这个替代函数是什么,你都必须使用。
unsigned long _beginthread(
void( __cdecl *start_address )( void * ),
unsigned stack_size,
void *arglist
);
void _endthread( void );
_beginthread和_endthread这两个是传统的函数,已经被_beginthreadex和_endthreadex所取代。不过这两种创建线程的方法其实都是在其内部调用了CreateThread,windows系统内部创建线程的方法只有CreateThread。
unsigned long _beginthreadex(
void *security, /*SECURITY_ATTRIBUTES构造体指针.它是定义函数返回handle是否继承于子进程;也可NULL,handle不能继承;Win95必须是NULL */
unsigned stack_size,/* 新线程的堆栈大小 可为0 */
unsigned (__stdcall *start_address)(void *),/*新线程的起始地址,线程函数的指针 static */
void *arglist, /* 传给线程函数的参数的指针 */
unsigned initflag, /*新线程的初始状态(0:运行;CREATE_SUSPENDED:暂停;ResumeThread:执行线程)
unsigned *thrdaddr /*新线程ID的地址*/
);
void _endthreadex( unsigned retval );//retval : Thread exit code
_beginthread和_endthread参数比较少,局限性比较强,不能创建具有安全属性的线程,不能创建让线程立即挂起,也不能获取线程的ID值,线程退出时退出代码被硬编码为0.
而_beginthreadex和_endthreadex是_beginthread和_endthread的升级版,解决了_beginthread和_endthread的问题。不过有一点需要注意的是调用_endthread函数内部在调用ExitThread之前调用了CloseHandle,而_endthread不会调用CloseHandle来关闭内核句柄,需要自己调用,其实这样可以解决某些bug.
//用户界面线程的AfxBeginThread的原型如下:
CWinThread* AFXAPI AfxBeginThread(
CRuntimeClass* pThreadClass,
int nPriority,
UINT nStackSize,
DWORD dwCreateFlags,
LPSECURITY_ATTRIBUTES lpSecurityAttrs)
//其中:
//参数1是从CWinThread派生的RUNTIME_CLASS类;
//参数2指定线程优先级,如果为0,则与创建该线程的线程相同;
//参数3指定线程的堆栈大小,如果为0,则与创建该线程的线程相同;
//参数4是一个创建标识,如果是CREATE_SUSPENDED,则在悬挂状态创建线程,在线程创建后线程挂起,否则线程在创建后开始线程的执行。
//参数5表示线程的安全属性,NT下有用。
//工作者线程的AfxBeginThread的原型如下:
CWinThread* AfxBeginThread(AFX_THREADPROC pfnThreadProc,
LPVOID lParam,
int nPriority = THREAD_PRIORITY_NORMAL,
UINT nStackSize = 0,
DWORD dwCreateFlags = 0,
LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL
);
//返回值: 一个指向新线程的线程对象的指针
//pfnThreadProc : 线程的入口函数,声明一定要如下: UINT MyThreadFunction(LPVOID pParam),不能设置为NULL;
//pParam : 传递入线程的参数,注意它的类型为:LPVOID,所以我们可以传递一个结构体入线程.
//nPriority : 线程的优先级,一般设置为 0 .让它和主线程具有共同的优先级.
//nStackSize : 指定新创建的线程的栈的大小.如果为 0,新创建的线程具有和主线程一样的大小的栈
//dwCreateFlags : 指定创建线程以后,线程有怎么样的标志.可以指定两个值:CREATE_SUSPENDED : 线程创建以后,会处于挂起状态,直到调用:ResumeThread;0 : 创建线程后就开始运行.
//lpSecurityAttrs : 指向一个 SECURITY_ATTRIBUTES 的结构体,用它来标志新创建线程的安全性.如果为 NULL,那么新创建的线程就具有和主线程一样的安全性
要结束线程的两种方式:
1.线程函数返回(强烈推荐)
2.线程通过调用ExitThread函数“杀死”自己(避免使用)
3.同一个进程或另外一个进程中的线程调用TerminateThread函数(避免使用)
4.包含线程的进程终止运行(避免使用)
注:ExitThread函数可以终止线程的运行,并让操作系统清理该线程使用的所有系统资源,但是你的C/C++资源(如C++对象)不会销毁。ExitThread是WIndows API函数,最好不要使用,推荐使用C/C++运行库函数_endthreadex,如果不是Microsoft的C++编译器,那么就使用编译器推荐的函数
TerminateThread终止线程是异步的,他可以终止任何一个线程,但是除非拥有这个线程的进程结束,否则系统不会销毁这个线程的堆栈。microsoft是故意这么做的,否则,假如其他还在运行的线程要引用呗“杀死”的那个线程堆栈上的值,就会引起访问违规。
进程退出时应当先告知所有子线程退出,否则相当于对每个剩余的线程调用TerminateThread,造成正确的应用 程序清理工作不会执行:C++对象的析构函数不会被调用,数据不会回写到磁盘等等。不要让子线程在为被告之的情况下突然死亡。
1. 使用全局变量进行通信
定义一个全局变量是在同一个进程中不同线程间进行通信的最简单方法。对于标准类型的全局变量,建议使用volatile修饰符修饰,目的是告诉编译器无需对该变量进行任何的优化,即无需将它放到寄存器中,并且该值可以被外部修改。如果线程间传递的信息量较大时也可定义一个结构,通过传递只想该结构的指针进行传递信息。
2.使用自定义消息
从一个线程向另外一个线程发送消息来达到通信的目的。一个线程想另外一个线程发送消息是通过操作系统实现的。利用windows的消息驱动机制,当一个线程发出一个消息时,操作系统首先接收到这个消息然后将发往目标线程,目标线程必须建立有消息循环机制。
例子代码如下:
// ThreadTest.cpp : 定义控制台应用程序的入口点。
//
#include "stdafx.h"
#include
#include
#define WM_MESSAGE1 WM_USER+100
#define WM_MESSAGE2 WM_USER+101
unsigned __stdcall ThreadFunc(void* param)
{
printf("进入线程1\n");
MSG msg;
while (1)
{
if (::PeekMessage(&msg,NULL,0,0,PM_REMOVE))
{
switch(msg.message)
{
case WM_QUIT:
printf("线程1收到消息执行完毕退出\n");
return 0;
case WM_MESSAGE1:
printf("收到线程消息:%s\n",msg.wParam);
break;
case WM_MESSAGE2:
_endthreadex(2);
printf("测试是否执行");
default:break;
}
}
}
printf("线程1正常退出\n");
return 1;
}
unsigned __stdcall ThreadFunc2(void* param)
{
printf("进入线程2\n");
char *pTest = "我是参数,从线程2发来";
::PostThreadMessage(*(unsigned int*)param,WM_MESSAGE1,(WPARAM)pTest,0);
MSG msg;
while (1)
{
if (::PeekMessage(&msg,NULL,0,0,PM_REMOVE))
{
switch(msg.message)
{
case WM_QUIT:
printf("线程2收到消息执行完毕退出\n");
return 0;
case WM_MESSAGE2:
printf("线程2收到消息\n");
break;
default:break;
}
}
}
printf("线程2正常退出\n");
return 1;
}
int _tmain(int argc, _TCHAR* argv[])
{
unsigned int iThreadID = 0;
HANDLE hThread = (HANDLE)_beginthreadex(NULL,0,ThreadFunc,NULL,0,&iThreadID);
if (hThread == NULL)
{
printf("%s","ERROR");
return 0;
}
unsigned int iThreadID2 = 0;
HANDLE hThread2 = (HANDLE)_beginthreadex(NULL,0,ThreadFunc2,&iThreadID,0,&iThreadID2);
if (hThread2 == NULL)
{
printf("%s","Error");
::PostThreadMessage(iThreadID,WM_QUIT,0,0);
CloseHandle(hThread);
return 0;
}
Sleep(100);
::PostThreadMessage(iThreadID2,WM_MESSAGE2,0,0);
::PostThreadMessage(iThreadID,WM_MESSAGE2,0,0);
Sleep(100);
DWORD dwExitCode;
GetExitCodeThread(hThread,&dwExitCode);
if (dwExitCode != STILL_ACTIVE)
{
printf("线程1收到消息调用_endthreadex退出\n");
}
::PostThreadMessage(iThreadID2,WM_QUIT,0,0);
//线程终止运行时,其关联的线程对象不会自动释放,除非对这个对象的所有未结束的引用都被关闭
//在这里这两个线程虽然已经结束,引用计数也减去了1,可是进程的对象句柄表中还是用了这两个线程的
//的内核对象,所以这两个线程的内核对象引用计数都是1,你可以调用CloseHandle将其关闭,当然如果你还需要
//使用的话,可以不释放,不过这样在程序的运行期间,容易造成内存泄露
CloseHandle(hThread);
CloseHandle(hThread2)
system("pause");
return 0;
}
运行结果截图如下:
当进行多线程编程时,对于各个线程都要使用的资源,这些线程间就存在竞争关系,那么这些线程就需要同步和互斥操作。
现在几乎所有的进程线程同步互斥的操作机制都是由四种方法实现的:
1. 事件EVENT,用来通知线程一些事件已发生,从而启动后续任务操作。(创建内核对象)
2. 临界区Critical Section,通过对多线程的串行化访问公共资源或一段代码,速度快适合控制数据访问(无内核对象)
3. 互斥量Mutex,为协调对单独一个共享资源的访问而设计的。(创建内核对象)
4. 信号量Semaphore,为控制一个具有有限用户资源的操作而设计。(创建内核对象)
下面对这四中方法分别作介绍:
用事件(Event)来同步线程是最具弹性的了,一个事件有两种状态:有信号状态和无信号状态,事件又分为两种类型:手动重置事件和自动重置事件。手动重置事件被置为有信号状态时,会唤醒所有等待的线程,而一直保持为有信号状态直至在程序中把它设为无信号状态。自动重置事件为有信号状态时,会唤醒一个等待线程,然后恢复为无信号状态,所以用自动重置事件来同步两个线程比较理想。
手动重置事件
当我们启动一个进程的时候,它创建一个手动重置的无信号状态的事件,并且将句柄保存在一个全局变量中,这样这个进程中的所有线程就可以非常容易的访问同一个事件对象。
这里举个例子,程序一开始启动三个线程并挂起(事件阻塞),等待事件,当主线程读取文件到内存完毕时,每个线程都会访问这块内存,一个线程进行单词计数,一个线程进程词法检查,一个线程进行语法检查。这个三个线程开始部分都是相同的,每个函数的开始部分都调用WaitForSingleObject使线程暂停运行,直到主线程读取完毕,并SetEvent,这三个线程才进入开调度状态开始运行,不过这三个线程都是以只读状态访问这块内存,否则可能会造成内存错误,这就是这三个线程可是同时对这块内存进程操作的原因。如果计算机上配有三个以上CPU,理论上这个3个线程能够真正地同时运行,从而可以在很短的时间内完成大量的操作
手动重置例子:
// ThreadTest.cpp : 定义控制台应用程序的入口点。
//
#include "stdafx.h"
#include
#include
#include
char szBuffer[255] = {0};
HANDLE hEvent = NULL;
unsigned __stdcall ThreadFunc1(void* param)
{
WaitForSingleObject(hEvent,INFINITE);
printf("单词计数线程收到信息:%s",szBuffer);
return 0;
}
unsigned __stdcall ThreadFunc2(void* param)
{
WaitForSingleObject(hEvent,INFINITE);
printf("词法分析线程收到信息:%s",szBuffer);
return 0;
}
unsigned __stdcall ThreadFunc3(void* param)
{
WaitForSingleObject(hEvent,INFINITE);
printf("语法分析线程收到信息:%s",szBuffer);
return 0;
}
int _tmain(int argc, _TCHAR* argv[])
{
//读取本地文件到内存
FILE *pFile = fopen("E:\\test.txt","r");
if (pFile == NULL)
{
printf("读取本地文件失败\n");
return 0;
}
if (fgets(szBuffer,sizeof(szBuffer),pFile) == NULL)
{
sprintf_s(szBuffer,sizeof(szBuffer),"文件为空,不存在数据");
}
fclose(pFile);
//创建手动重置事件,并设置为无信号状态
hEvent = CreateEvent(NULL,//如果lpEventAttributes是NULL,此句柄不能被继承。
TRUE,//如果是TRUE,则是手动重置,否则是自动重置
FALSE,//指定事件对象的初始状态。如果为TRUE,初始状态为有信号状态;否则为无信号状态
"TEST"//指定事件的对象的名称,如果lpName为NULL,将创建一个无名的事件对象。
);
//启动三个操作线程并挂起(事件阻塞)
HANDLE hThread1 = (HANDLE)_beginthreadex(NULL,0,ThreadFunc1,NULL,0,NULL);
HANDLE hThread2 = (HANDLE)_beginthreadex(NULL,0,ThreadFunc2,NULL,0,NULL);
HANDLE hThread3 = (HANDLE)_beginthreadex(NULL,0,ThreadFunc3,NULL,0,NULL);
//设置事件为有信号状态,启动所有线程
SetEvent(hEvent);
//线程执行完毕关闭线程在进程内核对象句柄表中的内核对象
Sleep(100);
CloseHandle(hThread1);
CloseHandle(hThread2);
CloseHandle(hThread3);
CloseHandle(hEvent);
return 0;
}
自动重置事件
如果你使用自动重置的事件而不是人工重置的事件,那么应用程序的行为特性就有很大的差别。当主线程调用SetEvent之后,系统只允许一个辅助线程变成可调度状态。同样,也无法保证系统将使哪个线程变为可调度状态。其余两个辅助线程将继续等待。已经变为可调度状态的线程拥有对内存块的独占访问权。
让我们重新编写线程的函数,使得每个函数在返回前调用S e t E v e n t函数(就像Wi n M a i n函数所做的那样)。
当主线程将文件内容读入内存后,它就调用SetEvent函数,这样操作系统就会使这三个在等待的线程中的一个成为可调度线程。我们不知道系统将首先选择哪个线程作为可调度线程。当该线程完成操作时,它也将调用S e t E v e n t函数,使下一个被调度。这样,三个线程会以先后顺序执行,至于什么顺序,那是操作系统决定的。所以,就算每个辅助线程均以读/写方式访问内存块,也不会产生任何问题,这些线程将不再被要求将数据视为只读数据。
自动重置例子
char szBuffer[255] = {0};
HANDLE hEvent = NULL;
unsigned __stdcall ThreadFunc1(void* param)
{
WaitForSingleObject(hEvent,INFINITE);
printf("单词计数线程收到信息:%s\n",szBuffer);
sprintf_s(szBuffer,sizeof(szBuffer),"单词计数中修改数据\n");
SetEvent(hEvent);
return 0;
}
unsigned __stdcall ThreadFunc2(void* param)
{
WaitForSingleObject(hEvent,INFINITE);
printf("词法分析线程收到信息:%s\n",szBuffer);
sprintf_s(szBuffer,sizeof(szBuffer),"词法分析中修改数据\n");
SetEvent(hEvent);
return 0;
}
unsigned __stdcall ThreadFunc3(void* param)
{
WaitForSingleObject(hEvent,INFINITE);
printf("语法分析线程收到信息:%s\n",szBuffer);
sprintf_s(szBuffer,sizeof(szBuffer),"语法分析中修改数据\n");
SetEvent(hEvent);
return 0;
}
int _tmain(int argc, _TCHAR* argv[])
{
//读取本地文件到内存
FILE *pFile = fopen("E:\\test.txt","r");
if (pFile == NULL)
{
printf("读取本地文件失败\n");
return 0;
}
if (fgets(szBuffer,sizeof(szBuffer),pFile) == NULL)
{
sprintf_s(szBuffer,sizeof(szBuffer),"文件为空,不存在数据");
}
fclose(pFile);
//创建手动重置事件,并设置为无信号状态
hEvent = CreateEvent(NULL,//如果lpEventAttributes是NULL,此句柄不能被继承。
FALSE,//如果是TRUE,则是手动重置,否则是自动重置
FALSE,//指定事件对象的初始状态。如果为TRUE,初始状态为有信号状态;否则为无信号状态
"TEST"//指定事件的对象的名称,如果lpName为NULL,将创建一个无名的事件对象。
);
//启动三个操作线程并挂起(事件阻塞)
HANDLE hThread1 = (HANDLE)_beginthreadex(NULL,0,ThreadFunc1,NULL,0,NULL);
HANDLE hThread2 = (HANDLE)_beginthreadex(NULL,0,ThreadFunc2,NULL,0,NULL);
HANDLE hThread3 = (HANDLE)_beginthreadex(NULL,0,ThreadFunc3,NULL,0,NULL);
//设置事件为有信号状态,启动所有线程
SetEvent(hEvent);
//线程执行完毕关闭线程在进程内核对象句柄表中的内核对象
Sleep(100);
CloseHandle(hThread1);
CloseHandle(hThread2);
CloseHandle(hThread3);
CloseHandle(hEvent);
return 0;
}
使用临界区域的第一个忠告就是不要长时间锁住一份资源。这里的长时间是相对的,视不同程序而定。对一些控制软件来说,可能是数毫秒,但是对另外一些程序来说,可以长达数分钟。但进入临界区后必须尽快地离开,释放资源。如果不释放的话,会如何?答案是不会怎样。如果是主线程(GUI线程)要进入一个没有被释放的临界区,呵呵,程序就会挂了!临界区域的一个缺点就是:Critical Section不是一个核心对象,无法获知进入临界区的线程是生是死,如果进入临界区的线程挂了,没有释放临界资源,系统无法获知,而且没有办法释放该临界资源。这个缺点在互斥器(Mutex)中得到了弥补。Critical Section在MFC中的相应实现类是CcriticalSection。CcriticalSection::Lock()进入临界区,CcriticalSection::UnLock()离开临界区。
API临界区相关函数:
voidInitializeCriticalSection(LPCRITICAL_SECTION lpCriticalSection) //初始化临界区
voidEnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection) //进入临界区
voidLeaveCriticalSection(LPCRITICAL_SECTION lpCriticalSection) //离开临界区
voidDeleteCriticalSection(LPCRITICAL_SECTION lpCriticalSection) //释放临界区资源
使用示例:
#include
static CRITICAL_SECTION cs; // 定义临界区对象,如果程序是OOP的,可以定义为类non-static成员
// 在进入多线程环境之前,初始化临界区
InitializeCriticalSection(&cs);
void f()
{
EnterCriticalSection(&cs);// 进入临界区,其它线程则无法进入
// 安全访问该区域
LeaveCriticalSection(&cs); // 离开临界区,其它线程可以进入
}
// 释放临界区资源,当不再使用临界区时调用该函数
DeleteCriticalSection(&cs);
EnterCriticalSection和LeaveCriticalSection跟new/delete一样是成对调用的,很多时候在调用EnterCriticalSection以后不得不在多处LeaveCriticalSection,因为临界区内有return,break,continue,goto等等跳转,一不小心就会造成死锁。基于这个原因,在很多开源代码中都对CRITICAL_SECTION进行了封装。
互斥器的功能和临界区域很相似。区别是:Mutex所花费的时间比Critical Section多的多,但是Mutex是核心对象(Event、Semaphore也是),可以跨进程使用,而且等待一个被锁住的Mutex可以设定TIMEOUT,不会像Critical Section那样无法得知临界区域的情况,而一直死等。MFC中的对应类为CMutex。Win32函数有:创建互斥体CreateMutex(),打开互斥体OpenMutex(),释放互斥体ReleaseMutex()。Mutex的拥有权并非属于那个产生它的线程,而是最后那个对此Mutex进行等待操作(WaitForSingleObject等等)并且尚未进行ReleaseMutex()操作的线程。线程拥有Mutex就好像进入Critical Section一样,一次只能有一个线程拥有该Mutex。如果一个拥有Mutex的线程在返回之前没有调用ReleaseMutex(),那么这个Mutex就被舍弃了,但是当其他线程等待(WaitForSingleObject等)这个Mutex时,仍能返回,并得到一个WAIT_ABANDONED_0返回值。能够知道一个Mutex被舍弃是Mutex特有的。
当有线程拥有这个Mutex对象的时候,Mutex对象会处于无信号状态。如果没有任何一个线程当前拥有这个对象,Mutex对象将处于有信号状态
Mutex和Event有很大的不同,Mutex有Owner的概念,如果Mutex为ThreadA所拥有,那麽ThreadA执行WaitForSingleObject()时,并不会停下来,而会立即传回WAIT_OBJECT_0,而其他的Thread执行WaitForSingleObject()则会停下来,直到Mutex的所有权被Release出来或TimeOut。而Thread如何取得Mutex所有权呢?如果Mutex没有拥有者,则第一个呼叫WaitForSingleObject的Thread会拥有该Mutex。
相关函数如下:第一个CreateMutex
HANDLECreateMutex(
LPSECURITY_ATTRIBUTESlpMutexAttributes,
BOOLbInitialOwner,
LPCTSTRlpName
);
第一个参数表示安全控制,一般直接传入NULL。
第二个参数用来确定互斥量的初始拥有者。如果传入TRUE表示互斥量对象内部会记录创建它的线程的线程ID号并将递归计数设置为1,由于该线程ID非零,所以互斥量处于未触发状态。如果传入FALSE,那么互斥量对象内部的线程ID号将设置为NULL,递归计数设置为0,这意味互斥量不为任何线程占用,处于触发状态。
第三个参数用来设置互斥量的名称,在多个进程中的线程就是通过名称来确保它们访问的是同一个互斥量。不管是哪个Process/Thread,只要传入的名称叁数是相同的一个字串,那CreateMutex()传回值(hMutex, handle of Mutex)会指向相同的一个Mutex。这和Event相同。
函数访问值:
成功返回一个表示互斥量的句柄,失败返回NULL。
第二个打开互斥量
HANDLEOpenMutex(
DWORDdwDesiredAccess,
BOOLbInheritHandle,
LPCTSTRlpName
);
函数说明:
第一个参数表示访问权限,对互斥量一般传入MUTEX_ALL_ACCESS。详细解释可以查看MSDN文档。
第二个参数表示互斥量句柄继承性,一般传入TRUE即可。
第三个参数表示名称。某一个进程中的线程创建互斥量后,其它进程中的线程就可以通过这个函数来找到这个互斥量。函数访问值:成功返回一个表示互斥量的句柄,失败返回NULL。
第三个释放互斥量
BOOLReleaseMutex (HANDLEhMutex)
函数说明:
访问互斥资源前应该要调用等待函数WaitForSingleObject或者WaitForMultipleObjects,结束访问时就要调用ReleaseMutex()来表示自己已经结束访问,其它线程可以开始访问了。如果Mutex没有拥有者,则第一个呼叫WaitForSingleObject的Thread会拥有该Mutex。
// ThreadTest.cpp : 定义控制台应用程序的入口点。
//
#include "stdafx.h"
#include
#include
DWORD WINAPI Fun1Proc(LPVOID lpParameter);
DWORD WINAPI Fun2Proc(LPVOID lpParameter);
int Count=5;
HANDLE hMutex;
void main()
{
HANDLE hThread1,hThread2;
hThread1=CreateThread(NULL,0,Fun1Proc,NULL,0,NULL);
hThread2=CreateThread(NULL,0,Fun2Proc,NULL,0,NULL);
CloseHandle(hThread1);
CloseHandle(hThread2);
hMutex=CreateMutex(NULL,FALSE,NULL);
//TRUE代表主线程拥有互斥对象 但是主线程没有释放该对象 互斥对象谁拥有 谁释放
//FLASE代表当前没有线程拥有这个互斥对象
Sleep(4000);
}
DWORD WINAPI Fun1Proc(LPVOID lpParameter)
{
while (true)
{
WaitForSingleObject(hMutex,INFINITE);
if (Count>0)
{
printf("t1:%d\n",Count--);
}
else
{
break;
}
ReleaseMutex(hMutex);
}
return 0;
}
DWORD WINAPI Fun2Proc(LPVOID lpParameter)
{
while (true)
{
WaitForSingleObject(hMutex,INFINITE);
if (Count>0)
{
printf("t2:%d\n",Count--);
}
else
{
break;
}
ReleaseMutex(hMutex);
}
return 0;
}
信号量是最具历史的同步机制。信号量是解决producer/consumer问题的关键要素。对应的MFC类是Csemaphore。Win32函数CreateSemaphore()用来产生信号量。ReleaseSemaphore()用来解除锁定。Semaphore的现值代表的意义是可用的资源数,如果Semaphore的现值为1,表示还有一个锁定动作可以成功。如果现值为5,就表示还有五个锁定动作可以成功。当调用Wait…等函数要求锁定,如果Semaphore现值不为0,Wait…马上返回,资源数减1。当调用ReleaseSemaphore()资源数加1,当然不会超过初始设定的资源总数。
WaitForSingleObject
DWORD WINAPI WaitForSingleObject(
__in HANDLE hHandle,
__in DWORD dwMilliseconds
);
参数
UINT CFlushDlg::MyThreadProc( LPVOID pParam )
{
while(WaitForSingleObject(g_event,MT_INTERVAL)!=WAIT_OBJECT_0)
{
//………………
}
return 0;
}
在这个线程函数中可以通过设置MT_INTERVAL来控制这个线程的函数体多久执行一次,当事件为无信号状态时函数体隔MT_INTERVAL执行一次,当设置事件为有信号状态时,线程就执行完毕了。
WaitForMultipleObjects
WaitForMultipleObjects是Windows中的一个功能非常强大的函数,几乎可以等待Windows中的所有的内核对象(关于该函数的描述和例子见MSDN,)。但同时该函数在用法上却需要一定的技巧。
DWORD WaitForMultipleObjects(
DWORD nCount,
const HANDLE* lpHandles,
BOOL bWaitAll,
DWORD dwMilliseconds
);
当WaitForMultipleObjects等到多个内核对象的时候,如果它的bWaitAll 参数设置为false。其返回值减去WAIT_OBJECT_0 就是参数lpHandles数组的序号。如果同时有多个内核对象被触发,这个函数返回的只是其中序号最小的那个。如果为TRUE 则等待所有信号量有效在往下执行。(FALSE 当有其中一个信号量有效时就向下执行)
DWORD WINAPI ThreadProc(LPVOID lpParameter)
{
DWORD dwRet = 0;
int nIndex = 0;
while(1)
{
dwRet = WaitForMultipleObjects(nCount,pHandles,false,INFINITE);
switch(dwRet)
{
case WAIT_TIMEOUT:
break;
case WAIT_FAILED:
return 1;
default:
{
nIndex = dwRet - WAIT_OBJECT_0;
ProcessHanlde(nIndex++);
//同时检测其他的事件
while(nIndex < nCount) //nCount事件对象总数
{
dwRet = WaitForMultipleObjects(nCount - nIndex,&pHandles[nIndex],false,0);
switch(dwRet)
{
case WAIT_TIMEOUT:
nIndex = nCount; //退出检测,因为没有被触发的对象了.
break;
case WAIT_FAILED:
return 1;
default:
{
nIndex = nIndex + dwRet - WAIT_OBJECT_0;
ProcessHanlde(nIndex++);
}
break
}
}
}
break;
}
}
return 0;
}
大概windows下线程的知识就这么多,在这篇文章中并不是所有的东西包括源码都是原创,有部分是自己写的,有部分是从一些文章中参考来的,由于参考的文章比较多就不一一罗列了,我在这只是对线程的相关知识做个总结,以备自己查看。
1、基本概念
线程是进程的一个执行流,是CPU调度的基本单位,是CPU中能独立运行的最小单位。也可以称为轻量级进程。
进程是分配资源的最小单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。
2、选择多线程而不是多进程的理由?
(1)线程启动时间远小于进程启动时间。启动一个新的进程必须分配给它独立的地址空间,建立众多的数据表来维护它的代码段、堆栈段和数据段,这是一种"昂贵"的多任务工作方式,而运行于一个进程中的多个线程,它们彼此之间使用相同的地址空间,共享大部分数据。
(2)线程间切换时间远小于进程间切换的时间。
(3)线程开销远小于进程开销。总的说来,一个进程的开销大约是一个线程开销的30倍左右。
(4)线程间方便的通信机制。对不同进程来说,它们具有独立的数据空间,要进行数据的传递只能通过通信的方式进行,这种方式不仅费时,而且很不方便。线程则不然,由于同一进程下的线程之间共享数据空间,所以一个线程的数据可以直接为其它线程所用,这不仅快捷,而且方便。当然,数据的共享也带来其他一些问题,有的变量不能同时被两个线程所修改,有的子程序中声明为static的数据更有可能给多线程程序带来灾难性的打击,这些正是编写多线程程序时最需要注意的地方。
此外,多线程程序作为一种多任务、并发的工作方式,当然有以下的优点:
1) 提高应用程序响应。这对图形界面的程序尤其有意义,当一个操作耗时很长时,整个系统都会等待这个操作,此时程序不会响应键盘、鼠标、菜单的操作,而使用多线程技术,将耗时长的操作(time consuming)置于一个新的线程,可以避免这种尴尬的情况。
2) 使多CPU系统更加有效。操作系统会保证当线程数不大于CPU数目时,不同的线程运行于不同的CPU上。
3) 改善程序结构。一个既长又复杂的进程可以考虑分为多个线程,成为几个独立或半独立的运行部分,这样的程序会利于理解和修改。
3、进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。