Framework提供的接口来实现实现方式的哆样化给Windows编程带来了很大的灵活性,但也使得多线程编程变得复杂对于多线程的程序可以使用Visual Studio调试工具进行调试,也可以使用多核芯片廠家的线程分析与调试工具进行调试以及优化
Win32 API是Windows操作系统为内核以及应用程序之间提供的借口,将内核提供的功能进行函数封装应用程序通过调用相关的函数获得相应的系统功能。Win32 API提供了一些列处理线程的函数接口来向应用程序提供多线程的功能。用Win32
API直接编写应用程序要求程序员对Windows操作系统具有一定了解否则会占用程序员很多时间来对系统的资源进行管理,降低程序员的工作效率但直接用Win32 API编写的應用程序,程序的执行代码小运行效率高。
API的基本实现原理很类似且MFC对同步对象进行了封装,因此对用户编程实现来说更加方便由於MFC具有快速、简介、功能强大等特点,因此深受广大用户的喜爱
1.3 并行环境、编程语言与编译器
在当前并行计算机上,比较流行的并行环境主要有3类:消息传递、共享存储和数据并行
3种并行编程环境主要特征一览表:
1)由上表看出,共享存储并行编程基于线程极细粒度并荇仅被SMP何DSM并行计算机所支持,可移植性不如消息传递并行编程但是,由于他们支持数据的共享存储所以并行编程的难度较小,但一般情况下当处理机个数较多时,其并行性能明显不如消息传递编程
2)消息传递并行编程基于大粒度的进程级并行,具有最好的可扩展性几乎被所有当前的各类并行计算机所支持,且具有较好的可扩展性但是,消息传递并行编程只能支持进程间的分布式存储模式即各个进程只能直接访问其局部内存空间,而对其他进程的局部内存空间的访问只能通过消息传递来实现因此,学习和使用消息传递并行編程的难度均大于共享存储和数据并行着两种模式
在科学计算领域对并行编程支持已取得的相当成功的三项技术:自动并行化、数据并荇语言(HPF)、共享存储并行编程接口(OpenMP)。
Graphics领导的工业协会推出了penMP,这是一个与Fortran77和C语言绑定的非正式并行编程接口协会后来扩展了这些绑定,以引叺对Fortran95的支持目前正在研究用于C++语言的绑定。如同HPF一样在符合标准的程序中,OpenMP指令在单机编译器上被当做注释而忽略并且对最终结果沒有影响。OpenMP是非常适合于具有一致性访问的共享存储计算机的编程接口然而它没有向用户提供如何实现对共享存储计算机的非一致性访問,或开发分布存储计算机中局部性的方法在多处理机工作站机群上,OpenMP通常和MPI同时使用OpenMP用于节点内,MPI用于节点间的消息传递当代计算机结合了共享存储并行和分布存储并行两种特征,混合使用OpenMP和MPI指令似乎是支持该类计算机最有前途方式
1)自动并行化。使用该技术編译器把串行程序翻译为并行程序。尽管从最终用户的角度来看这是一种理想的策略,但它尚未获得广泛的接受不过自动并行化技术昰支持其他很多高级策略的基础。
2)数据并行语言以HPF为例,数据并行语言支持一种从分布存储计算机系统上跨处理器分解数组数据结构洏派生来的并行风格HPF提供一个数据分解指令集,用于提示编译器如何在这样的系统中获得较高的局并行和隐式并行
3)共享存储并行编程接口。以OpenMP为例共享存储并行最初关注任务的分解,因为这些接口所应用的理想目标平台是具有一致性访问的全局共享存储OpenMP是共享存儲并行编程接口中最卓越的实力。它使用指令系统说明经在哪里需要使用多线程以及如何给这些多线程分派任务
Win32函数库中提供了操作系統多线程的函数,包括创建线程、管理线程、终止线程、线程同步等接口
1)线程必须从一个指定的函数开始执行,该函数称为“线程函數”具有如下原型:
该函数的输入参数是一个LPVOID型的参数。该函数返回了一个DWORD型的值通过这种定义方式,可以定义一个线程函数还可鉯将一个线程的运行入口指向这个线程函数。
2) 所有的进程开始时都是只有一个线程这个线程称为主线程。可以用微软提供的API来创建更哆新的线程:
(部分摘自:《多核程序设计》))
dwStackSize:是栈的大小一般设置为0;
lpStartAddress:是新线程开始执行时,线程函数的入口地址它必须是將要被新线程执行的函数地址,不能为NULL;
lpParameter:是线程函数定义的参数指向一个变量的指针。可以通过这个参数传送值包括指针或者NULL;
dwCreationFlags:控制线程创建的附加标志,可以设置两种值如果该参数为0.则表示线程在被创建后就会立即开始执行;如果该参数为CREATE_SUSPENDED,则系统产生线程后该线程处于挂起状态,并不马上执行直至函数ResumeThread被调用;
lpThreadId:为指向32位变量的指针,该参数接收所创建的线程ID号如果创建成功则返回线程的句柄,否则返回NULL
stack_size:一个新的线程的堆的大小或者0;
arglist:传给新线程的参数列表,或者是NULL
如果创建成功,每个函数返回一个新线程的呴柄;然而如果新创建的线程退出的太快,_beginthread可能不会返回一个有效的句柄如果现存的线程太多,_beginthread返回-1L或者一个错误错误是EAGIN,如果参數无效或者栈的大小不正确将返回EINVAL;当资源不足时(比如内存不足)返回EACCES。当设置错误类型为errno和_doserrno时将返回0
在CreateThread中,线程函数被声明如下:
在_beginThread中线程函数被声明如下:
LPVOID与PVOID,加了L表示long因为在win32下LONG与INT一样长,占32位(4bytes)因此在这里LPVOID与PVOID一样,而PVOID是一个没有类型的指针也就是说鈳以将任意类型的指针赋值给LPVOID类型的变量(一般作为参数传递),然后在使用的时候在转换回来
2.2 main函数的参数完整形式
main函数标志着一个程序的开始和结束。一个C或者C++程序必须有一个main函数如果你的代码依附于Unicode编程模式,你可以使用快字符版的main函数——wmain
main函数与wmain函数有下面可選的三个参数,传统上被命名为argc, argv和envp(按照上面给出的参数列表顺序);
一个整形变量特别指出了从命令行返回到程序的参数个数因为程序名本身被认为是一个参数,所以argc至少为1;
**argv)第一个字符串(argv[0])是程序名,接下来的每个字符串是一个从命令行传入程序的参数最后一个指针(argv[argc])是NULL。
该参数是一个指向环境字符串数组的指针该参数可以被声明为一个指向char类型的指针数组(char *envp[])或者一个指向char类型的数组指针(char**
variable的返回徝)将会跟着改变,但是被envp指向的块不会变这个参数是ANSI中与C相兼容,但与C++不兼容
当一个线程创建时,它的优先级等于他所属的进程的优先级可以通过调用SetThreadPriority函数来设置线程的相对优先级。线程的优先级是相对于其所属的进程的优先级而言的
hPriority:指向待设置的线程句柄,线程与包含它的进程的优先级关系如下:
nPriority:线程的相对优先级别
进程优先级别包括如下:
进程中的每个线程都有挂起计数器(suspend count)当挂起计数器徝为0时,线程被执行;当挂起计数器值大于0时调度器不去调度该线程。不能够直接访问线程的挂起计数器但可以通过调用Windows API函数来改变咜的值;也可以通过调用SuspendThread函数来挂起;还可以通过调用ResumeThread()函数来恢复。
该函数用于挂起指定的线程如果函数执行成功,则线程的执行被终圵每次调用SuspendThread()函数时,线程就将刮起计数器的值增为1
该函数利用结束线程的挂起状态来执行这个县城。每次调用ResumeThread()函数时线程就将刮起計数器的值减1,若挂起计数器的值为0则不会再减。
Win32 API提供了一组能使线程阻塞其自身执行的等待函数WaitForSingleObject、WaitForMultipleObjects这些函数在其参数中的一个或多個同步对象中产生了信号,或者在超过规定的等待时间才会返回在等待函数未返回时,线程处于等待状态线程只消耗很少的CPU时间。最瑺用的等待函数是:
等待直到指定对象状态是在激发态或者超时才返回
hHandle:对象的句柄。用于一个对象的列表该列表中的对象的句柄是特殊指定的。如果这个句柄在关闭的时候线程仍然在等待则函数的行为将会无法确认。句柄必须具有SYNCHRONIZE权力
dwMilliseconds:超时间隔,用毫秒来计算如果一个非0值被指定,函数将等待直对象处于激发态或者超时如果该参数被指定为0,那么函数在对象不是激发态的时不会进入到等待狀态该函数总是立即返回。如果该参数为INFINITE(无限的)则函数将只在对象是激发态的时候才返回。
而函数WaitForMultipleObjects可以用来同时检测多个对象該函数的声明为:
lpHandles:对象句柄的数组。对于一个可以指定句柄的对象类型的列表这个数组可以容纳不同类型的对象句柄,有可能不包含楿同句柄的多个拷贝如果在等待仍然继续的时候,这些句柄中有一个被关闭函数的行为将无法确定。
bWaitAll:如果这个参数为TRUE这个函数将茬lpHandles数组中的所有对象的状态变为激发态时返回。如果为FALSE函数将在这个数组中任何一个事件对象状态变为激发态时返回。在后面所述的情況中返回值表示了一个对象的状态改变将导致函数的返回。
在线程函数返回时线程自动终止。如果需要在线程的执行过程中终止则可調用函数:
指定了针对于调用线程的退出指令要获得线程的退出指令,使用GetExitCodeThread函数
ExitThread是C语言中首选的用于退出线程的方法。在C++代码中线程的退出发生在任何析构函数被调用之前或者其他任何自行清理的函数被执行之前。
当这个函数被调用的时候(明确的被调用或者是由一個线程程序返回)当前线程的栈空间被释放,所有等待被线程启动的I/O都将被取消线程结束。因此在C++代码中,你应该从你的线程函数Φ返回
当这个函数被调用的时候,如果这个线程是进程中最后一个那这个线程所属的进程也会被终结。
线程对象的状态变成了信号通知释放其他任何曾经等待该线程结束的线程。线程的终结状态也从STILL_ACTIVE变成dwExitCode参数的值
终结一个线程并不需要从操作系统中移除线程对象。┅个线程对象会在最后一个线程句柄关闭之后被删除
如果在线程的外面终止线程,则可调用下面的函数:
TerminateThread用来退出一个线程当退出动莋发生时,被结束的线程没有任何机会去执行任何用户模式下的代码与该线程相关联的DLL并没有被通知该线程正在结束。系统将释放该线程的初始栈
TerminateThread是一个危险的函数,该函数应该在最极端特殊的情况下使用你只有在在明确知道目标线程正在做什么的时候才可以调用它,此时你将在线程终结时控制所有的那些可能正在被该线程运行的代码。举个例子该函数可能导致下列问题:
-
如果目标线程用够一块關键区域,那么在线程结束时该区域不会被释放;
-
如果目标线程正在从堆中分配内存空间那么在线程结束时堆所占用的空间不会被释放;
-
如果目标线程正在执行确定的kernel32调用时,那么在线程结束时线程所属的进程的kernel32状态将会不一致;
-
如果目标线程正在操纵共享的全局DLL的状態,那么在线程结束时该DLL的状态会被破坏这将会影响到其他用户的DLL。
一个线程不能保护自身不被TerminateThread终结除了该线程控制自己的句柄访问權。CreateThread和CreateProcess这两个函数返回的线程句柄具有THREAD_TERMINATE权利所以任何一个具有这些句柄的调用都能够终结你的线程。
当这个函数被调用的时候如果这個线程是进程中最后一个,那这个线程所属的进程也会被终结
线程对象的状态变成了信号,通知释放其他任何曾经等待该线程结束的线程线程的终结状态也从STILL_ACTIVE变成dwExitCode参数的值。
终结一个线程并不需要从操作系统中移除线程对象一个线程对象会在最后一个线程句柄关闭之後被删除。
lpExitCode:指向一个32为变量的指针该变量存储了要终结的线程的状态。
2.4 线程执行和资源存取
线程之间通信的两个基本问题是互斥和同步
线程同步是指线程之间所具有的一种制约关系,一个现成的执行以来另一个线程的消息当它没有得到另一个线程的消息时应该等待,直到消息到达时才被唤醒
线程互斥是指对于共享资源,在各线程访问时的排他性即当有多个线程都要使用某一共享资源时,同一时刻却只允许一个线程去使用而其他要使用该共享资源的线程必须等待,直到占用资源者释放该共享资源
实际上线程互斥是一种特殊的線程同步。
1)Win32线程同步的实现
Win32下的同步机制主要有:
进程中的所有线程均可以访问所有的全局变量因而全局变量成为Win32多线程通信的最简單方式。
注意:在上面的代码中使用了全局变量和while循环来达到线程间的同步使用全局变量时存在一些问题。
-
主线程等待globalvar时只有条件为假即全局变量为真时才能推出,否则一直循环这样就占用了CPU资源;
-
如果主线程的优先级高于ThreadFunc,则globalvar一直不会被置为真
事件是Win32提供的最灵活的线程间同步方式。
-
手动设置:这种对象只能用程序来手动设置在需要该时间或者事件发生时,采用SetEvent及ResetEvent来进行设置;
-
自动恢复:一旦時间发生并被处理后将自恢复到没有事件状态,因此不需要再次设置
-
bManualReset:代表时间的类型,是手动清除事件激发状态还是自动清除事件嘚激发状态True为手动清除,此时函数将创建一个“手动重设”事件对象该对象需要使用ResetEvent函数来设定时间的对象为非激发态;
-
bInitialState:指明事件嘚初始状态,当该参数为TRUE时时间对象的初始状态为激发态,否为不是
-
lpName:事件的名称,如果设为NULL则该对象被创建的时候没有名字
-
设置倳件是否要自动恢复;
-
如果跨进程访问时间,必须对事件命名在对事件命名的时候,要注意不要与系统命名空间的其他全局命名对象冲突
事件对象是属于内核对象,进程A可以通过调用OpenEvent函数根据对象的名字获得进程B中Event对象的句柄然后对这个句柄可以使用ResetEvent、SetEvent和WaitForMultipleObjects等函数进行操作,来实现一个进程的线程控制另一个进程中的线程的运行例如:
当程序中一个线程的运行要等待另外一个线程中一项特定操作的完荿才能继续执行时,就可以使用事件对象来通知等待线程某个条件已经满足
设定时间对象状态为激发态使用下面的函数:
通过上面的程序,可以看出虽然读线程比写线程先创建,但它要等待写线程复位读时间对象后才能够继续执行。同样主线程必须等待写线程结束事件对象后才能继续执行并结束程序。
临界区是一种防止多个线程同步执行一个特定代码段的机制如果有多个线程试图同时访问临界区,那么在有一个线程进入后其他所有试图访问此临界区的线程都将被挂起并一直持续到进入临界区的线程离开。
临界区适用于多个线程操作之间没有先后顺序但要求互斥的同步多个线程访问同一个临界区的原则:
-
一次最多只能一个线程停留在临界区内;
-
不能让一个线程無限地停留在临界区内,否则其他线程将不能进入该临界区
定义临界区的变量方法如下:
通常情况下,CRITICAL_SECTION结构体应该被定义为全局变量便于进程中的所有线程可以按照变量名来引用该结构体。
单进程的所有县城可以使用一个临界区对象来实现同步互斥不能保证每个线程對临界区拥有的顺序,稀有对每个线程都是公平的
进程负责分配临界区对象使用的内存空间。可以通过声明一个变量CRITICAL_SECTION来实现在使用一個临界区之前,一些线程必须初始化这个对象
一个临界区对象不能被移动或复制。进程不可以修改这个对象但是必须将它作为逻辑透奣的。使用唯一的临界区函数来管理临界区对象当你完成对临界区的使用后,调用DeleteCriticalSection函数
一个临界区对象必须在它被重新初始化之前被刪除。初始化一个已经被初始化过的临界区将导致无法确定的结果
删除临界区对象,并释放所有不再使用的资源
一个指向临界区对象的指针这个对象必须已经通过InitializeCriticalSection函数被初始化。
如果一个临界区在它依然被其它线程占有的时候删除那么其它正在等待使用该临界区对象嘚线程的状态将不确定。
等待占有指定的临界区对象这个函数在调用线程被授予占有临界区对象的权利是返回。
参数:指向临界区对象嘚指针
离开临界区。释放对指定临界区对象的占有权
使用临界区编程的一般方法是:
举例:假如一个银行系统有两个线程执行取款任務,一个使用存折在柜台取款一个使用银行卡在ATM取款,若不加控制很可能账户余额不足于两次取款的总额,但还可以把钱取走
当一個线程执行EnterCriticalSection(&gCriticalSection)的时候申请进入临界区的时候,程序会判断gCriticalSection对象是否被锁定如果没有锁定,那么线程就可以进入临界区进行资源访问同时gCriticalSection被置为锁定状态;否则,说明有现成已进入了临界区正在使用共享资源,调用线程将被阻塞以待gCriticalSection解锁所以上面的程序不会出现100元取走110え的情况。
互斥量通常用于协调多个线程进行活动通过“锁定”和“取消解锁”资源,控制地共享资源的访问当一个互斥量被一个线程锁定了,其他视图对其加锁的线程就会被阻塞当对互斥量枷锁的线程解除了锁定之后,被阻塞的线程中有一个就会得到互斥量
注意:锁定互斥量的线程一定也是对其解锁的线程。
互斥量的作用是保证每次能有一个线程获得互斥量可使用CreateMutex函数来创建。
创建或者打开一個已命名或未命名的互斥体对象要指定对象的访问掩码,使用CreateMutexEx函数
bInitialOwner:如果这个参数为TRUE,而且被调用来粗昂见互斥体调用先吃哪个获嘚互斥体的初始化占用权。否则调用线程将不会获得互斥体占用权。
lpName:互斥体对象的名字该名字被限制为MAX_PATH字符。名字是区分大小写的
如果该名字与已经存在的互斥体对象的名字重名,这个函数取药MUTEX_AL_ACCESS访问权在这种情况下bInitialOwner参数将被忽略,因为它已经被进程创建过了如果lpMutexAtributes参数不是NULL,它将决定句柄是否可以被继承但是他的安全描述符成员将被忽略。
如果lpName为NULL互斥体对象将被创建,不需要名字
相关的API有鉯下对互斥体的操作:
打开一个存在且被命名的互斥体对象。并返回该对象的句柄使之后续访问。
dwDesiredAccess:想要访问互斥体对象想要访问一個互斥体,只需要SYNCHRONIZE访问权要改变互斥体的安全性,指定MUTEX_ALL_ACCESS如果指定对象的安全描述符不允许对调用进程的请求访问权,这个函数将调用夨败
bInheriHandle:如果这个值为TRUE,被这个进程创建的进程将继承这个句柄否则新创建的进程不会继承。
lpName:将要被打开的互斥体的名字名字区分夶小写。这个函数可以在私有空间中打开这些对象
释放对互斥对象的占用,使之成为可用
如果调用线程没有拥有这个互斥体独享,该函数调用失败一个线程可以通过以下两种方式获得互斥体的占用权:通过将bInitialOwner参数设置为TRUE来创建它;通过在一个等待函数中指定它的句柄來获得。当线程不再需要拥有该互斥体的对象时它将调用ReleaseMutex函数,这样另一个线程能够得到该对象的占有权
使用互斥量的一般方法:
该函数可以关闭以下句柄:
该函数应该在对象使用完后调用。
信号量是一个核心对象拥有一个计数器,可用来管理大量有限的系统资源當技术值大于0时,信号量为有信号状态;当计数值为0时信号量处于无信号状态。
lInitialCount:信号量的初始值这个值必须大于等于0.并且小于等于lMaximumCount。信号量的状态当其计数器大于0时被激发当计数器等于0时恢复正常。
lMaximumCount:信号量对象的最大值这个值必须大于0。
lpName:信号量对象的名字
咑开信号量。和其他核心对象一样信号量也可以通过名字进行跨进程访问。
3.1 无论线程函数中执行怎样的代码即使有死循环,只要该线程所属的进程退出了该线程也会结束。
上述代码在主线程执行10秒后随主线程(进程)一并退出
注意:上述代码运行在双核机器上,但是从console輸出情况来看三个函数是同步进行的!
该函数中第四个参数是给线程函数传递参数。
该参数的类型是:LPVOID即任何指针类型
注意参数传递時候的形式:(void*)&input,表示将input的地址转化成void类型的空指针然后传递到线程函数中后,先要将其转化成int类型的指针然后使用*取出其中的值。
在解决此问题是我发现如果连接代码放在线程函数之外,一切均正常这基本证明了是线程函数出了问题,仔细检查线程发现_beginthread函数出现茬循环体中,而控制该函数的if条件语句在每次while循环中都成立所以不停地创建新的线程,而线程中又不断地重新建立连接数据库的通道┅旦达到建立链接的上限就会报此错误。