Windows环境下的多线程编程

Windows环境下的多线程编程
Windows环境下的多线程编程

Windows环境下的多线程编程浅讲这是一个简单的关于多线程编程的指南,任何初学者都能在这里找到关于编写简单多线程程序的方法。如果需要更多的关于这方面的知识可以参考MSDN或本文最后列出的参考文献。本文将以C语言作为编程语言,在Lcc-Win32编译器下进行编译调试,使用MS-Visual Studio的人可以参考其相关的使用手册。没有C语言基础的朋友可以找一本简单的教程看一下,相信会是有帮助的。

首先解释什么是线程,谈到这个就不得不说一下什么是进程,这是两个相关的概念。为了方便理解,我推荐大家下载并使用一下flashget(网际快车)这个下载软件,当然迅雷也行,只是不如flashget这么直观了。O.K.言归正传,当我们打开flashget这个软件时,就是启动了一个进程。这个进程就指flashget这个软件的运行状态。当我们用它来下载时都是在整个flashget软件里操作,所以可以把单个进程看作整个程序的最外层。然后我们可以看到,在用flashget下载时可以选择同时使用多少线程来下载的选项(这里介绍个小技巧,用超级兔子可以优化这里的选项,将最大的线程数限制从10改为30),这个选项就包含了我所要讲的线程的概念。当同时用比如5根线程来下载的话,flashget就会同时使用5个独立的下载程序来下载文件(各个线程通过某种方式通信,以确定各自所要下载的部分,关于线程间的通信在后面介绍)。所以线程可以看作是在进程框架下独立运行的与进程功能相关的程序。如果将windows看作进程的话,那么里面跑得QQ啊,MSN什么的都可以看作是windows的线程了。当然进程本身也可以看作是线程,只是凌驾于其它线程之上的线程罢了。另外比如我们使用浩方对战平台来网上对战,当用浩方启动魔兽时,由于运行的是两个不同的程序,那就是多进程编程了,这要比多线程复杂点,但也是复杂的有限,有兴趣的人可以参考MSDN上的相关资料。

最后声明一下,文中的例子都没有过多的注释,不过我都使用了最简单的例子(至少我个人已经想不出更简单的了),如果读者对理解这里的程序上有困难,那么我的建议是这篇文章已经不适合你了,你应该看一些更基础的书,比如《小学生基础算数》^_^。

为了使对多线程编程有个更感性的认识,这里先给出一个简单的多线程程序。

(注:这是一个利用标准C99函数实现的初级多线程实例,由于兼容性的问题可能在MS-Visual Studio下无法编译通过,遇到这个问题人可以跳过这个实例的实际运作方式或者改用Lcc-Win32)

//***********一个简单的多线程程序****************

#include

void thread (void *p)

{

while(1){

//这里可以加入一些你所希望线程执行的程序,比如:

//puts("This chick turns me on.");

//break;

}

endthread();

}

int main(void)

{

int b=0;

for(int i=0;i<30;i++){

printf("begin thread%d\n",b++);

beginthread(thread,0,NULL);

}

while(1);

return 1;

}

//***********************************************

代码的一开始是先指明所要用到的头文件,stdheaders.h是Lcc-Win32专门制作的包含了所有符合C99标准的库函数的头文件。这一点在后面不再指出,使用

MS-Visual Studio的人可以自行添入stdio.h等标准头文件。接着是一个子程序,这

个就是一般的线程程序,在C语言中把它作为一个子程序单独写出。再后面就是main 函数,它起的作用就好比整个程序的外框架,具体要做什么则有线程来处理。可以看到在main函数和线程函数中都有无限的循环,这是因为:如果mian函数执行完了,那么无论线程函数处于一个怎样的状态整个程序都会退出,而如果线程中没有无限循环,那么线程的自动结束并不会影响到main函数,是否要结束main函数可以根据实际需要。在main函数中有一个beginthread()函数,这个就是专门用来启动线程的函数,可以看到这个函数的第一个参数就是需要启动的线程的函数地址,不过本文是讨论如何在windows下实现多线程编程,就不对这个函数进行介绍了,而把更多的精力放到对后面windowsAPI(从C语言的观点来开,就是函数)的介绍上,并且beginthread()这个函数过于轻量级,在略微复杂一点的场合下就不能满足要求了。如果执行这个程序并把'//'符号去掉的话,可以发现屏幕的输出毫无章法,这是由于操作系统安时间片给每个线程分配执行时间,当一个线程还没执行完毕,但是分给它的时间片用完了,只得暂时停止工作,等到下一个分给它的时间片时继续工作,这就产生了屏幕上杂乱的结果(即各个线程交错执行的结果),要解决这个问题要关系到线程的同步,这将在后面介绍。

