一、 多线程概述
进程和线程都是操作系统的概念。进程是应用程序的执行实例,每个进程是由私有的虚拟地址空间、代码、数据和其它各种系统资源组成,进程在运行过程中创建的资源随着进程的终止而被销毁,所使用的系统资源在进程终止时被释放或关闭。
线程是进程内部的一个执行单元。系统创建好进程后,实际上就启动执行了该进程的主执行线程,主执行线程以函数地址形式,比如说main或WinMain函数,将程序的启动点提供给Windows系统。主执行线程终止了,进程也就随之终止。
每一个进程至少有一个主执行线程,它无需由用户去主动创建,是由系统自动创建的。用户根据需要在应用程序中创建其它线程,多个线程并发地运行于同一个进程中。每个线程具有自己的堆栈和自己的 CPU 寄存器副本。其他资源(如文件、静态数据和堆内存)由进程中的所有线程共享。所以线程间的通讯非常方便,多线程技术的应用也较为广泛。但是使用这些公共资源的线程必须同步。Win32 提供了几种同步资源的方式,包括信号、临界区、事件和互斥体。
每个进程都有私有的虚拟地址空间,进程的所有线程共享同一地址空间。每个线程被CPU分配一个时间片,一旦被激活,它正常运行直到时间片耗尽并被挂起,此时,操作系统选择另一个线程进行运行。通过时间片轮转,又出于各个时间片很小(20毫秒级),看起来就像多个线程同时在工作。实际上,只有在多处理器系统上才是真正的在可得到的处理器上同时运行多个线程。基于Win32的应用程序可以通过把给定进程分解(或创建)多个线程挖掘潜在的CPU时间,而且还可以加强应用程序,以使用户提高效率,加强反应能力以及进行后台辅助处理。
在Windows操作系统中,Win32应用程序可以在Windows平台上运行多个实例,每个应用程序实例都是一个独立的进程,而一个进程可以由不止一个线程来实现。对于一个进程来说,当应用程序有几个任务要同时运行时,建立多个线程是有用的。如打印时,利用多线程机制实现多线程,就可在需要打印时创建一个负责完成打印功能的打印线程。创建打印线程之后,系统就变成了多线程。当进行打印时,CPU轮换着分配给这两个线程时间片,所以打印和其他功能一起同时在运行,这就充分利用了CPU处理打印工作之外的空闲时间片,并且避免了用户长久地等待打印时间。这就是所谓的由多线程来实现的多任务,在进行打印任务的同时又可以进行别的任务。要说明的一点是,目前大多数的计算机都是单处理器(CPU)的,为了运行所有这些线程,操作系统为每个独立线程安排一些CPU时间,操作系统以轮换方式向线程提供时间片,这就给人一种假象,好象这些线程都在同时运行。由此可见,如果两个非常活跃的线程为了抢夺对CPU的控制权,在线程切换时会消耗很多的CPU资源,反而会降低系统的性能。这一点在多线程编程时应该注意。
Win32 SDK函数支持进行多线程的程序设计,并提供了操作系统原理中的各种同步、互斥和临界区等操作。Visual C++ 6.0中,使用MFC类库也实现了多线程的程序设计,线程被分为工作者线程(Worker Thread)和用户界面线程(User Interface Thread)两大类。前者常用于处理后台任务,执行这些后台任务并不会耽搁用户对应用程序的使用,即用户操作无需等待后台任务的完成。后者常用来独立的处理用户输入和相应用户的事件。其中用户界面线程的特点是拥有单独的消息队列,可以具有自己的窗口界面,能够对用户输入和事件做出响应。在应用程序中,根据用户界面线程具有消息队列这一特点,可以使之循环等待某一事件发生后再进行处理。由于Windows95时抢先式多任务的操作系统,即使一个线程因等待某事件而阻塞,其他线程仍然可以继续执行。
二、Win32 API对多线程编程的支持
Win32 提供了一系列的API函数来完成线程的创建、挂起、恢复、终结以及通信等工作。下面将选取其中的一些重要函数进行说明。
1、HANDLE CreateThread(LPSECURITY_ATTRIBUTES lpThreadAttributes,
DWORD dwStackSize,
LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter,
DWORD dwCreationFlags,
LPDWORD lpThreadId);
该函数在其调用进程的进程空间里创建一个新的线程,并返回已建线程的句柄,其中各参数说明如下:
* lpThreadAttributes:指向一个 SECURITY_ATTRIBUTES 结构的指针,该结构决定了线程的安全属性,一般置为 NULL;
* dwStackSize:指定了线程的堆栈深度,一般都设置为0;
* lpStartAddress:表示新线程开始执行时代码所在函数的地址,即线程的起始地址。一般情况为(LPTHREAD_START_ROUTINE)ThreadFunc,ThreadFunc 是线程函数名;
* lpParameter:指定了线程执行时传送给线程的32位参数,即线程函数的参数;
* dwCreationFlags:控制线程创建的附加标志,可以取两种值。如果该参数为0,线程在被创建后就会立即开始执行;如果该参数为CREATE_SUSPENDED,则系统产生线程后,该线程处于挂起状态,并不马上执行,直至函数ResumeThread被调用;
* lpThreadId:该参数返回所创建线程的ID;
如果创建成功则返回线程的句柄,否则返回NULL。
2、DWORD SuspendThread(HANDLE hThread);
该函数用于挂起指定的线程,如果函数执行成功,则线程的执行被终止。
3、DWORD ResumeThread(HANDLE hThread);
该函数用于结束线程的挂起状态,执行线程。
4、VOID ExitThread(DWORD dwExitCode);
该函数用于线程终结自身的执行,主要在线程的执行函数中被调用。其中参数dwExitCode用来设置线程的退出码。
5、BOOL TerminateThread(HANDLE hThread,DWORD dwExitCode);
一般情况下,线程运行结束之后,线程函数正常返回,但是应用程序可以调用TerminateThread强行终止某一线程的执行。各参数含义如下:
* hThread:将被终结的线程的句柄;
* dwExitCode:用于指定线程的退出码。
使用TerminateThread()终止某个线程的执行是不安全的,可能会引起系统不稳定;虽然该函数立即终止线程的执行,但并不释放线程所占用的资源。因此,一般不建议使用该函数。
6、BOOL PostThreadMessage(DWORD idThread,
UINT Msg,
WPARAM wParam,
LPARAM lParam);
该函数将一条消息放入到指定线程的消息队列中,并且不等到消息被该线程处理时便返回。
* idThread:将接收消息的线程的ID;
* Msg:指定用来发送的消息;
* wParam:同消息有关的字参数;
* lParam:同消息有关的长参数;
调用该函数时,如果即将接收消息的线程没有创建消息循环,则该函数执行失败。
7、DWORD WaitForSingleObject(
HANDLE hHandle, // 表示等待对象的句柄
DWORD dwMilliseconds // 等待时间,单位为毫秒
);
如果等待对象一直被占用,则该函数就一直等待,直到等待超时,函数才返回。如果等待时间设INFINITE,则该函数会一直等待下去,直到等待对象处于通知状态。函数返回值表明返回原因,其返回值有以下几种:
函数返回值 描述(前三种情况下函数执行成功)
WAIT_ABANDONED 在当前拥有内核对象的线程终止之前,内核对象的所有权没有被拥有内核对象的线程释放,但内核对象的所有权被授予了当前调用的线程,内核对象处于未通知状态
WAIT_OBJECT_0 所标识的对象有信号
WAIT_TIMEOUT 超时,所标识的对象无信号
WAIT_FAILED 函数执行失败
8、DWORD WaitForMultipleObjects(
DWORD nCount, // 句柄数组中的句柄数,最大值不能超过MAXIMUM_WAIT_OBJECTS
CONST HANDLE *lpHandles, // 内核对象句柄数组,数组中对象可以是不同类型内核对象
BOOL fWaitAll, // 等待标志
DWORD dwMilliseconds // 等待时间,单位为毫秒
);
如果等待时间超时,函数无条件立即返回;如果等待时间为0,函数在检查对象数组中各个对象状态后立即放回;如果等待时间设为INFINITE,则一直等待,直至满足条件。fWaitAll指定等待方式,如果为TRUE,则在对象数组中所有对象都处于通知状态时才返回,如果为FALSE,则只有对象数组中有一个对象处于通知状态,就返回。函数的返回值表明返回原因,其返回值有以下几种:
函数返回值 描述(前三种情况下函数执行成功)
WAIT_OBJECT_0 to (WAIT_OBJECT_0 + nCount – 1) 如果fWaitAll为TRUE,表示对象数组所有对象都处于通知状态如果fWaitAll为FALSE,返回值与WAIT_OBJECT_0的差标示处于通知状态的对象在对象数组中的索引,如果有多个对象都处于通知状态,则返回这多个对象在对象数组中索引值最小的那个对象的索引值
WAIT_ABANDONED_0 to (WAIT_ABANDONED_0 + nCount – 1) 如果fWaitAll为TRUE,表明至少有一个被抛弃的互斥对象
如果fWaitAll为FALSE,返回值与WAIT_OBJECT_0的差标示被抛弃的
互斥对象的索引值
WAIT_TIMEOUT 超时,所标识的对象无信号
WAIT_FAILED 函数执行失败
上面的7、8两个函数主要用在试图进入共享资源的线程函数入口处,主要用来判断内核对象是否允许本线程的进入。适用于该函数的同步控制对象主要有:更改通知(Change notification )、控制台输入(Console input)、事件(Event )、作业(Job)、互斥(Mutex)、进程(Process)、信号量(Semaphore)、线程(Thread)等待计时器(Waitable timer)。
三、互斥对象
互斥对象(mutex)内核对象能够确保线程拥有对单个资源的互斥访问权。
互斥对象的组成:一个使用数量、一个线程ID(用于标识系统中的哪个线程当前拥有互斥对象)、一个递归计数器(用于指明该线程拥有互斥对象的次数)
互斥对象的使用规则如下
* 如果线程ID是0(这是个无效ID),互斥对象不被任何线程所拥有,并且发出该互斥对象的通知信号。
* 如果ID是个非0数字,那么一个线程就拥有互斥对象,并且不发出该互斥对象的通知信号。
* 与所有其他内核对象不同, 互斥对象在操作系统中拥有特殊的代码,允许它们违反正常的规则(后面将要介绍这个异常情况)。
互斥对象作用
通常来说,互斥对象用于保护由多个线程访问的内存块。如果多个线程要同时访问内存块,内存块中的数据就可能遭到破坏。互斥对象能够保证访问内存块的任何线程拥有对该内存块的独占访问权,这样就能够保证数据的完整性。
1、创建互斥对象
HANDLE CreateMutex(
PSECURITY_ATTRIBUTES psa,
BOOL fInitialOwner,
PCTSTR pszName);
fInitialOwner参数用于控制互斥对象的初始状态。如果传递FALSE(这是通常情况下传递的值),那么互斥对象的I D和递归计数器均被设置为0。这意味着该互斥对象没有被任何线程所拥有,因此要发出它的通知信号。
如果为fInitialOwner参数传递TRUE,那么该对象的线程ID被设置为调用线程的ID,递归计数器被设置为1。由于ID是个非0数字,因此该互斥对象开始时不发出通知信号。
2、打开互斥对象
通过调用OpenMutex,另一个进程可以获得它自己进程与现有互斥对象相关的句柄:
HANDLE OpenMutex(
DWORD fdwAccess,
BOOL bInheritHandle,
PCTSTR pszName);
通过调用一个等待函数,并传递负责保护资源的互斥对象的句柄,线程就能够获得对共享资源的访问权。在内部,等待函数要检查线程的ID,以了解它是否是0(互斥对象发出通知信号)。如果线程ID是0,那么该线程ID被设置为调用线程的I D,递归计数器被设置为1,同时,调用线程保持可调度状态。
如果等待函数发现ID不是0(不发出互斥对象的通知信号),那么调用线程便进入等待状态。系统将记住这个情况,并且在互斥对象的ID重新设置为0时,将线程ID设置为等待线程的ID,将递归计数器设置为1,并且允许等待线程再次成为可调度线程。与所有情况一样,对互斥内核对象进行的检查和修改都是以原子操作方式进行的。
3、释放互斥对象
当目前拥有对资源的访问权的线程不再需要它的访问权时,它必须调用ReleaseMutex函数来释放该互斥对象:
BOOL ReleaseMutex(HANDLE hMutex);
该函数将对象的递归计数器递减1。如果线程多次成功地等待一个互斥对象,在互斥对象的递归计数器变成0之前,该线程必须以同样的次数调用ReleaseMutex函数。当递归计数器到达0时,该线程I D也被置为0,同时该对象变为已通知状态。
当一个线程调用ReleaseMutex函数时,该函数要查看调用线程的ID是否与互斥对象中的线程ID相匹配。如果两个ID相匹配,递归计数器就会像前面介绍的那样递减。如果两个线程的ID不匹配,那么ReleaseMutex函数将不进行任何操作,而是将FA L S E(表示失败)返回给调用者。
四、多线程实例
下面是vs2008下两个线程模拟卖火车票的小程序。
#include "stdafx.h"
#include <windows.h>
#include <iostream>
using namespace std; DWORD WINAPI ThreadFunc1(LPVOID lpParameter);//thread data
DWORD WINAPI ThreadFunc2(LPVOID lpParameter);//thread data
int index=0;
int tickets=10;
HANDLE hMutex;
int _tmain(int argc, _TCHAR* argv[])
{
HANDLE hThread1;
HANDLE hThread2; //创建线程, 并返回已建线程的句柄
hThread1 = CreateThread(NULL, 0, ThreadFunc1, NULL, 0, NULL);
hThread2 = CreateThread(NULL, 0, ThreadFunc2, NULL, 0, NULL); CloseHandle(hThread1); // 关闭句柄, 此时系统会递减新线程的线程内核对象的使用计数, 当创建的线程
CloseHandle(hThread2); // 执行完毕后, 系统也会递减线程内核对象的使用计数, 计数为0时, 系统释放线程内核对象 //创建互斥对象
LPCWSTR str = (LPCWSTR)"tickets";
hMutex = CreateMutex(NULL, TRUE, str); // TRUE表示调用CreateMutex的线程拥有了互斥对象
if (hMutex) //确定是否确实创建了一个新内核对象,而不是打开了一个现有对象
{
if (ERROR_ALREADY_EXISTS==GetLastError())
{
cout<<"only one instance can run!"<<endl;
return 0;
}
}
WaitForSingleObject(hMutex, INFINITE); // 等待互斥对象通知
ReleaseMutex(hMutex); // 申请两次则要释放两次
ReleaseMutex(hMutex); // 谁申请谁释放 Sleep(4000); // 保证卖完10张票前,主线程不能退出
return 0;
}
//线程1的入口函数
DWORD WINAPI ThreadFunc1(LPVOID lpParameter)//thread data
{
while (true) // 保证线程的不断执行
{
WaitForSingleObject(hMutex,INFINITE); // 等待互斥对象通知
if (tickets>0)
{
Sleep(1);
cout<<"thread1 sell ticket :"<< tickets-- <<endl; // 对共享对象进行操作
}
else
break;
ReleaseMutex(hMutex); // 释放互斥对象
}
return 0;
}
//线程2的入口函数
DWORD WINAPI ThreadFunc2(LPVOID lpParameter)//thread data
{
while (true) // 保证线程的不断执行
{
WaitForSingleObject(hMutex,INFINITE); // 检测hMutex对象的信号状态,当有信号时返回,无信号则一直等待下去
if (tickets>0)
{
Sleep(1); //使操作系统有时间去执行其它线程
cout<<"thread2 sell ticket :"<< tickets-- <<endl; // 对共享对象进行操作
}
else
break;
ReleaseMutex(hMutex); // 释放对互斥体对象的占用,使之成为可用
} return 0;
}