多核编程学习笔记之同步(一)——采用Win32线程API实现 (转)
http://blog.csdn.net/woshiqianlong125/article/details/6159692
多核编程学习笔记之同步(一)——采用Win32线程API实现
分布式 2010-03-17 09:50:35 阅读66 评论0 字号:大中小订阅
I. 务虚
1.1 总体介绍
在Windows平台下可以通过Windows的线程库来实现多核编程,可以利用Win32API或MFC以及.Net Framework提供的接口来实现。实现方式的多样化给Windows编程带来了很大的灵活性,但也使得多线程编程变得复杂。对于多线程的程序可以使用Visual Studio调试工具进行调试,也可以使用多核芯片厂家的线程分析与调试工具进行调试以及优化。
1.2 Windows线程库介绍
Win32 API是Windows操作系统为内核以及应用程序之间提供的借口,将内核提供的功能进行函数封装,应用程序通过调用相关的函数获得相应的系统功能。Win32 API提供了一些列处理线程的函数接口,来向应用程序提供多线程的功能。用Win32 API直接编写应用程序要求程序员对Windows操作系统具有一定了解,否则会占用程序员很多时间来对系统的资源进行管理,降低程序员的工作效率。但直接用Win32 API编写的应用程序,程序的执行代码小,运行效率高。
MFC是微软提供的,称为“微软基础函数类库”(Microsoft Foundation Classes),即用类库的方式将Win32 API进行封装,以类的方式提供给开发者。在MFC类库中,提供了对多线程的支持。由于MFC是在Win32 API基础上进行封装,其基本原理与Win32 API的基本实现原理很类似,且MFC对同步对象进行了封装,因此对用户编程实现来说更加方便。由于MFC具有快速、简介、功能强大等特点,因此深受广大用户的喜爱。
1.3 并行环境、编程语言与编译器
在当前并行计算机上,比较流行的并行环境主要有3类:消息传递、共享存储和数据并行。
3种并行编程环境主要特征一览表:
特征 | 消息传递 | 共享存储 | 数据并行 |
典型代表 | MPI, PVM | OpenMP | HPF |
可移植性 | 所有主流并行计算机 | SMP, DSM | SMP, DSM, MPP |
并行粒度 | 进程级大粒度 | 进程极细粒度 | 进程极细粒度 |
并行操作方式 | 异步 | 异步 | 松散同步 |
数据存储模式 | 分布式存储 | 共享存储 | 共享存储 |
数据分配方式 | 显示 | 隐式 | 半隐式 |
学习入门难度 | 较难 | 容易 | 偏易 |
可扩展性 | 好 | 较差 | 一般 |
1)由上表看出,共享存储并行编程基于线程极细粒度并行,仅被SMP何DSM并行计算机所支持,可移植性不如消息传递并行编程。但是,由于他们支持数据的共享存储,所以并行编程的难度较小,但一般情况下,当处理机个数较多时,其并行性能明显不如消息传递编程。
2)消息传递并行编程基于大粒度的进程级并行,具有最好的可扩展性,几乎被所有当前的各类并行计算机所支持,且具有较好的可扩展性,但是,消息传递并行编程只能支持进程间的分布式存储模式,即各个进程只能直接访问其局部内存空间,而对其他进程的局部内存空间的访问只能通过消息传递来实现。因此,学习和使用消息传递并行编程的难度均大于共享存储和数据并行着两种模式。
在科学计算领域对并行编程支持已取得的相当成功的三项技术:自动并行化、数据并行语言(HPF)、共享存储并行编程接口(OpenMP)。
OpenMP:1997年,有Silicon Graphics领导的工业协会推出了penMP,这是一个与Fortran77和C语言绑定的非正式并行编程接口。协会后来扩展了这些绑定,以引入对Fortran95的支持,目前正在研究用于C++语言的绑定。如同HPF一样,在符合标准的程序中,OpenMP指令在单机编译器上被当做注释而忽略,并且对最终结果没有影响。OpenMP是非常适合于具有一致性访问的共享存储计算机的编程接口,然而它没有向用户提供如何实现对共享存储计算机的非一致性访问,或开发分布存储计算机中局部性的方法。在多处理机工作站机群上,OpenMP通常和MPI同时使用,OpenMP用于节点内,MPI用于节点间的消息传递。当代计算机结合了共享存储并行和分布存储并行两种特征,混合使用OpenMP和MPI指令似乎是支持该类计算机最有前途方式。
1)自动并行化。使用该技术,编译器把串行程序翻译为并行程序。尽管从最终用户的角度来看,这是一种理想的策略,但它尚未获得广泛的接受。不过自动并行化技术是支持其他很多高级策略的基础。
2)数据并行语言。以HPF为例,数据并行语言支持一种从分布存储计算机系统上跨处理器分解数组数据结构而派生来的并行风格。HPF提供一个数据分解指令集,用于提示编译器如何在这样的系统中获得较高的局并行和隐式并行。
3)共享存储并行编程接口。以OpenMP为例,共享存储并行最初关注任务的分解,因为这些接口所应用的理想目标平台是具有一致性访问的全局共享存储。OpenMP是共享存储并行编程接口中最卓越的实力。它使用指令系统说明经在哪里需要使用多线程以及如何给这些多线程分派任务。
1.4 使用Win32 线程API
Win32函数库中提供了操作系统多线程的函数,包括创建线程、管理线程、终止线程、线程同步等接口。
II. 务实
2.1 线程创建
1)线程必须从一个指定的函数开始执行,该函数称为“线程函数”,具有如下原型:
DWORD WINAPI ThreadFunc(LPVOID lpvThreadParm);
该函数的输入参数是一个LPVOID型的参数。该函数返回了一个DWORD型的值。通过这种定义方式,可以定义一个线程函数,还可以将一个线程的运行入口指向这个线程函数。
2) 所有的进程开始时都是只有一个线程,这个线程称为主线程。可以用微软提供的API来创建更多新的线程:
(部分摘自:MSDN)
(部分摘自:《多核程序设计》))
法一:CreateThread
Syntax
C++
HANDLE WINAPI CreateThread( __in_opt LPSECURITY_ATTRIBUTES lpThreadAttributes, __in SIZE_T dwStackSize, __in LPTHREAD_START_ROUTINE lpStartAddress, __in_opt LPVOID lpParameter, __in DWORD dwCreationFlags, __out_opt LPDWORD lpThreadId);
参数:
lpThreadAttributes:指向SECURITY_ATTRIBUTES结构的指针,该结构指定了线程的安全属性,默认为NULL;
dwStackSize:是栈的大小,一般设置为0;
lpStartAddress:是新线程开始执行时,线程函数的入口地址。它必须是将要被新线程执行的函数地址,不能为NULL;
lpParameter:是线程函数定义的参数。指向一个变量的指针。可以通过这个参数传送值,包括指针或者NULL;
dwCreationFlags:控制线程创建的附加标志,可以设置两种值。如果该参数为0.则表示线程在被创建后就会立即开始执行;如果该参数为CREATE_SUSPENDED,则系统产生线程后,该线程处于挂起状态,并不马上执行,直至函数ResumeThread被调用;
lpThreadId:为指向32位变量的指针,该参数接收所创建的线程ID号。如果创建成功则返回线程的句柄,否则返回NULL。
法二:_beginthread
使用process.h头文件中声明的C执行时期连接库函数_beginthread
原型:
uintptr_t _beginthread( void( *start_address )( void * ), unsigned stack_size, void *arglist );
参数解释:
start_address:起止地址的一个例行程序,执行一个新的线程,调用约定是_cdecl或者_clrcall,其实就是一个线程函数的地址;相应的线程函数是: void _cdecl ThreadProc(void* pParam);
stack_size:一个新的线程的堆的大小或者0;
arglist:传给新线程的参数列表,或者是NULL。
返回值:
如果创建成功,每个函数返回一个新线程的句柄;然而,如果新创建的线程退出的太快,_beginthread可能不会返回一个有效的句柄。如果现存的线程太多,_beginthread返回-1L或者一个错误,错误是EAGIN,如果参数无效或者栈的大小不正确,将返回EINVAL;当资源不足时(比如内存不足)返回EACCES。当设置错误类型为errno和_doserrno时将返回0。
注意区别:
在CreateThread中,线程函数被声明如下:
DWORD WINAPI ThreadFunc(LPVOID lpvThreadparm);
在_beginThread中,线程函数被声明如下:
void _cdecl ThreadProc(void* pParam);
LPVOID与PVOID,加了L表示long,因为在win32下LONG与INT一样长,占32位(4bytes),因此在这里LPVOID与PVOID一样,而PVOID是一个没有类型的指针,也就是说可以将任意类型的指针赋值给LPVOID类型的变量(一般作为参数传递),然后在使用的时候在转换回来。
2.2 main函数的参数完整形式
(部分摘自:http://topic.csdn.net/t/20020815/10/942336.html)
CODE:
main( int argc, char *argv[ ], char *envp[ ] ) { program-statements } wmain( int argc, wchar_t *argv[ ], wchar_t *envp[ ] ) { program-statements }
main函数标志着一个程序的开始和结束。一个C或者C++程序必须有一个main函数。如果你的代码依附于Unicode编程模式,你可以使用快字符版的main函数——wmain。
main函数与wmain函数有下面可选的三个参数,传统上被命名为argc, argv和envp(按照上面给出的参数列表顺序);
argc
一个整形变量特别指出了从命令行返回到程序的参数个数。因为程序名本身被认为是一个参数,所以argc至少为1;
argv
一个以空字符结尾的字符串数组。这个参数可以被声明为一个指向char类型的指针数组(char *argv[]或者wmain中的wchar_t *argv[])或者被声明为一个指向char类型的数组指针(char **argv 或者wmain中的wchar_t **argv)。第一个字符串(argv[0])是程序名,接下来的每个字符串是一个从命令行传入程序的参数。最后一个指针(argv[argc])是NULL。
envp
该参数是用于microsoft平台的
该参数是一个指向环境字符串数组的指针。该参数可以被声明为一个指向char类型的指针数组(char *envp[])或者一个指向char类型的数组指针(char** envp)。如果你的程序使用wmain而不是main,使用wchar_t数据类型取代char。数组的结尾是一个NULL。环境块传给main或者wmain的是当前环境的一个冻结的拷贝。如果你后来改变通过对运行putenv或_wputenv的环境,当前环境(作为getenv/_wgetenv and the _environ/ _wenviron variable的返回值)将会跟着改变,但是被envp指向的块不会变。这个参数是ANSI中与C相兼容,但与C++不兼容。
2.3 线程管理
1)设置线程的优先级
当一个线程创建时,它的优先级等于他所属的进程的优先级。可以通过调用SetThreadPriority函数来设置线程的相对优先级。线程的优先级是相对于其所属的进程的优先级而言的。
原型:
BOOL SetThreadPriority(HANDLE hPriority, int nPriority);
参数:
hPriority:指向待设置的线程句柄,线程与包含它的进程的优先级关系如下:
线程优先级 = 进程优先级 + 线程相对优先级
nPriority:线程的相对优先级别
对于线程优先级别:
空闲 | THREAD_PRIORITY_IDLE | 15 |
最低线程 | THREAD_PRIORITY_LOWEST | 2 |
低于正常线程 | THREAD_PRIORITY_BELOW_NORMAL | 1 |
正常线程 | THREAD_PRIORITY_NORMAL | 0 |
高于正常线程 | THREAD_PRIORITY_ABOVE_NORMAL | -1 |
最高线程 | THREAD_PRIORITY_HIGHEST | -2 |
关键时间 | THREAD_PRIORITY_TIME_CRITICAL | -15 |
进程优先级别包括如下:
实时 | REALTIME_PRIORITY_CLASS |
高 | HIGH_PRIORITY_CLASS |
高于正常 | ABOVE_NORMAL_PRIORITY_CLASS |
正常 | NORMAL_PRIORITY_CLASS |
低于正常 | BELOW_NORMAL_PRIORITY_CLASS |
空闲 | IDLE_PRIORITY_CLASS |
2)线程的挂起与恢复
进程中的每个线程都有挂起计数器(suspend count)。当挂起计数器值为0时,线程被执行;当挂起计数器值大于0时,调度器不去调度该线程。不能够直接访问线程的挂起计数器,但可以通过调用Windows API函数来改变它的值;也可以通过调用SuspendThread函数来挂起;还可以通过调用ResumeThread()函数来恢复。
原型:
DWORD SuspendThread(HANDLE hThread);
该函数用于挂起指定的线程,如果函数执行成功,则线程的执行被终止。每次调用SuspendThread()函数时,线程就将刮起计数器的值增为1。
DWORD ResumeThread(HANDLE hThread);
该函数利用结束线程的挂起状态来执行这个县城。每次调用ResumeThread()函数时,线程就将刮起计数器的值减1,若挂起计数器的值为0,则不会再减。
3)线程等待
Win32 API提供了一组能使线程阻塞其自身执行的等待函数WaitForSingleObject、WaitForMultipleObjects。这些函数在其参数中的一个或多个同步对象中产生了信号,或者在超过规定的等待时间才会返回。在等待函数未返回时,线程处于等待状态,线程只消耗很少的CPU时间。最常用的等待函数是:
原型:
DWORD WaitForSingleObject(HANDLE hHandle, DWORD dwMilliseconds);
描述:
等待直到指定对象状态是在激发态或者超时才返回。
参数:
hHandle:对象的句柄。用于一个对象的列表,该列表中的对象的句柄是特殊指定的。如果这个句柄在关闭的时候线程仍然在等待,则函数的行为将会无法确认。句柄必须具有SYNCHRONIZE权力。
dwMilliseconds:超时间隔,用毫秒来计算,如果一个非0值被指定,函数将等待直对象处于激发态或者超时。如果该参数被指定为0,那么函数在对象不是激发态的时不会进入到等待状态。该函数总是立即返回。如果该参数为INFINITE(无限的),则函数将只在对象是激发态的时候才返回。
而函数WaitForMultipleObjects可以用来同时检测多个对象,该函数的声明为:
DWORD WaitForMultipleObjects(DWORD nCount, CONST HANDLE* lpHandles, BOOL bWaitAll, DWORD dwMilliseconds);
参数:
nCount:lpHandles指向的数组中,对象句柄的个数。对象的最大数量表示为MAXIMUM_WAIT_OBJECTS。
lpHandles:对象句柄的数组。对于一个可以指定句柄的对象类型的列表。这个数组可以容纳不同类型的对象句柄,有可能不包含相同句柄的多个拷贝。如果在等待仍然继续的时候,这些句柄中有一个被关闭,函数的行为将无法确定。
bWaitAll:如果这个参数为TRUE,这个函数将在lpHandles数组中的所有对象的状态变为激发态时返回。如果为FALSE,函数将在这个数组中任何一个事件对象状态变为激发态时返回。在后面所述的情况中,返回值表示了一个对象的状态改变将导致函数的返回。
dwmillisecondes:超时间隔,使用毫秒。
4)线程终结
在线程函数返回时,线程自动终止。如果需要在线程的执行过程中终止则可调用函数:
(部分摘自MSDN)
原型:
void ExitThread(DWORD dwExitCode);
参数:
指定了针对于调用线程的退出指令。要获得线程的退出指令,使用GetExitCodeThread函数。
备注:
ExitThread是C语言中首选的用于退出线程的方法。在C++代码中,线程的退出发生在任何析构函数被调用之前或者其他任何自行清理的函数被执行之前。
当这个函数被调用的时候(明确的被调用或者是由一个线程程序返回),当前线程的栈空间被释放,所有等待被线程启动的I/O都将被取消,线程结束。因此,在C++代码中,你应该从你的线程函数中返回。
当这个函数被调用的时候,如果这个线程是进程中最后一个,那这个线程所属的进程也会被终结。
线程对象的状态变成了信号,通知释放其他任何曾经等待该线程结束的线程。线程的终结状态也从STILL_ACTIVE变成dwExitCode参数的值。
终结一个线程并不需要从操作系统中移除线程对象。一个线程对象会在最后一个线程句柄关闭之后被删除。
原型:
如果在线程的外面终止线程,则可调用下面的函数:
bool TerminateThread(HANDLE hThread, DWORD ExitCode);
备注:
TerminateThread用来退出一个线程。当退出动作发生时,被结束的线程没有任何机会去执行任何用户模式下的代码。与该线程相关联的DLL并没有被通知该线程正在结束。系统将释放该线程的初始栈。
在Windows Server 2003 和Windows XP/2000系统中,目标线程的初始栈不会被释放,这将导致资源溢出。
TerminateThread是一个危险的函数,该函数应该在最极端特殊的情况下使用。你只有在在明确知道目标线程正在做什么的时候才可以调用它,此时,你将在线程终结时控制所有的那些可能正在被该线程运行的代码。举个例子,该函数可能导致下列问题:
- 如果目标线程用够一块关键区域,那么在线程结束时该区域不会被释放;
- 如果目标线程正在从堆中分配内存空间,那么在线程结束时堆所占用的空间不会被释放;
- 如果目标线程正在执行确定的kernel32调用时,那么在线程结束时,线程所属的进程的kernel32状态将会不一致;
- 如果目标线程正在操纵共享的全局DLL的状态,那么在线程结束时该DLL的状态会被破坏,这将会影响到其他用户的DLL。
一个线程不能保护自身不被TerminateThread终结,除了该线程控制自己的句柄访问权。CreateThread和CreateProcess这两个函数返回的线程句柄具有THREAD_TERMINATE权利,所以任何一个具有这些句柄的调用都能够终结你的线程。
当这个函数被调用的时候,如果这个线程是进程中最后一个,那这个线程所属的进程也会被终结。
线程对象的状态变成了信号,通知释放其他任何曾经等待该线程结束的线程。线程的终结状态也从STILL_ACTIVE变成dwExitCode参数的值。
终结一个线程并不需要从操作系统中移除线程对象。一个线程对象会在最后一个线程句柄关闭之后被删除。
原型:
BOOL GetExitCodeThread(HANDLE hThread, LPDWORD lpExitCode);
参数:
hThread:线程句柄;
lpExitCode:指向一个32为变量的指针,该变量存储了要终结的线程的状态。
描述:
This function retrieves the termination status of the specified thread。
2.4 线程执行和资源存取
线程之间通信的两个基本问题是互斥和同步。
线程同步是指线程之间所具有的一种制约关系,一个现成的执行以来另一个线程的消息,当它没有得到另一个线程的消息时应该等待,直到消息到达时才被唤醒。
线程互斥是指对于共享资源,在各线程访问时的排他性。即当有多个线程都要使用某一共享资源时,同一时刻却只允许一个线程去使用,而其他要使用该共享资源的线程必须等待,直到占用资源者释放该共享资源。
实际上线程互斥是一种特殊的线程同步。
1)Win32线程同步的实现
Win32下的同步机制主要有:
- 全局变量
- 事件(Event)
- 临界区
- 互斥量
- 信号量
全局变量:
进程中的所有线程均可以访问所有的全局变量,因而全局变量成为Win32多线程通信的最简单方式。
CODE:
#include <windows.h> #include <iostream>
using namespace std;
int globalvar = false;
DWORD WINAPI ThreadFunc(LPVOID pParam) { cout << "ThreadFunc" << endl; Sleep(2000); globalvar = true; return 0; }
int main(int artc, char* argv[]) { HANDLE hThread = CreateThread(NULL, 0, ThreadFunc, NULL, 0, NULL); if (!hThread) { cout << "Thread Create Error!" << endl; CloseHandle(hThread); } while (!globalvar) { cout << "Thread while!" << endl; } cout << "Thread Exit!" << endl;
return 0; }
注意:在上面的代码中使用了全局变量和while循环来达到线程间的同步。使用全局变量时存在一些问题。
- 主线程等待globalvar时只有条件为假,即全局变量为真时才能推出,否则一直循环,这样就占用了CPU资源;
- 如果主线程的优先级高于ThreadFunc,则globalvar一直不会被置为真。
2)事件
事件是Win32提供的最灵活的线程间同步方式。
事件存在两种状态:
- 激发状态(Signaled or True)
- 未激发状态(Unsignal or False)
事件可分为两类:
- 手动设置:这种对象只能用程序来手动设置,在需要该时间或者事件发生时,采用SetEvent及ResetEvent来进行设置;
- 自动恢复:一旦时间发生并被处理后,将自恢复到没有事件状态,因此不需要再次设置。
创建事件的函数:
HANDLE CreateEvent(
LPSECURITY_ATTRIBUTES lpEventAttributes,
BOOL bManualReset,
BOOL bInitialState,
LPCTSTR lpName
);
参数:
- lpEventAttributes:与CreateThread第一个参数赖斯,是一个指向SECURITY_ATTRIBUTES结构的指针;
- bManualReset:代表时间的类型,是手动清除事件激发状态还是自动清除事件的激发状态。True为手动清除,此时函数将创建一个“手动重设”事件对象,该对象需要使用ResetEvent函数来设定时间的对象为非激发态;
- bInitialState:指明事件的初始状态,当该参数为TRUE时,时间对象的初始状态为激发态,否为不是。
- lpName:事件的名称,如果设为NULL则该对象被创建的时候没有名字。
使用事件机制要注意:
- 设置事件是否要自动恢复;
- 设置事件的初始状态;
- 如果跨进程访问时间,必须对事件命名。在对事件命名的时候,要注意不要与系统命名空间的其他全局命名对象冲突。
事件对象是属于内核对象,进程A可以通过调用OpenEvent函数根据对象的名字获得进程B中Event对象的句柄,然后对这个句柄可以使用ResetEvent、SetEvent和WaitForMultipleObjects等函数进行操作,来实现一个进程的线程控制另一个进程中的线程的运行。例如:
HANDLE hEvent = OpenEvent(EVENT_ALL_ACDESS, true, “MyEvent”);
ResetEvent(hEvent);
当程序中一个线程的运行要等待另外一个线程中一项特定操作的完成才能继续执行时,就可以使用事件对象来通知等待线程某个条件已经满足。
设定时间对象状态为激发态使用下面的函数:
原型:
BOOL WINAPI SetEvent( __in HANDLE hEvent );
参数:
hEvent:一个事件对象的句柄。使用CreateEvent或OpenEvent来返回这个句柄。
CODE:
#include "stdafx.h"
HANDLE evRead, evFinish;
void ReadThread(LPVOID param) { WaitForSingleObject(evRead, INFINITE); Sleep(1000); cout << "Reading..." << endl; SetEvent(evFinish); }
void WriteThread(LPVOID param) { Sleep(1000); cout << "Writing..." << endl; SetEvent(evRead); }
int main(int artc, char* argv[]) { evRead = CreateEvent(NULL, FALSE, FALSE, NULL); evFinish = CreateEvent(NULL, FALSE, FALSE, NULL);
_beginthread(ReadThread, 0, NULL); _beginthread(WriteThread, 0, NULL);
WaitForSingleObject(evFinish, INFINITE); Sleep(1000); cout << "*** Ending..." << endl; return 0; }
通过上面的程序,可以看出,虽然读线程比写线程先创建,但它要等待写线程复位读时间对象后,才能够继续执行。同样主线程必须等待写线程结束事件对象后,才能继续执行并结束程序。
3)临界区
临界区是一种防止多个线程同步执行一个特定代码段的机制。如果有多个线程试图同时访问临界区,那么在有一个线程进入后其他所有试图访问此临界区的线程都将被挂起,并一直持续到进入临界区的线程离开。
临界区适用于多个线程操作之间没有先后顺序但要求互斥的同步。多个线程访问同一个临界区的原则:
- 一次最多只能一个线程停留在临界区内;
- 不能让一个线程无限地停留在临界区内,否则其他线程将不能进入该临界区。
定义临界区的变量方法如下:
CRITICAL_SECTION gCriticalSection;
通常情况下,CRITICAL_SECTION结构体应该被定义为全局变量,便于进程中的所有线程可以按照变量名来引用该结构体。
相关的API包括:
描述:
初始化临界区(对象)
原型:
void WINAPI InitializeCriticalSection( __out LPCRITICAL_SECTION lpCriticalSection );
参数:
lpCriticalSection:一个纸箱临界区对象的指针;
备注:
单进程的所有县城可以使用一个临界区对象来实现同步互斥。不能保证每个线程对临界区拥有的顺序,稀有对每个线程都是公平的。
进程负责分配临界区对象使用的内存空间。可以通过声明一个变量CRITICAL_SECTION来实现。在使用一个临界区之前,一些线程必须初始化这个对象。
在临界区对象被初始化之后,进程的所有线程能够通过EnterCriticalSection, TryEnterCriticalSection或者LeaveCriticalSection函数指定这个对象,以此提供互斥来访问共享资源。
一个临界区对象不能被移动或复制。进程不可以修改这个对象,但是必须将它作为逻辑透明的。使用唯一的临界区函数来管理临界区对象。当你完成对临界区的使用后,调用DeleteCriticalSection函数。
一个临界区对象必须在它被重新初始化之前被删除。初始化一个已经被初始化过的临界区将导致无法确定的结果。
描述:
删除临界区对象,并释放所有不再使用的资源
原型:
void WINAPI DeleteCriticalSection( __inout LPCRITICAL_SECTION lpCriticalSection );
参数:
一个指向临界区对象的指针。这个对象必须已经通过InitializeCriticalSection函数被初始化。
备注:
在一个临界区对象被删除以后,不要在其它任何工作在临界区中的函数里引用这个对象(比如EnterCriticalSection, TryEnterCriticalSection, LeaveCriticalSection),除了InitializeCriticalSection和InitializeCriticalSectionAndSpinCount。如果你试图这么做,内存将被损坏,其它不可预料的错误也会发生。
如果一个临界区在它依然被其它线程占有的时候删除,那么其它正在等待使用该临界区对象的线程的状态将不确定。
描述:
等待占有指定的临界区对象。这个函数在调用线程被授予占有临界区对象的权利是返回。
原型:
void WINAPI EnterCriticalSection( __inout LPCRITICAL_SECTION lpCriticalSection );
参数:指向临界区对象的指针。
描述:
离开临界区。释放对指定临界区对象的占有权。
原型:
void WINAPI LeaveCriticalSection( __inout LPCRITICAL_SECTION lpCriticalSection );
参数:
临界区对象指针。
使用临界区编程的一般方法是:
CODE:
void WriteData() { EnterCriticalSection(&gCriticalSection); // do something LeaveCriticalSection(&gCriticalSection); }
举例:假如一个银行系统有两个线程执行取款任务,一个使用存折在柜台取款,一个使用银行卡在ATM取款,若不加控制,很可能账户余额不足于两次取款的总额,但还可以把钱取走。
CODE:
#include "stdafx.h"
int total = 100; HANDLE hEvent_Fin[2]; CRITICAL_SECTION gCriticalSection; // 全局临界区对象
void WithdrawThread1(LPVOID param) { EnterCriticalSection(&gCriticalSection); if (total - 90 >= 0) { total -= 90; cout << "Thread1: You withdrawed 90!" << endl; } else { cout << "Thread1: You do not have that mush money!" << endl; } LeaveCriticalSection(&gCriticalSection); SetEvent(hEvent_Fin[0]); Sleep(1000); }
void WithdrawThread2(LPVOID param) { EnterCriticalSection(&gCriticalSection); if (total >= 20) { total -= 20; cout << "Thread2: You withdrawed 20!" << endl; } else { cout << "Thread2: You do not have that much money!" << endl; } LeaveCriticalSection(&gCriticalSection); SetEvent(hEvent_Fin[1]); Sleep(1000); }
int main(int argc, char* argv[]) { hEvent_Fin[0] = CreateEvent(NULL, FALSE, FALSE, NULL); hEvent_Fin[1] = CreateEvent(NULL, FALSE, FALSE, NULL); InitializeCriticalSection(&gCriticalSection); _beginthread(WithdrawThread2, 0, NULL); _beginthread(WithdrawThread1, 0, NULL); WaitForMultipleObjects(2, hEvent_Fin, TRUE, INFINITE); DeleteCriticalSection(&gCriticalSection); Sleep(1000);
return 0; }
代码释义:
当一个线程执行EnterCriticalSection(&gCriticalSection)的时候申请进入临界区的时候,程序会判断gCriticalSection对象是否被锁定,如果没有锁定,那么线程就可以进入临界区进行资源访问,同时gCriticalSection被置为锁定状态;否则,说明有现成已进入了临界区,正在使用共享资源,调用线程将被阻塞以待gCriticalSection解锁。所以上面的程序不会出现100元取走110元的情况。
4)互斥量
互斥量通常用于协调多个线程进行活动,通过“锁定”和“取消解锁”资源,控制地共享资源的访问。当一个互斥量被一个线程锁定了,其他视图对其加锁的线程就会被阻塞。当对互斥量枷锁的线程解除了锁定之后,被阻塞的线程中有一个就会得到互斥量。
注意:锁定互斥量的线程一定也是对其解锁的线程。
互斥量的作用是保证每次能有一个线程获得互斥量,可使用CreateMutex函数来创建。
描述:
创建或者打开一个已命名或未命名的互斥体对象。要指定对象的访问掩码,使用CreateMutexEx函数。
原型:
HANDLE WINAPI CreateMutex(
__in_opt LPSECURITY_ATTRIBUTES lpMutexAttributes,
__in BOOL bInitialOwner,
__in_opt LPCTSTR lpName );
参数:
lpMutexAttributes:一个指向SECURITY_ATTRIBUTES结构体的指针。如果这个参数为NULL,这个句柄不能被子进程继承。结构体中的lpSecurityDescriptor成员为新互斥体指定了一个安全描述符。如果lpMutexAttributesWieNULL,则互斥体将得到一个默认的安全描述符。
bInitialOwner:如果这个参数为TRUE,而且被调用来粗昂见互斥体,调用先吃哪个获得互斥体的初始化占用权。否则,调用线程将不会获得互斥体占用权。
lpName:互斥体对象的名字,该名字被限制为MAX_PATH字符。名字是区分大小写的。
如果该名字与已经存在的互斥体对象的名字重名,这个函数取药MUTEX_AL_ACCESS访问权。在这种情况下bInitialOwner参数将被忽略,因为它已经被进程创建过了。如果lpMutexAtributes参数不是NULL,它将决定句柄是否可以被继承,但是他的安全描述符成员将被忽略。
如果lpName为NULL,互斥体对象将被创建,不需要名字。
相关的API有以下对互斥体的操作:
描述:
打开一个存在且被命名的互斥体对象。并返回该对象的句柄,使之后续访问。
原型:
HANDLE WINAPI OpenMutex(
__in DWORD dwDesiredAccess,
__in BOOL bInheritHandle,
__in LPCTSTR lpName );
参数:
dwDesiredAccess:想要访问互斥体对象。想要访问一个互斥体,只需要SYNCHRONIZE访问权。要改变互斥体的安全性,指定MUTEX_ALL_ACCESS,如果指定对象的安全描述符不允许对调用进程的请求访问权,这个函数将调用失败。
bInheriHandle:如果这个值为TRUE,被这个进程创建的进程将继承这个句柄。否则新创建的进程不会继承。
lpName:将要被打开的互斥体的名字。名字区分大小写。这个函数可以在私有空间中打开这些对象。
描述:
释放对互斥对象的占用,使之成为可用。
原型:
BOOL WINAPI ReleaseMutex( __in HANDLE hMutex );
参数:
互斥体兑现的句柄。CreateMutex或者OpenMutex函数返回这个句柄。
备注:
如果调用线程没有拥有这个互斥体独享,该函数调用失败。一个线程可以通过以下两种方式获得互斥体的占用权:通过将bInitialOwner参数设置为TRUE来创建它;通过在一个等待函数中指定它的句柄来获得。当线程不再需要拥有该互斥体的对象时,它将调用ReleaseMutex函数,这样另一个线程能够得到该对象的占有权。
使用互斥量的一般方法:
CODE:
void WriteData() { WaitForSingleObject(hMutex, INFINITE); // do something ReleaseMutex(hMutex); }
*****************************************
关闭句柄函数的使用
原型:
BOOL WINAPI CloseHandle( __in HANDLE hObject );
备注:
该函数可以关闭以下句柄:
- Access token
- Communications device
- Console input
- Console screen buffer
- Event
- File
- File mapping
- I/O completion port
- Job
- Mailslot
- Memory resource notification
- Mutex
- Named pipe
- Pipe
- Process
- Semaphore
- Thread
- Transaction
- Waitable timer
该函数应该在对象使用完后调用。
*****************************************
CODE:
/* * Description:互斥量的使用方法 */ #include "stdafx.h"
#define THREAD_INSTANCE_NUMBER 3 LONG g_fResourceinUse = FALSE; LONG g_lCounter = 0;
DWORD ThreadProc(void* pData) { // LPVOID int ThreadNumberTemp = (*(int*)pData); HANDLE hMutex; cout << "ThreadProc: " << ThreadNumberTemp << " is running!" << endl; // 打开该互斥体之后,该线程便独自占有该互斥体 if ((hMutex = OpenMutexA(MUTEX_ALL_ACCESS, FALSE, "Mutex.Test")) == NULL) { cout << "Open Mutex error!" << endl; } cout << "ThreadProc" << ThreadNumberTemp << " gets the mutex" << endl; ReleaseMutex(hMutex); // 释放互斥体句柄 CloseHandle(hMutex); return 0; }
int main(int argc, char* argv[]) { int i; DWORD ID[THREAD_INSTANCE_NUMBER]; HANDLE h[THREAD_INSTANCE_NUMBER]; HANDLE hMutex; if ((hMutex = OpenMutexA(MUTEX_ALL_ACCESS, FALSE, "Mutex.Text")) == NULL) { // 如果名字为Mutex.Text的互斥体没有被创建 // 则调用CreateMutex函数来创建 if ((hMutex = CreateMutexA(NULL, FALSE, "Mutex.Test")) == NULL) { cout << "Create Mutex error!" << endl; return 0; } } for (i = 0; i < THREAD_INSTANCE_NUMBER; i++) { // 创建线程 h[i] = CreateThread (NULL, 0, (LPTHREAD_START_ROUTINE)ThreadProc, (void*)&ID[i], 0, &(ID[i])); if (h[i] == NULL) { cout << "CreateThread error!" << ID[i] << endl; } else { cout << "CreateThread: " << ID[i] << endl; } } WaitForMultipleObjects(THREAD_INSTANCE_NUMBER, h, TRUE, INFINITE); cout << "Close the Mutex Handle!" << endl; CloseHandle(hMutex);
return 0; }
5)信号量
信号量是一个核心对象,拥有一个计数器,可用来管理大量有限的系统资源。当技术值大于0时,信号量为有信号状态;当计数值为0时,信号量处于无信号状态。
相关API:
描述:
创建信号量
原型:
HANDLE WINAPI CreateSemaphore(
__in_opt LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,
__in LONG lInitialCount,
__in LONG lMaximumCount,
__in_opt LPCTSTR lpName );
参数:
lpSemaphoreAttributes:一个纸箱SECURITY_ATTRIBUTES结构体的指针。如果这个参数为NULL,子进程将不能继承这个句柄;
lInitialCount:信号量的初始值。这个值必须大于等于0.并且小于等于lMaximumCount。信号量的状态当其计数器大于0时被激发,当计数器等于0时恢复正常。
lMaximumCount:信号量对象的最大值。这个值必须大于0。
lpName:信号量对象的名字。
描述:
释放信号量。
原型:
BOOL WINAPI ReleaseSemaphore(
__in HANDLE hSemaphore,
__in LONG lReleaseCount,
__out_opt LPLONG lpPreviousCount );
描述:
打开信号量。和其他核心对象一样,信号量也可以通过名字进行跨进程访问。
原型:
HANDLE WINAPI OpenSemaphore(
__in DWORD dwDesiredAccess,
__in BOOL bInheritHandle,
__in LPCTSTR lpName );
III. 编程总结
3.1 无论线程函数中执行怎样的代码,即使有死循环,只要该线程所属的进程退出了,该线程也会结束。
CODE:
#include <iostream> #include <windows.h> #include <process.h>
using namespace std;
void ThreadFunc1(PVOID param) { while(1) { Sleep(2000); cout << "Thread 1" << endl; } }
void ThreadFunc2(PVOID param) { while(1) { Sleep(2000); cout << "Thread 2" << endl; } }
void ThreadFunc3(PVOID param) { while(1) { Sleep(2000); cout << "Thread 3" << endl; } }
int main(int argc, char* argv[]) { cout << "************* Main Thread *************" << endl;
_beginthread(ThreadFunc1, 0, NULL); _beginthread(ThreadFunc2, 0, NULL); _beginthread(ThreadFunc3, 0, NULL); Sleep(10000); cout << "************* End *******************" << endl;
return 0;
上述代码在主线程执行10秒后随主线程(进程)一并退出。
注意:上述代码运行在双核机器上,但是从console输出情况来看,三个函数是同步进行的!
3.2 通过CreateThread函数给线程函数传递值
该函数中第四个参数是给线程函数传递参数。
该参数的类型是:LPVOID,即任何指针类型
CODE:
DWORD WINAPI FunTwo(LPVOID param) { while(true) { Sleep(1000); cout << "FunTwo: " << *(int*)param << endl; } return 0; }
// 传递参数
HANDLE hand1 = CreateThread(NULL, 0, FunOne, (void*)&input, CREATE_SUSPENDED, NULL);
注意参数传递时候的形式:(void*)&input,表示将input的地址转化成void类型的空指针,然后传递到线程函数中后,先要将其转化成int类型的指针,然后使用*取出其中的值。
X. 异常
x.1 Can't create TCP/IP socke
在解决此问题是,我发现如果连接代码放在线程函数之外,一切均正常,这基本证明了是线程函数出了问题,仔细检查线程,发现_beginthread函数出现在循环体中,而控制该函数的if条件语句在每次while循环中都成立,所以不停地创建新的线程,而线程中又不断地重新建立连接数据库的通道,一旦达到建立链接的上限就会报此错误。