1 windows中几个常用多线程的函数介绍

=======================================================

CreateThread() //创建一个新的线程

CreateRemoteThread() //在另一个进程中创建一个线程(不在本文讨论范围内)ExitThread() //正常的结束一个线程的执行

TerminateThread() //立即中止线程的执行(理论上最好不去使用)

GetExitCodeThread() //得到线程结束时的退出码

Get/SetThreadPriority() //得到/设置线程的优先级

Suspend/ResumeThread() //挂起/启动一个线程

CloseHandle() //关闭一个线程的句柄

=======================================================

下面对其简要介绍

HANDLE CreateThread(

LPSECURITY_ATTRIBUTES lpThreadAttributes, //线程安全属性

DWORD dwStackSize, //线程堆栈的初始大小,一般设为0,使用默认设置

LPTHREAD_START_ROUTINE lpStartAddress, //线程函数的起始地址

LPVOID lpParameter, //线程函数的参数

DWORD dwCreationFlags, //创建方式

LPDWORD lpThreadId //线程标示符

};

‘lpThreadAttributes’作为线程安全属性设置参数,一般设为NULL,表示线程返回的句柄不能被子进程所继承。

‘dwStackSize’参数一般设为0以使用系统默认值,但是高级用户可以将其设为小于系统默认值以节省开销。

‘lpStartAddress’为线程函数的首地址。

‘lpParameter’为线程函数所需要的参数,并且必须以指针的方式传递,没有参数时可设为NULL。

‘dwCreationFlags’表示当线程创建后是先挂起不执行呢,还是立即执行。如果设为CREATE_SUSPENDED则先不执行,等待ResumeThread()函数来开启线程的执行,如果是0,则立即执行。

‘lpThreadId’是线程的标示号,每个线程的号不同,由操作系统设置,所以这个参数只要提供空间,而不需要赋值。通常设为NULL。

最后如果HANDLE返回NULL,表示出错了。(注:各类handle在windows的C观点来开只是一个void型指针,占用32bit空间,用来标示不同的API对象。)一个常见的该函数可以这样写:

#include

#include

#include

DWORD WINAPI ThreadFun(LPVOID IpParameter) //线程函数

{

char ch;

while(1)

{

if((ch=getchar())=='\n')

{

printf("111111");

Sleep(500);

}

}

}

void main(void)

{

DWORD dw;

HANDLE handle;

handle=CreateThread(NULL,0,ThreadFun,NULL,0,NULL);//error check is very important

while(1)

{

if(handle==NULL)

{

puts("Wrong!");

}

printf("\n主线程在运行!\n");

Sleep(1000);

}

}

*****************************************

其它的API(函数)的格式都十分简单,通过几个简单的实例就能看出怎么用了,如果需要具体格式可以参看MSDN或本文的参考文献。

这里只需要指出以下几点:

如果要结束一个线程,除了让其自动在执行完后退出外,ExitThread()应当作为首选。当还处于初级多线程编程阶段时,应当根本不去使用TerminateThread()这个函数,除非了解整个系统的一举一动。

如果没有特别需要,一般不建议设置优先级,所有线程可以使用默认值。

注:所有代码的err-check is omitted,实际代码还需要读者自己补上err-check 代码。

//*************实例1****************

#include

#include

#include

int repeat=1;

DWORD WINAPI Thread1(LPVOID lpParameter)

{

while(repeat)

{

puts("How r u?");

}

return 23; //(I)

}

DWORD WINAPI Thread2(LPVOID lpParameter)

{

DWORD exitcode=34;

while(repeat)

{

puts("Fine.");

}

ExitThread(exitcode); //(II)

return 21;

}

