第15章 多线程
程序与进程的关系:程序是计算机指令的集合,它以文件的形式存储在磁盘上,而进程通常被定义为一个正在运行的程序的实例,是一个程序在其自身的地址空间中的一次执行活动。一个程序可以对应多个进程,同时,在一个进程中也可以同时访问多个程序。
进程是资源申请、调度和独立运行的单位,因此它使用系统中的运行资源。程序不能申请系统资源,不能被系统调度,也不能作为独立运行的单位,因此它不占用系统的运行资源。
进程与线程的关系:进程从来不执行任何东西,它只是线程的容器。若要使进程完成某项操作,它必须拥有一个在它的环境中运行的线程,此线程负责执行包含在进程的地址空间中的代码。也就是说,进程实际上是线程的执行环境。
单个进程可能包含若干个线程,这些线程都“同时”(时间片)执行进程地址空间中的代码。每个进程至少拥有一个线程。当创建一个进程时,操作系统会自动创建这个进程的第一个线程,称为主线程,也就是执行主函数的线程。此后主线程可以创建其他线程。
线程只有一个内核对象和一个栈,保留的记录很少,因此所需要的内存也很少。此外当进程间切换时,需要交换整个地址空间,而线程之间的切换只是执行环境的改变,因此效率比较高,因此在编程中经常采用多线程来解决编程问题,而尽量避免创建新的进程。
进程地址空间:系统赋予每个进程独立的虚拟地址空间,对于32位进程来说,这个地址空间是4GB。其中2GB是内核方式分区,供内核代码、设备驱动程序、设备IO高速缓存、非页面内存池的分配和进程页面表等使用,而用户方式分区使用的地址空间约为2GB,这个分区是进程的私有地址空间所在的地方。
互斥对象:一般来说,对多线程程序,如果这些线程需要访问共享资源,就需要进行线程间的同步处理。互斥对象属于内核对象,它能够确保线程拥有对单个资源的互斥访问权。互斥对象包含一个使用数量,一个线程ID和一个计数器。其中ID用于标识系统中哪个线程当前拥有互斥对象,计数器用于指明该线程拥有互斥对象的次数。使用CreateMutex和ReleaseMutex函数来创建和释放互斥对象。线程必须主动请求共享对象的使用权才有可能获得该所有权,这可以通过WaitForSingleObject函数来实现,在调用WaitForSingleObject函数后,该函数会一直等待,这样就会暂停线程的执行,只有在两种情况下才会返回:
1)指定的对象变成有信号状态;
2)指定的等待时间间隔已过;
可以通过其返回值判断引起函数返回的事件。
同一个线程多次拥有互斥对象的情况:操作系统通过互斥对象内部的计数器来维护同一个线程请求到该互斥对象的次数,当调用WaitForSingleObject请求互斥对象时,操作系统需要判断当前请求互斥对象的线程的ID是否与互斥对象当前拥有者的线程ID相等,如果相等,即使该互斥对象处于未通知状态,调用线程任然能够获得其所有权。操作系统通过互斥对象内部的计数器来维护同一个线程请求到该互斥对象的次数,如果多次在同一个线程中请求同一个互斥对象,则需要相应的多次调用ReleaseMutex函数释放该互斥对象。
代码分析:模拟售票系统
#include <windows.h>
#include <iostream.h>
//声明线程入口函数原型,线程入口函数名称可变,但函数类型必须遵照以下形式
DWORD WINAPI Fun1Proc( LPVOID lpParameter );
DWORD WINAPI Fun2Proc( LPVOID lpParameter );
int tickets = 100; //要销售的票数还剩下100张,这100张票由Fun1Proc与Fun2Proc两个线程负责销售
HANDLE hMutex;//互斥对象句柄;typedef void *HANDLE;
void main()
{
HANDLE hThread1,hThread2;
hThread1 = CreateThread(
NULL, //使用缺省的安全性
0, //指定初始提交栈的大小
Fun1Proc,//指定线程入口函数地址
NULL, //传递给线程的参数
0, //附加标记,0表示线程创建后立即运行
NULL); //线程ID,在Win98/95中不能设置为NULL
CloseHandle(hThread1);
// 调用CloseHandle函数并没有终止新创建的线程,只是表示在主线程中对新创建的线程的引用不感兴趣,因此将它关闭;当不再需要线程句柄时,应将其关闭,以便系统及时释放资源;
hThread2=CreateThread(NULL,0,Fun2Proc,NULL,0,NULL);
CloseHandle(hThread2);
hMutex = CreateMutex(NULL, TRUE,"MuName");
//创建一个命名的互斥对象;第二个参数为真,指定了创建这个对象的线程,即主线程获得该对象的所有权,如果主线程不释放该互斥对象,则该对象将处于无信号状态
if(hMutex)
{//判断是不是第一次创建的互斥对象
if(ERROR_ALREADY_EXISTS == GetLastError())
{//如果为真,说明互斥对象已经创建
cout<<"已经有一个实例在运行了,只能有一个实例运行同时运行。"<<endl;
return;
}
}
WaitForSingleObject(hMutex,INFINITE);
//因为请求的互斥对象线程ID与拥有互斥对象线程ID相同,
//可以再次请求成功,计数器加1
ReleaseMutex(hMutex); //第一次释放,计数器减1,但仍有信号
ReleaseMutex(hMutex); //再一次释放,计数器为零
while(tickets>0) { Sleep(4000);}
}
/*------------实现线程入口函数Fun1Proc---------------*/
DWORD WINAPI Fun1Proc( LPVOID lpParameter )
{
while(TRUE)
{
WaitForSingleObject(hMutex,INFINITE); //请求互斥对象
if(tickets>0)
{Sleep(1);
cout<<"thread 1 sell ticket: ";
}
else
break;
ReleaseMutex(hMutex);
}
return 0;
} ///endof Fun1Proc()
/*-----------实现线程入口函数Fun2Proc--------------*/
DWORD WINAPI Fun2Proc( LPVOID lpParameter )
{
while(TRUE)
{
WaitForSingleObject(hMutex,INFINITE);
if(tickets>0)
{Sleep(1);
cout<<"thread 2 sell ticket: ";
}
else
break;
ReleaseMutex(hMutex);
}
return 0;
}
如果某个线程得到其所需的互斥对象的所有权,完成其代码的运行,但是没有释放互斥对象的所有权就退出之后,操作系统一旦发现该线程已终止,就会将线程所拥有的互斥对象的ID设为0,并将其计数归零。