int KillBothThread(void)

{

repeat=0;

return 1;

}

int main(void)

{

HANDLE handle1,handle2;

DWORD dw1,dw2; //线程标示符

DWORD ec1,ec2; //退出码

//handle1=CreateThread(NULL,0,Thread1,NULL,0,&dw1); //(III)

handle1=CreateThread(NULL,0,Thread1,NULL,CREATE_SUSPENDED,&dw1);

Sleep(200);

handle2=CreateThread(NULL,0,Thread2,NULL,0,&dw2);

Sleep(200);

ResumeThread(handle1);

Sleep(400); //(IV)

SuspendThread(handle2);

Sleep(200);

ResumeThread(handle2); //(V)

KillBothThread();

Sleep(50); //(VI)

GetExitCodeThread(handle1,&ec1); //获得退出线程的退出码

GetExitCodeThread(handle2,&ec2);

CloseHandle(handle1);

CloseHandle(handle2);

printf("%d**%d**\n",ec1,ec2);

return 1;

}

//*******************************************

这个程序体现了前面所讲述的大部分精神,除了优先级的部分,都有所涉及。对于Thread1()使用的是普通的return来退出,而Thread2()则使用了带有强制性意味的ExitThread()函数,并且在最后的main()函数输出部分看出,通过这两种方式都可以获得预期的退出码,特别是在Thread2()线程,通过指定退出码的方式,就不会获得象21这样由return回的码了,这对于线程的出错诊断十分有用。由于在最开始所举的那个简单多线程的例子,由于C99标准函数无法获得退出码,那里的线程只能用在有限的场合中。所以(I)和(II)指出的退出线程方式可以根据需要灵活使用。

整个函数的过程是先启动Thread1线程,但是将其挂起,然后开启Thread2线程,这个线程一开动就马上执行,接着再打开Thread1,让它运行,最后再关闭Thread2。所以可以再最终的执行屏幕上看到,先是一连串的‘fine’然后再是‘fine’和‘how r u’的混合体,最后又成了只有‘how r u’。读者可以试着将标记(III)处的解释号去掉,然后删除后一行,比较看看有什么不同。通过实际运行的屏幕可以明显发觉,在标记(IV)处的输出行数要明显少于前面的行数,为什么时间长了,输出的行数确少了呢?这是由于系统这时要调度两个线程,很多时间开销在了线程的调度上,所以如果非必要,多线程只会浪费系统资源。

标记(V)是个十分易忽视的地方。这时Thread2被挂起了,也就是不会被执行,这

时对全局变量的改变只会影响Thread1,所以如果删掉(V)这句代码,那么最后的退出码就会得到一个0x103(十进制259)的表示线程还未退出的代码。例如现在标示(VI)处定义了sleep(50),如果去掉这句,那么就会得到"259**259**"的输出。还有线程切换和执行需要时间,如果这里换作sleep(20),那么可能有时得到正确的输出,有时又得到259这个代码,这是由于多线程的系统调用时间片是不确定的所导致的。

如果将上面程序稍加变动,变成如下的程序,但是效果一样。

//**************实例2***************

#include

#include

#include

//int repeat=1;

DWORD WINAPI Thread1(int *repeat)//

{

while(*repeat)

{

puts("How r u?");

}

return 23;

}

DWORD WINAPI Thread2(int *repeat)

{

DWORD exitcode=34;

while(*repeat)

{

puts("Fine.");

}

ExitThread(exitcode);

return 1;

}

int KillBothThread(int *repeat)

{

*repeat=0;

return 1;

}

int main(void)

{

HANDLE handle1,handle2;

DWORD dw1,dw2;

DWORD ec1,ec2;

int repeat=1;

//handle1=CreateThread(NULL,0,Thread1,NULL,0,&dw1);

handle1=CreateThread(NULL,0,Thread1,&repeat,CREATE_SUSPENDED,&dw1);

//do some err check

Sleep(200);

handle2=CreateThread(NULL,0,Thread2,&repeat,0,&dw2);

Sleep(200);

ResumeThread(handle1);

Sleep(400);

SuspendThread(handle2);

Sleep(200);

ResumeThread(handle2);

KillBothThread(&repeat);

Sleep(50);

GetExitCodeThread(handle1,&ec1);

GetExitCodeThread(handle2,&ec2);

printf("%d**%d**\n",ec1,ec2);

return 1;

}//***********************************

很明显的可以看出,变量repeat的位置已经不是全局变量了,但是其依旧对线程有控制作用,这时因为传递的是指针,线程函数每次执行都会去检查那个位置,所以就好像线程每次都会去额外的检测那个输入参数似的。

对于设置优先级一般应用的场合不多,本文限于篇幅不再多加讨论,需要了解的可以参看MSDN或本文的参考文献。

上面的线程属于一般的工作线程,比如可以用在保存文档,打印或者网络连接上,下面再介绍一类叫做消息线程的两个API函数,GetMessage()和PostThreadMessage()。

~~~~~~~~~~~~~~~

BOOL GetMessage( //从消息队列中取出一条消息

LPMSG lpMsg, //存放取到的消息信息

HWND hWnd, //接收消息的句柄,为NULL时表示当前线程的所有HWND型数据都可以接收向该线程发送的消息,否则可以指明由具体哪个hWnd来接收消息

UINT wMsgFilterMin, //消息队列的第一个消息

UINT wMsgFilterMax //消息队列的最后一个消息

};

~~~~~~~~~~~~~~~

BOOL PostThreadMessage( //利用该函数向线程发送消息

DWORD idThread, //目标线程的消息ID号,也就是线程的标示符

UINT Msg, //消息名

WPARAM wParam, //第一个消息参数

LPARAM lParam //第二个消息参数

};

~~~~~~~~~~~~~~~~

有一点windows编程经验的人应该可以通过对上面的参数分析知道如何来使用这两个函数,不过这里还是给出一个其基本应用,来加深了解。

//************实例3*********************

#include

#include

#include

#define ms1 WM_USER+1

#define ms2 WM_USER+2

#define ms3 WM_USER+3

MSG msg;

DWORD WINAPI Thread1(void *a)

{

puts("Start to receive message");

while(GetMessage(&msg,NULL,0,0)>0)

{

switch(msg.message)

{

case ms1:

puts("I have received msg1");

break;

case ms2:

puts("I have received msg2");

break;

case ms3:

puts("I have received msg3");

break;

default:

break;

}

}

puts("Quit");

return 0;

}

DWORD WINAPI Thread2(DWORD *dw)

{

PostThreadMessage(*dw,ms1,0,0);

Sleep(500);

//PostThreadMessage(*dw,ms2,0,0); //可以去掉'//'标示符看看效果

//Sleep(500);

PostThreadMessage(*dw,ms3,0,0);

Sleep(500);

PostThreadMessage(*dw,WM_QUIT,0,0);

Sleep(50);

return 1;

}

int main(void)

{

HANDLE handle1,handle2;

DWORD dw1,dw2;

handle1=CreateThread(NULL,0,Thread1,NULL,0,&dw1);

Sleep(1000); //(I)

handle2=CreateThread(NULL,0,Thread2,&dw1,0,&dw2);

PostThreadMessage(dw1,ms2,0,0);

Sleep(1000);

while(1);

return 1;

}//******************************************************

这个程序要注意以下几点:对于发送的消息要清楚了解其意义,比如发送的消息号是数字1,那么GetMessage()是收不到这个消息的,一般用户自定义的消息号从WM_USER(1024)开始。还有在标记(I)处,如果没有这句等待语句,那么第一个消息是收不到的,因为这时消息接收机制GetMessage()还没开始运作。只有在GetMessage()运行后发送出的消息才会有作用。

特别的,上面这种向线程传递消息的方法还可以用来进行线程间的通信。

最后在这一节指出,还有一种叫做纤程的东西可以由用户来控制各个程序的调用过程,但是用处不大,或者有时过于繁琐,有兴趣的读者可以参看本文的参考文献。

2 线程间的通信方法

多线程之间常常要交换数据,或者共享数据,这种通信在windows下一般有四种方法:

一是全局变量方式,这种方式在实例1中有很好的体现;

二是参数传递方式,实例2就是采用的这种方法;

三是消息传递方式,参看实例3;

四是线程间的同步,下面进行介绍;

3 互斥量

关于信号量的同步概念,大多都会设计死锁的避免问题,有很多有趣的算法,不过和编程相比过于复杂了,这里不再介绍,有兴趣的人可以查看一些相关资料。

windows提供的互斥量就是一个解决死锁的方案,并且功能十分强大,它不仅可以解决线程间的同步问题,甚至可以解决进程间的同步问题。不过说白了,这些信号量之间的一系列纷繁复杂的关系,最终还是关于临界段,也就是原子操作的设计。在进行有关对线程同步问题的编程前,了解这个知识是十分必要的。所谓原子操作,

就是指代码段必须一次性完成,在中间的执行过程中不容许被打断,或者说在分时系统中,临界段代码在执行时所占用的资源不能被其它程序或者代码所占用。可以把它认为是整个程序中最小执行单位,所以称作原子,这里再强调一下,原子操作是不容许被打断的(单线程)或者在执行过程中所占用的资源被其它程序修改(多线程或多进程)。考虑这样一种情况,有一个‘全局’变量a,这时有两个线程都要对其进行操作,其中一个x1线程是执行a=(a+1)*3,另一个x2线程是执行a=(a/3)-1,原则上如果两个线程被同步的很好,那么在两个线程分别执行同样次后a的值应该保持不变,但是如果没有同步就可能出现这样或与之类似的一种情况:当x1执行了a+1后,操作系统进行了线程切换,这时由x2占用系统的时间片,然后它进行了(a/3)的操作,这时线程又被切换到了x1,显然这样的无规则随机线程切换将导致结果是凌乱的,永远不会知道最终a的确定值。这时如果把x1的操作a=(a+1)*3看作一个整体原子,x2的a=(a/3)-1也看作一个原子的话,结果就不一样了。当x1执行了一般被x2占用了系统资源,但是x2发现变量a仍处于原子操作期,所以它就什么也不作,只是设置某个标记,告诉x1执行完这个原子操作后立即把系统资源还给x2,让x2执行,这样两个线程都可以彼此无扰的进行各自应该干的工作了。而这种等待原子操作完成并且重新占有系统资源的方式就是各种不同信号机制所要解决的,这里我们使用windows提供的互斥量来实现这个功能。

现面介绍几个常用的相关函数(API)。

====================================

CreateMutex();//创建一个互斥量

OpenMutex();//打开一个互斥量

ReleaseMutex();//释放一个互斥量

WaitForSingleObject();//等待某个信号

===================================

创建一个新的互斥量:CreateMutex();

~~~~~~~~~~~~~~~~~~~~

HANDLE CreateMutex(

LPSECURITY_ATTRIBUTES lpMutexAttributes,

BOOL bInitialOwner,

LPCTSTER lpName

);

lpMutexAttributes决定返回句柄是否可以被子进程继承,一般设为NULL,说明不能被子进程继承。

bInitialOwner决定互斥量的初始所有权。如果设为TRUE,则创建它的线程拥有它,也就是说一旦这个线程创建了这个互斥量,则这个线程就拥有了对临界段(原子操作)的所有权,这一权限直到它使用ReleaseMutex()释放这个互斥量为止。

lpName指向一个字符串,这个字符串为该互斥量的名字,并且注意该字符串区分大小写。如果没有名字可以设为NULL。起名字是为了在进程间实现同步,如果只在线程中可以不给互斥量起名,通过返回的句柄值就可以区分不同的互斥量。

这里提醒一点,如果有多个线程创建了同一个互斥量,那么实际上只创建了一个,其它只是打开了这个互斥量。一般打开互斥量只在多进程编程中有用,多线程则一般不用。

~~~~~~~~~~~~~~~~~~~~

打开一个已有的互斥变量:OpenMutex();

HANDLE OpenMutex(

DWORD dwDesiredAccess,

BOOL bInheritHandle,

LPCTSTR lpName

);

dwDesiredAccess指定互斥量访问的所有权,出于兼容性的考虑,一般设为MUTEX_ALL_ACCESS。

bInheritHandle指定句柄可否被子进程继承。如果为真,则可以继承。

lpName指明要打开的互斥量的名字(区分大小写)。

正如前面提到,这个函数一般用于进程间的同步。

~~~~~~~~~~~~~~~~~~~~~

释放对互斥量的所有权:ReleaseMutex();

BOOL ReleaseMutex(

HANDLE hMutex

);

hMutex既是要释放的互斥量句柄。

~~~~~~~~~~~~~~~~~~~~~

等待某个事件(比如这里我们所涉及到的是等待互斥量):WaitForSingleObject();

DWORD WaitForSingleObject(

HANDLE hHandle,

DWORD dwMilliseconds

);

hHandle为要等待的句柄,在这里就是要等待的互斥量。

dwMilliseconds为等待时间,单位为毫秒。设为0则检查是否等待到了后立即返

回,如果设为INFINITE则无限等待下去。

注:上面都没有涉及函数返回值的介绍,这是因为本文前面已经指出本文忽略err-check,所有有关资料可以参看MSDN或后面的参考文献。

在看应该怎么准确使用这些API之前我还要罗嗦几句。首先这里最后一个等待函数只介绍了最简单的一个函数,它只能等待一个对象,这里千万要注意一点,不能用这个函数等待超过1一个以上的对象,有的人可能会这样:

线程1:

{

if(WaitForSingleObject(A)==TRUE||WaitForSingleObject(B)==TRUE)

then(WaitForSingleObject(A)==TRUE||WaitForSingleObject(B)==TRUE)

then do something in thread 1;

}

线程2:

{

if(WaitForSingleObject(A)==TRUE||WaitForSingleObject(B)==TRUE)

then(WaitForSingleObject(A)==TRUE||WaitForSingleObject(B)==TRUE)

then do something in thread 2;

}

或者:

线程1:

{

if(WaitForSingleObject(A)==TRUE)

then(WaitForSingleObject(B)==TRUE)

then do something in thread 1;

}

线程2:

{

if(WaitForSingleObject(A)==TRUE)

then(WaitForSingleObject(B)==TRUE)

then do something in thread 2;

}

或者:

线程1:

{

if(WaitForSingleObject(A)==TRUE)

then(WaitForSingleObject(B)==TRUE)

then do something in thread 1;

}

线程2:

{

if(WaitForSingleObject(B)==TRUE)

then(WaitForSingleObject(A)==TRUE)

then do something in thread 2;

}

上面种种,无论怎样改变等待次序都会造成‘死锁’,因为这个函数不是为这种功能设计的,WaitForMultipleObjects()是专门为多信号等待设计的,多信号可以使用这个函数,不过限于篇幅不再介绍了,大家可以参考MSDN文档。

另外互斥量的使用在代码中就像是一个打包过程:等待直到获得互斥量->需要原子操作的代码->释放互斥量。从程序员的角度来看是十分简单的。O.K.接着让我们先看一个没有同步的线程执行结果(其实前面的几个例子都没有同步,读者可能已经发现他们的输出结果很乱)。

//************实例4*************************

#include

#include

#include

DWORD WINAPI Thread1(void *a)

{

while(1)

printf("He is a really cool guy at first sight.\n");

return 1;

}

DWORD WINAPI Thread2(void *a)

{

while(1)

printf("But every time meeting a girl,he's too shy to say a word.\n");

return 1;

}

int main(void)

{

HANDLE handle1,handle2;

DWORD dw1,dw2;

handle1=CreateThread(NULL,0,Thread1,NULL,0,&dw1);

handle2=CreateThread(NULL,0,Thread2,&dw1,0,&dw2);

Sleep(100);

return 1;

}

//**************************************

可以发现输出的结果是几乎让人无法看懂的,这时因为两个线程同时享用了标准输出设备(我们的显示器),现在我们可以看看如果使用了互斥量把标准输出设备包装成原子操作会有什么结果。

//***********实例5************************

#include

#include

#include

HANDLE hMutex;

DWORD WINAPI Thread1(void *a)

{

while(1)

{

WaitForSingleObject(hMutex,INFINITE);

//需要被包装成原子的代码段被包裹在WaitForSingleObject()和ReleaseMutex()之间。

printf("He is a really cool guy at first sight.\n");

ReleaseMutex(hMutex);

}

return 1;

}

DWORD WINAPI Thread2(void *a)

{

while(1)

{

WaitForSingleObject(hMutex,INFINITE);

printf("But every time meeting a girl,he's too shy to say a word.\n");

ReleaseMutex(hMutex);

}

return 1;

}

int main(void)

{

HANDLE handle1,handle2;

DWORD dw1,dw2;

hMutex=CreateMutex(NULL,FALSE,NULL);

handle1=CreateThread(NULL,0,Thread1,NULL,0,&dw1);

handle2=CreateThread(NULL,0,Thread2,&dw1,0,&dw2);

Sleep(1000);

return 1;

}

//***********************************************

下面是个额外的例子,为了让大家看看如何在进程间用互斥量进行同步。注意进程就是两个独立的执行程序,所以下面的两个程序要分开编译成exe可执行文件,然后分别运行,必须先运行program1然后再运行program2。

//************实例6********************

//----program 1----------------------------

//这个process先制造互斥量然后释放它

#include

#include

HANDLE hMutex;

int main(void)

{

hMutex=CreateMutex(NULL,TRUE,"lj");

puts("Before pressing any key to release the mutex,

please shift to the other process first.");

getch();

ReleaseMutex(hMutex);

puts("The mutex has been released.

Sheft to the other process and take a look.");

puts("Press any key for quit.");

getch();

return 1;

}

//----program 2----------------------------

//这个process等待被释放的互斥量然后捕获它

#include

#include

HANDLE hMutex;

int main(void)

{

hMutex=OpenMutex(MUTEX_ALL_ACCESS,TRUE,"lj");

puts("Waiting for the mutex.");

WaitForSingleObject(hMutex,INFINITE);

puts("Got it.");

puts("Press any key for quit.");

getch();

return 1;

}

//******************************************

通过上面的两个例子可以看到,互斥量不仅可以用在线程中,并且还可以在进程中发挥它的作用,但是它功能强大所以就太耗费系统资源,执行起来速度慢,针对这一情况MS又设计了只能在进程间使用的临界段,为了不和前面的互斥量相混,我们就称其为临界段,这样也是为了和其它资料相符。

4 临界段的使用

什么是临界段,简单来说可以把它看作是轻量级的互斥量,一种只能在一个线程内对多个进程起同步作用的信号量。不过它的效率高,执行速度快。常用的有:

========================================

InitializeCriticalSection(); //初始化一个临界段

EnterCriticalSection(); //等待直到获得临界段

TryEnterCriticalSection(); //尝试获得临界段,如果不成功,则马上退出

LeaveCriticalSection(); //释放临界段

DeleteCriticalSection(); //删除一个已建立的临界段

========================================

和之前一样,我们来看这些API的函数原型。

~~~~~~~~~~~~~~~~~~~~

InitializeCriticalSection(

LPCRITICAL_SECTION lpCriticalSection //指向临界段对象的指针

);

在使用临界段之前,必须先要声明一个类型为CRITICAL_SECTION的‘全局’变量,就好象之前在互斥量中做的一样,那里要声明一个指向互斥量的handle。然后在使用这个临界段时要先调用InitializeCriticalSection()这个函数来初始化临界段对象。接着就可以通过后面介绍的函数来进行同步了。

~~~~~~~~~~~~~~~~~~~~

VOID EnterCriticalSection(

LPCRITICAL_SECTION lpCriticalSection //指向临界段对象的指针

);

BOOL TryEnterCriticalSection(

LPCRITICAL_SECTION lpCriticalSection

);

这两个函数是类似的,都是获取对临界段的掌控权,但是前一个函数如果无法获得临界段的掌控权,就将一直执行这个操作,而后者就会立即退回。这个就类似互斥量中等待信号函数WaitForSingleObject()中使用INFINITE或者0作为参数相似。

~~~~~~~~~~~~~~~~~~~~~~

LeaveCriticalSection(

LPCRITICAL_SECTION lpCriticalSection

);

这个函数就类似于之前在互斥量中的ReleaseMutex()函数。但是要注意,如果一个没有临界段掌控权的线程调用了这个函数,那么另一个调用EnterCriticalSection()将可能永远等待下去,这是致命的bug,但是目前MS似乎无法解决这个问题,所以编程时要小心安排程序逻辑。

~~~~~~~~~~~~~~~~~~~~~~

DeleteCriticalSection(

LPCRITICAL_SECTION lpCriticalSection

);

根据名字可以显然看出这个函数的作用,这个函数可以用来当程序不再需要同步时清除临界段对象,以防止内存泄漏。

之前已经看过不同步产生的后果,这里就不再举例,直接来看一个简单的使用临界段例子:

//**************实例7****************

#include

#include

CRITICAL_SECTION cs;

DWORD WINAPI Thread(void *a)

{

for(int i=0;i<20;i++){

EnterCriticalSection(&cs);

printf("Girls are my only favorite.\n");

LeaveCriticalSection(&cs);

}

return 1;

}

int main(void)

{

InitializeCriticalSection(&cs);

HANDLE handle;

DWORD dw;

handle=CreateThread(NULL,0,Thread,NULL,0,&dw);

for(int i=0;i<10;i++){

EnterCriticalSection(&cs);

printf("She's so sexy and turns me on.\n");

/*if(i==9) //可以补上这句代码看看会发生什么

DeleteCriticalSection(&cs);*/

LeaveCriticalSection(&cs);

}

sleep(100);

//DeleteCriticalSection(&cs);

return 1;

}

//************************************

5 事件

通过前面的讲述,如果你是一个细心且天资聪颖的人话,那么就会自然的想到,上面的那些方法似乎都是针对某些共享资源的,这些方法都是将那些可能会被多个线程相互干扰的资源封装为原子操作。那么是否可以将线程进行有序的安排呢。当然可以,这就是这里所要讲述的内容。

前面的那些方法是无法用来控制线程的调度的,线程依然由操作系统进行随机(从用户的角度来看)的调度。MS为我们设计了事件这个方法以使得一般用户可以通过它来控制线程的调用操作。这里先指出,通过后面的介绍,读者将会发现,似乎事件和互斥量是十分类似的,几乎没有什么差别。的确,从某种角度来说它们是类似的,并且可以互换着使用,但是请记住,写在事件函数后的代码操作不是原子操作,事件只是用来启动某段代码的执行罢了。所以当使用事件时,线程内针对共享的资源还是要另外进行同步的。为了使读者更感性的理解这一点,下面有一段代码,可以试着运行一下,就清楚了(也可以先看后面的API介绍,再回过头来看这一

段)。

//**********实例8****************

#include

#include

#include

HANDLE hEvent; //事件句柄

DWORD WINAPI Thread1(void *a)

{

WaitForSingleObject(hEvent,INFINITE); //等待事件有信号

while(1)

printf("Fucking fucker, u r the bull shit.\n");

return 1;

}

DWORD WINAPI Thread2(void *a)

{

int i;

for(i=0;i<20;i++)

printf("I am here, and I have nothing to do.\n");

SetEvent(hEvent); //将事件设为有信号

while(1)

printf("I am here, and I have nothing to do.\n");

return 1;

}

int main(void)

{

HANDLE handle1,handle2;

DWORD dw1,dw2;

hEvent=CreateEvent(NULL,FALSE,FALSE,NULL); //建立事件信号量,并将其设置为无信号handle1=CreateThread(NULL,0,Thread1,NULL,0,&dw1);

handle2=CreateThread(NULL,0,Thread2,NULL,0,&dw2);

Sleep(2000);

return 1;

}

//****************************

在没有设置事件前,程序是很有秩序的执行Thread2(),但是一旦将事件设置为有信号,这时Thread1()也开始运行了,显示的文字就变得凌乱无序了。

常用的事件API有下面几个:

===================================

CreateEvent(); //创建一个事件

OpenEvent(); //打开一个已创建的事件

SetEvent(); //触发一个事件(将事件设为有信号)

ResetEvent(); //复位一个事件(将事件设为无信号)

PulseEvent(); //触发一个事件,被函数捕获后,再复位(具体请参看MSDN文档)

WaitForSingleObject(); //等待一个事件

相关主题
相关文档
最新文档