MFC实战篇——线程的创建和多线程简单示例

35 篇文章 42 订阅
12 篇文章 4 订阅

🔳🔳 绘制线条 、画刷绘图、绘制连续线条、绘制扇形效果的线条


🔳🔳 插入符【文本插入符|图形插入符】、窗口重绘、路径、字符输入【设置字体|字幕变色】


🔳🔳 菜单命令响应函数、菜单命令的路由、基本菜单操作、动态菜单操作、电话本实例


🔳🔳 对话框的创建与显示、动态创建按钮、控件的访问【控件调整|静态文本控件|编辑框控件】、对话框伸缩功能、输入焦点的传递、默认按钮的说明


🔳🔳 MFC对话框:逃跑按钮、属性表单、向导创建


🔳🔳 在对话框程序中让对话框捕获WM_KEYDOWN消息


🔳🔳修改应用程序窗口的外观【窗口光标|图标|背景】、模拟动画图标、工具栏编程、状态栏编程、进度栏编程、在状态栏上显示鼠标当前位置、启动画面


🔳🔳设置对话框、颜色对话框、字体对话框、示例对话框、改变对话框和控件的背景及文本颜色、位图显示

一、基本概念

1.1 进程

1. 程序和进程

程序是计算机指令的集合,它以文件的形式存储在磁盘上。
进程通常被定义为一个正在运行的程序的实例,是一个程序在其自身的地址空间中的一次执行活动。

我们编写的程序在编译后生成的后缀为.exe的可执行程序,是以文件的形式存储在磁盘上的,当运行这个可执行程序时,就启动了该程序的一个实例,我们把它称之为进程一个程序可以对应多个进程,例如可以同时打开多个记事本程序的进程,同时,在一个进程中也可以同时访问多个程序进程是资源申请、调度和独立运行的单位,因此,它使用系统中的运行资源;而程序不能申请系统资源,不能被系统调度,也不能作为独立运行的单位,因此,它不占用系统的运行资源

进程是资源申请、调度和独立运行的单位,因此它使用系统中的运行资源;
程序不能申请系统资源,不能被系统调度,也不能作为独立运行的单位。因此,它不占用系统的运行资源。

2. 进程组成

进程由两部分组成:
1)操作系统用来管理进程的内核对象
内核对象也是系统用来存放关于进程的统计信息的地方。内核对象是操作系统内部分配的一个内存块,该内存块是一种数据结构,其成员负责维护该对象的各种信息。由于内核对象的数据结构只能被内核访问使用,因此应用程序在内存中无法找到该数据结构,并直接改变其内容,只能通过Windows提供的一些函数来对内核对象进行操作

2)地址空间
包含所有可执行模块或者DLL模块的代码和数据。另外,它包含动态内存分配的空间,例如线程的栈和堆分配空间。
进程从来不执行任何东西,它只是线程的容器。若要使进程完成某项操作,它必须拥有一个在它的环境中运行的线程,此线程负责执行包含在进程的地址空间中的代码。也就是说,真正完成代码执行的是线程,而进程只是线程的容器,或者说是线程的执行环境

  • 单个进程可能包含若干个线程,这些线程都“同时”执行进程地址空间中的代码。
  • 每个进程至少拥有一个线程,来执行进程的地址空间中的代码。

当创建一个进程时,操作系统会自动创建这个进程的第一个线程,称为主线程,也就是执行main函数或WinMain函数的线程,可以把main函数或WinMain函数看作是主线程的进入点函数。此后,主线程可以创建其他线程。

3. 进程的组成

系统赋予每个进程独立的虚拟地址空间。对于32位进程来说,这个地址空间是4GB.因为对32位指针来说,它能寻址的范围是 2 32 2^{32} 232,即4GB。

每个进程都有它自己的私有地址空间。进程A可能有一个存放在它的地址空间中的数据结构,地址是0x12345678,而进程B则有一个完全不同的数据结构存放在它的地址空间中,地址也是0x12345678,当进程A中运行的线程访问地址为0x12345678的内存时这些线程访问的是进程A的数据结构。当进程B中运行的线程访问地址为0x12345678的内存时,这些线程访问的是进程B的数据结构。进程A中运行的线程不能访问进程B的地址空间中的数据结构,反之亦然。4GB是虚拟的地址空间,只是内存地址的一个范围。在你能成功地访问数据而不会出现非法访问之前,必须赋予物理存储器,或者将物理存储器映射到各个部分的地址空间。这里所说的物理存储器包括内存和页文件的大小,读者可以同时按下键盘上的【Ctrl+Alt-Del】键,然后在弹出的对话框上单击【任务管理器】按钮,在随后显示的Windows任务管理器对话框的右下方就可以看到当前内存的使用情况。

实际上, 4GB虚拟地址空间中, 2GB是内核方式分区,供内核代码、设备驱动程序、设备I/O高速缓冲、非页面内存池的分配和进程页面表等使用,而用户方式分区使用的地址空间约为2GB,这个分区是进程的私有地址空间所在的地方,其中还有一部分地址空间是作为NULL指针分区。一个进程不能读取、写入、或者以任何方式访问驻留在该分区中的另一个进程的数据。对于所有应用程序来说,该分区是维护进程的大部分数据的地方。

1.2 线程

1. 线程组成

线程由两部分组成:
1)线程的内核对象。
操作系统用它来对线程实施管理。内核对象也是系统用来存放线程统计信息的地方。
2)线程栈。
它用于维护线程在执行代码时需要的所有函数参数和局部变量。当创建线程时,系统创建一个线程内核对象。该线程内核对象不是线程本身,而是操作系统用来管理线程的较小的数据结构。可以将线程内核对象视为由关于线程的统计信息组成的一个小型数据结构。线程总是在某个进程环境中创建的系统从进程的地址空间中分配内存,供线程的栈使用。新线程运行的进程环境与创建线程的环境相同。因此,新线程可以访问进程的内核对象的所有句柄、进程中的所有内存和在这个相同的进程中的所有其他线程的堆栈这使得单个进程中的多个线程确实能够非常容易地互相通信。线程只有一个内核对象和一个栈,保留的记录很少,因此所需要的内存也很少。由于线程需要的开销比进程少,因此在编程中经常采用多线程来解决编程问题,而尽量避免创建新的进程。

2. 线程运行

操作系统为每一个运行线程安排一定的CPU时间——时间片。系统通过一种循环的方式为线程提供时间片,线程在自己的时间内运行,因时间片相当短,因此给用户的感觉就好像多个线程是同时运行的一样。在生活中,如果我们把一根点燃的香快速地从眼前划过,看到的将是一条线。实际上这条线是由许多点组成的,由于人眼具有视觉残留效应,因而我们的感觉好像就是一条线。如果将这根香很慢地从眼前划过,我们就能够看到一个一个"的点。同样地,因为线程执行的时间片非常短,所以在多个线程之间会频繁地发生切换,给我们的感觉好像就是这些线程在同时运行一样。如果计算机拥有多个CPU,线程就能真正意义上同时运行了。

3. 单线程程序和多线程程序

对单线程程序来说,在进程的地址空间中只有一个线程在运行。例如,一位病人去医院看病,需要动手术,医院为他安排了一位医生为他动手术,那么这位医生就是主线程,由这个主线程完成动手术这一任务。

多线程程序,在进程地址空间中有多个线程,其中有一个是主线程。例如在上述例子中,医院为了保证这位病人的手术能够成功,为医生配备了几位护士,医生作为主线程,护士作为所创建的线程,由这多个线程同时完成为病人动手术这一任务。医生负责主刀,一位护士负责为医生递送动手术用的器具,一位护士负责为医生擦汗,他们共同完成为病人动手术这一任务,当然,效率就比较高,病人生存的希望也就比较大了。这就是多线程程序的好处。

在单CPU的条件下,某一时刻只能有一个线程在运行

在单CPU的条件下,某一时刻只能有一个线程在运行,为什么还要编写多线程程序呢?读者应注意,我们所编写的多线程程序,每一个线程可以独立地完成一个任务。当该程序移植到多CPU的平台上时,其中的多个线程就可以真正并发地同时运行了


那我们是否可以用多进程程序来取代多线程程序呢?当然这也是可以的,但是还是应该尽量采用多线程程序。
有两个原因:一是对进程的创建来说,系统要为进程分配私有的4GB的虚拟地址空间,当然它所占用的资源就比较多,而对多线程程序来说,多个线程是共享同一个进程的地址空间,所以占用的资源较少。另一个理由是当进程间切换时,需要交换整个地址空间,而线程之间的切换只是执行环境的改变,因此效率比较高。

二、线程创建函数

创建线程函数可以使用系统提供的API函数:CreateThread完成,该函数将创建一个线程。

2.1 声明

WINBASEAPI
_Ret_maybenull_
HANDLE
WINAPI
CreateThread(
    _In_opt_ LPSECURITY_ATTRIBUTES lpThreadAttributes,
    _In_ SIZE_T dwStackSize,
    _In_ LPTHREAD_START_ROUTINE lpStartAddress,
    _In_opt_ __drv_aliasesMem LPVOID lpParameter,
    _In_ DWORD dwCreationFlags,
    _Out_opt_ LPDWORD lpThreadId
    );

2.2 参数

  • lpThreadAttributes
    指向SECURITY_ATTRIBUTES结构体的指针,这里可以为其传递NULL,让该线程使用默认的安全性。但是,如果希望所有的子进程能够继承该线程对象的句柄,就必须设定一个SECURITY_ATTRIBUTES结构体,将它的bInheritHandle成员初始化为TRUE.

  • dwStackSize
    设置线程初始栈的大小,即线程可以将多少地址空间用于它自己的栈,以字节为单位。系统会把这个参数值四舍五入为最接近的页面大小。页面是系统管理内存时使用的内存单位,不同CPU其页面大小不同, x86使用的页面大小是4KB。当保留地址空间的一块区域时,系统要确保该区域的大小是系统页面大小的倍数。例如,希望保留10KB的地址空间区域,系统会自动对这个请求进行四舍五入,使保留的区域大小是页面大小的倍数,在x86平台下,系统将保留一块12KB的区域,即4KB的整数倍。

    如果这个值为0,或者小于默认的提交大小,那么默认将使用与调用该函数的线程相同的栈空间大小。

  • IpStartAddress
    指向应用程序定义的LPTHREAD_START_ROUTINE类型的函数的指针,这个函数将由新线程执行,表明新线程的起始地址。我们知道main函数是主线程的入口函数,同样地,新创建的线程也需要有一个入口函数,这个函数的地址就由此参数指定。这就要求在程序中定义一个函数作为新线程的入口函数,该函数的名称任意,但函数类型必须遵照下述声明形式:
    DWORD WINAPI ThreadProc(LPVOID lpParameter);
    即新线程入口函数有一个LPOVID类型的参数,并且返回值是DWORD类型。许多初学者不知道这个函数名称: ThreadProc能够改变。实际上,在调用CreateThread创建新线程时,我们只需要传递线程函数的入口地址,而线程函数的名称是无所谓的。

  • IpParameter
    对于main函数来说,可以接受命令行参数。同样,我们可以通过这个参数给创建的新线程传递参数。该参数提供了一种将初始化值传递给线程函数的手段。这个参数的值既可以是一个数值,也可以是一个指向其他信息的指针

  • dwCreationFlags
    设置用于控制线程创建的附加标记
    它可以是两个值中的一个: CREATE_SUSPENDED0,如果该值是CREATE_SUSPENDED,那么线程创建后处于暂停状态,直到程序调用了ResumeThread函数为止;如果该值是0,那么线程在创建之后就立即运行

  • IpThreadId
    这个参数是一个返回值,它指向一个变量,用来接收线程ID,当创建一个线程时,系统会为该线程分配一个ID

    注意:在Windows 2000和Windows NT4下,可以为此参数传递NULL,表明对线程的ID不感兴趣,从而不会返回线程的标识符。
    但在Windows 95或98下,此参数值不能为NULL,必须指定一个变量来接收线程ID,笔者使用的操作系统是Windows 2000,因此此参数值可以为NULL.

三、简单多线程实例

下面编写一个多线程程序。

a. 多线程实例1
#include<iostream>
#include <windows.h>

using namespace std;

DWORD WINAPI Fun1Proc(LPVOID);
void main() {
	HANDLE hThread1;
	hThread1 = CreateThread(NULL, 0, Fun1Proc, NULL, 0, NULL);
	CloseHandle(hThread1);
	cout << "the main thread is running......" << endl;

}
DWORD WINAPI Fun1Proc(LPVOID lp) {
	cout << "1:The Thread 1 is runing......" <<endl;
	cout << "2:The Thread 1 is runing......" << endl;
	cout << "3:The Thread 1 is runing......" << endl;
	cout << "4:The Thread 1 is runing......" << endl;
	cout << "5:The Thread 1 is runing......" << endl;
	cout << "6:The Thread 1 is runing......" << endl;
	cout << "7:The Thread 1 is runing......" << endl;
	cout << "8:The Thread 1 is runing......" << endl;
	cout << "9:The Thread 1 is runing......" << endl;
	cout << "10:The Thread 1 is runing......" << endl;
	cout << "11:The Thread 1 is runing......" << endl;
	cout << "12:The Thread 1 is runing......" << endl;
	cout << "13:The Thread 1 is runing......" << endl;
	cout << "14:The Thread 1 is runing......" << endl;
	cout << "15:The Thread 1 is runing......" << endl;

	return 0;
}

上面的代码创建了一个简单的多线程程序,主要有以下几个部分组成:
1)包含必要的头文件
程序需要访问Windows API函数,因此需要包含Window.h文件,此外还需要C++标准输出函数,所以需要iostream.h。
2)线程函数
线程函数的名字起名为Fun1Proc,功能就是输出几句话,然后退出。
3)main函数
当程序启动运行后,就会产生主线程, man函数就是主线程的入口函数。在这个主线程中可以创建新的线程。

在上述main函数中首先调用CreateThread函数创建一个新线程(下面将这个新线程称为线程1),该函数的:

  • 第一个参数设置为NULL,让新线程使用默认的安全性;
  • 第二个参数设置为0,让新线程采用与调用线程一样的栈大小;
  • 第三个参数指定线程1入口函数的地址;
  • 第四个参数是传递给线程1的参数,这里不需要使用这个参数,所以将其设置为NULL:
  • 第五个参数,即线程创建标记,设置为0,让线程一旦创建就立即运行;
  • 第六个参数,即新线程的ID,因为这里不需要使用该ID,所以将其设置为NULL.

在创建线程完成之后,调用CloseHandle函数关闭新线程的句柄。这里,读者可能就会产生这样的疑问:刚刚创建了线程,为什么又将它关闭了呢?读者应注意,实际上调用CloseHandle函数并没有中止新创建的线程,只是表示在主线程中对新创建的线程的引用不感兴趣,因此将它关闭。另一方面,当关闭该句柄时,系统会递减该线程内核对象的使用计数。当创建的这个新线程执行完毕之后,系统也会递减该线程内核对象的使用计数。当使用计数为0时,系统就会释放该线程内核对象。如果没有关闭线程句柄,系统就会一直保持着对线程内核对象的引用,这样,即使该线程执行完毕,它的引用计数仍不会为0,这样该线程内核对象也就不会被释放,只有等到进程终止时,系统才会清理这些残留的对象因此,在程序中,当不再需要线程句柄时,应将其关闭,让这个线程内核对象的引用计数减1

接下来, main函数在标准输出设备上输出一句话: “main thread is running”。

运行程序:

可以看到窗口中输出了一句话: “the main thread is running”,表明主线程运行了,但是线程1的内容输出的并不完整。为什么会出现这样的结果呢?是因为线程创建失败了吗?实际情况并非如此,对于主线程来说,操作系统为它分配了时间片,因此它能够运行。在上述主线程的入口函数main中, 当调用第2行代码创建线程后,就会接着执行它的下一行代码,即调用CloseHandle函数关闭线程句柄,之后就是执行其第4行代码,在标准输出设备上输出一句话,然后该函数就退出了,也就是说主线程执行完成了。当主线程执行完毕后,进程也就退出了,这时进程中所有的资源,包括还没有执行的线程都要退出,也就是说新创建的线程1还没执行完成就退出了。

为了让新创建的线程能够得到完整执行的机会,就需要使主线程暂停执行,即放弃执行权利,操作系统就会从等待运行的线程队列中选择一个线程来执行,这时新创建的线程就可以得到执行的机会

在程序中,如果想让某个线程暂停运行,可以调用Sleep函数,该函数可使调用线程暂停自己的运行,直到指定的时间间隔过去为止。该函数的原型声明如下所示:
void Sleep (DWORD dwMilliseconds);
Sleep函数有一个DWORD类型的参数,指定线程睡眠的时间值,以毫秒为单位,也就是说,如果将此参数指定为1000,实际上是让线程睡眠1秒钟。因此,在上述main函数的最后添加下面这条语句,让主线程暂停运行1s,使其放弃执行的权利,操作系统就会选择线程1让其运行。当该线程运行完成之后,或者1s间隔时间已过,主线程就会恢复运行, main函数退出,进程结束。

b. 多线程实例2

#include<iostream>
#include <windows.h>

using namespace std;

DWORD WINAPI Fun1Proc(LPVOID);
void main() {
	HANDLE hThread1;
	hThread1 = CreateThread(NULL, 0, Fun1Proc, NULL, 0, NULL);
	CloseHandle(hThread1);
	cout << "the main thread is running......" << endl; 
    Sleep(1000);
}
DWORD WINAPI Fun1Proc(LPVOID lp) {
	cout << "1:The Thread 1 is runing......" <<endl;
	cout << "2:The Thread 1 is runing......" << endl;
	cout << "3:The Thread 1 is runing......" << endl;
	cout << "4:The Thread 1 is runing......" << endl;
	cout << "5:The Thread 1 is runing......" << endl;
	cout << "6:The Thread 1 is runing......" << endl;
	cout << "7:The Thread 1 is runing......" << endl;
	cout << "8:The Thread 1 is runing......" << endl;
	cout << "9:The Thread 1 is runing......" << endl;
	cout << "10:The Thread 1 is runing......" << endl;
	cout << "11:The Thread 1 is runing......" << endl;
	cout << "12:The Thread 1 is runing......" << endl;
	cout << "13:The Thread 1 is runing......" << endl;
	cout << "14:The Thread 1 is runing......" << endl;
	cout << "15:The Thread 1 is runing......" << endl;

	return 0;
}

运行程序,出现以下结果:



仔细观察,发现main函数输出的一句话最后有换行命令,但是上图中发现并未立即换行而是等到线程1运行一段时间后执行的换行,而且每一次的换行位置可能不同。

c. 多线程实例3
#include<iostream>
#include <windows.h>

using namespace std;

DWORD WINAPI Fun1Proc(LPVOID);
int index = 0;
void main() {
	HANDLE hThread1;
	hThread1 = CreateThread(NULL, 0, Fun1Proc, NULL, 0, NULL);
	CloseHandle(hThread1);
	while (index++ < 20) {
		cout << "the main thread is running......" << endl;
	}
}
DWORD WINAPI Fun1Proc(LPVOID lp) {
	while (index++ < 20) {
		cout << "The Thread 1 is runing......" << endl;
	}
	return 0;
}



在这里插入图片描述
再次运行程序,在该窗口上,可以看到主线程和线程1交替地输出了自己的信息。也就是说,主线程运行一段时间之后,当它的时间片到期后,操作系统会选择线程1开始运行,为线程1分配一个时间片当线程1运行一段时间后,它的时间片到期了,操作系统又会选择主线程开始运行。于是就看到主线程和线程1在交替运行。这就说明,主线程和其他线程在单CPU平台上是交替运行的,当然,如果在多CPU平台上,主线程和其创建的线程就可以真正地并发运行了

四、线程同步

4.1 火车站售票模拟程序

下面,我们来编写一个模拟火车站售票系统的程序。我们知道,在实际生活中,多个人可以同时购买火车票。也就说,火车站的售票系统肯定是采用多线程技术实现的。这里,我们在上面已编写的程序中再创建一个线程:线程2,然后由主线程创建的两个线程(线程1和线程2)负责销售火车票。

定义了一个全局的变量: tickets,用来表示销售的剩余票。本例为该变量赋予初值: 20,也就说新创建的两个线程将负责销售20张票。对于第一个线程函数(FunlProc)来说,为了让该线程能够不断地销售火车票,需要进行一个while循环。在此循环中,判断tickets变量的值,如果大于0,就销售一张票,即输出thread 1 sells ticket:,接着将当前所卖出的票号打印出来,然后tickets变量的值减1;如果tickets等于或小于0,则表明票已经卖完了,调用break语句终止while循环。对于第二个线程函数(Fun2Proc)来说,其实现过程与第一个线程函数是一样的,只是输出语句是: thread 2 sells ticket:。对主线程来说,这时需要保证在创建的两个线程卖完这20张票之前,该线程不能退出。否则,如果主线程退出了,进程就结束了,线程1和线程2也就退出了。因此,在两个线程卖完票之前,不能让主线程退出。这时,有些读者可能就会想到可以这样做:为了让主线程持续运行,让它进行一个空的while循环,例如在main函数的最后添加如下代码:
while (TRUE) {}
要注意的是,采用这种方式,对于主线程来说,它是能够运行的,并且它将占用CPU的时间,这样就会影响程序执行的效率。因此,为了让主线程不退出,并且不影响程序运行的效率,我们可以调用Sleep函数,并让其睡眠一段时间,例如4秒。这样,当程序执行到Sleep函数时,主线程就放弃其执行的权利进入等待状态,这时的主线程是不占用CPU时间的

d. 多线程实例4

#include<iostream>
#include <windows.h>

using namespace std;

DWORD WINAPI Fun1Proc(LPVOID);
DWORD WINAPI Fun2Proc(LPVOID);
int index = 0;
int tickets = 20;
void main() {
	HANDLE hThread1;
	HANDLE hThread2;
	//创建线程
	hThread1 = CreateThread(NULL, 0, Fun1Proc, NULL, 0, NULL);
	hThread2 = CreateThread(NULL, 0, Fun2Proc, NULL, 0, NULL);
	CloseHandle(hThread1);
	CloseHandle(hThread2);
	//主线程休息4s,此时运行别的线程
	Sleep(4000);
}
//线程1的入口函数
DWORD WINAPI Fun1Proc(LPVOID lp) {
	while (TRUE) {
		if (tickets>0)
		{
			cout << "thread 1 sells ticket:" << tickets-- << endl;
		}
		else
		{
			break;
		}
	}

	return 0;
}
//线程2的入口函数
DWORD WINAPI Fun2Proc(LPVOID lp) {
	while (TRUE) {
		if (tickets > 0)
		{
			cout << "thread 2 sells ticket:" << tickets-- << endl;
		}
		else
		{
			break;
		}
	}
	return 0;
}

Build并运行程序:

可以看到线程1从第20张票开始销售,当该线程执行一段时间后(第一次循环的输出还没有执行完),线程2就开始运行,并全部卖完。线程2结束后,线程1接着上次执行的位置开始执行(接着输出上次未结束的输出语句)。

可以看到线程1从第20张票开始销售,当该线程执行一段时间后,线程2开始运行,该线程执行一段时间后,线程1又继续执行。线程1和线程2就是按这种方式交替执行,直到销售完20张票,即最后打印出最后一张票号: 1。

4.2 多线程程序容易出现的问题

事实上,上述程序存在一个隐患,它有一个潜在的问题,例如,当变量tickets为1时,线程1函数Fun1Proc进入if语句块后,正好该线程的时间片到期了,操作系统就会选择线程2让其执行,而这时变量tickets的值还没有减1,因此这时变量tickets的值仍是1,线程2进入它的if语句块中,于是线程2执行卖票操作,打印出票号1,然后tickets变量执行自减操作,其值变为0。如果当线程2执行完成上述操作之后,正好又轮到线程1开始运行了。而这时线程1上次是执行到if语句块之后才暂停的,因此它将从这个地方继续开始执行,于是它输出当前的票号,而这时tickets变量的值已经是0了,也就是说,我们会看到线程1卖了号码为0的票

当然这种情况是不允许的。有些读者会认为这种情况可能太凑巧了吧,刚才运行时也没有看到这种情况发生。但是读者应注意,火车站售票系统是一个长时间运行的系统,在长时间的运行过程中,两个线程会发生频繁的切换,在这种切换过程中,就有可能会出现上述所说的这种情况,一旦出现这种情况,最后的结果是不可预料的。对于火车站来说,如果一个座位卖出两张票,那么这个后果是比较严重的。自然,火车站就会找到原先开发这套系统的公司让其进行修改,而最终的修改任务一定是落到我们程序员的头上。对程序员来说,在修改一个软件bug时,往往需要重现该错误,然后才能查找该错误的原因。于是,程序员就会开始运行这套系统,希望能够捕捉到这个错误,然而这种错误只是在一些特殊的情况下才会发生,程序员可能运行了好几天也没有看到该错误重现。对于一个不可重现的错误来说,修改起来是非常困难的。因此,这就要求我们程序员在编写代码时,应尽量避免这种错误的发生。

上述问题的出现主要是因为两个线程访问了同一个全局变量: tickets为了避免这种问题的发生,就要求在多个线程之间进行一个同步处理,保证一个线程访问共享资源时,其他线程不能访问该资源。对本例来说,就是当一个线程在销售火车票的过程中,其他线程在该时间段内不能访问同一种资源,本例就是指全局变量: tickets,必须等到前者完成火车票的销售过程之后,其他线程才能访问该资源

这与我们在商场买衣服时所进行的活动类似,当我们在试衣间进行试衣服这一活动时,其他试衣服的人必须等待,只有等待我们完成了试衣服这一活动并离开试衣间时,其他人才能进入该试衣间。

为了能够看到上面所说的那种情况,在上述线程函数中,当进入if语句中之后,立即调用Sleep函数,让线程睡眠片刻。使其成为该语句块的第一行代码:sleep (1);

e. 多线程实例5

#include<iostream>
#include <windows.h>

using namespace std;

DWORD WINAPI Fun1Proc(LPVOID);
DWORD WINAPI Fun2Proc(LPVOID);
int index = 0;
int tickets = 20;
void main() {
	HANDLE hThread1;
	HANDLE hThread2;
	//创建线程
	hThread1 = CreateThread(NULL, 0, Fun1Proc, NULL, 0, NULL);
	hThread2 = CreateThread(NULL, 0, Fun2Proc, NULL, 0, NULL);
	CloseHandle(hThread1);
	CloseHandle(hThread2);
	//主线程休息4s,此时运行别的线程
	Sleep(4000);
}
//线程1的入口函数
DWORD WINAPI Fun1Proc(LPVOID lp) {
	while (TRUE) {
		if (tickets>0)
		{
			Sleep(1);
			cout << "thread 1 sells ticket:" << tickets-- << endl;
		}
		else
		{
			break;
		}
	}

	return 0;
}
//线程2的入口函数
DWORD WINAPI Fun2Proc(LPVOID lp) {
	while (TRUE) {
		if (tickets > 0)
		{
			Sleep(1);
			cout << "thread 2 sells ticket:" << tickets-- << endl;
		}
		else
		{
			break;
		}
	}

	return 0;
}

现在,我们来分析一下程序的执行过程,当程序进入到线程1的if语句块时,就会调用新添加的Sleep语句,从而线程1就要放弃其执行的权利,操作系统就会选择另一个线程来执行。于是线程2开始执行,进入到它的if语句中,就会调用它的Sleep函数,因此线程2也睡眠了。于是又轮到线程1继续执行,打印出当前销售的票号,票号减1,对线程2来说,当其暂停时间到了之后,又继续从其Sleep函数后面的语句开始执行,也打印出当前销售的票号,票号减1,然后线程1和线程2继续按照这种方式执行下去,直到卖完所有的20张票。我们可以再次运行程序,看看会出现什么样的情况
:
在这里插入图片描述

从运行结果可以看到程序最后会销售出0号的票。这就是上面所说的多个线程访问同一个资源时可能会出现的情况。当然,这里我们是人为地让线程发生切换。但是如果系统采用所示代码实现的话,当系统长时间运行时,也可能会出现这样的情况。一般来说,对多线程程序,如果这些线程需要访问共享资源,就需要进行线程间的同步处理。

4.3 利用互斥对象实现线程同步

互斥对象(mutex)属于内核对象,它能够确保线程拥有对单个资源的互斥访问权

互斥对象包含:

  • 一个使用数量
  • 一个线程ID:ID用于标识系统中的哪个线程当前拥有互斥对象
  • 一个计数器:用于指明该线程拥有互斥对象的次数

为了创建互斥对象,需要调用函数: CreateMutex,该函数可以创建或打开一个命名的或匿名的互斥对象,然后程序就可以利用该互斥对象完成线程间的同步

1. CreateMutex函数

1)原型声明:


WINBASEAPI
_Ret_maybenull_
HANDLE
WINAPI
CreateMutexA(
    _In_opt_ LPSECURITY_ATTRIBUTES lpMutexAttributes,
    _In_ BOOL bInitialOwner,
    _In_opt_ LPCSTR lpName
    );

WINBASEAPI
_Ret_maybenull_
HANDLE
WINAPI
CreateMutexW(
    _In_opt_ LPSECURITY_ATTRIBUTES lpMutexAttributes,
    _In_ BOOL bInitialOwner,
    _In_opt_ LPCWSTR lpName
    );

#ifdef UNICODE
#define CreateMutex  CreateMutexW
#else
#define CreateMutex  CreateMutexA
#endif // !UNICODE

2)参数

  • IpMutexAttributes
    一个指向SECURITY_ATTRIBUTES结构的指针,可以给该参数传递NULL值,让互斥对象使用默认的安全性。
  • blnitialOwner
    BOOL类型,指定互斥对象初始的拥有者如果该值为真,则创建这个互斥对象的线程获得该对象的所有权;否则,该线程将不获得所创建的互斥对象的所有权
  • IpName
    指定互斥对象的名称如果此参数为NULL,则创建一个匿名的互斥对象。如果调用成功,该函数将返回所创建的互斥对象的句柄如果创建的是命名的互斥对象,并且在CreateMutex函数调用之前,该命名的互斥对象存在,那么该函数将返回已经存在的这个互斥对象的句柄,而这时调用GetLastError函数将返回ERROR_ALREADY_EXISTS

2. ReleaseMutex函数

另外,当线程对共享资源访问结束后,应释放该对象的所有权,也就是让该对象处于已通知状态。这时需要调用·ReleaseMutex函数,该函数将释放指定对象的所有权。该函数的原型声明如下所示:

WINBASEAPI
BOOL
WINAPI
ReleaseMutex(
    _In_ HANDLE hMutex
    );

ReleaseMutex函数只有一个HANDLE类型的参数,即需要释放的互斥对象的句柄该函数的返回值是BOOL类型,如果函数调用成功,返回非0值;否则返回0值

3. WaitForSingleObject函数

另外,线程必须主动请求共享对象的使用权才有可能获得该所有权,这可以通过调用WaitForSingleObject函数来实现,
1)该函数的原型声明:

WINBASEAPI
DWORD
WINAPI
WaitForSingleObject(
    _In_ HANDLE hHandle,
    _In_ DWORD dwMilliseconds
    );

2)参数

  • hHandle
    所请求的对象的句柄。本例将传递已创建的互斥对象的句柄: hMutex。一旦互斥对象处于有信号状态,则该函数就返回。如果该互斥对象始终处于无信号状态,即未通知的状态,则该函数就会一直等待,这样就会暂停线程的执行
  • dwMilliseconds
    指定等待的时间间隔,以毫秒为单位如果指定的时间间隔已过,即使所请求的对象仍处于无信号状态, WaitForSingleObject函数也会返回。==如果将此参数设置为0,那么. WaitForSingleObject函数将测试该对象的状态并立即返回;如果将此参数设置为INFINITE,则该函数会永远等待,直到等待的对象处于有信号状态才会返回。

调用WaitForSingleObject函数后,该函数会一直等待,只有在以下两种情况下才会返回:

  • 指定的对象变成有信号状态;
  • 指定的等待时间间隔已过。

如果函数调用成功,那么WaitForSingleObject函数的返回值将表明引起该函数返回的事件,下表列出了该函数可能的返回值:

首先定义了一个HANDLE类型的全局变量: hMutex,用保存即将创建的互斥对象句柄。接下来,在main函数中,调用CreateMutex函数创建一个匿名的互斥对象。然后在线程1和线程2中**,在需要保护的代码前面添加WaitForSingleObject函数的调,让其请求互斥对象的所有权,这样线程1和线程2就会一直等待,除非所请求的对象于有信号状态,该函数才会返回,线程才能继续往下执行,即才能执行受保护的代码**。代码中,在线程1和线程2访问它们共享的全局变量tickets之前,加了下述语句,实现线程同步:waitForsingleobject (hMutex, INFINITE);
当执行到这条语句时,线程1和线程2就会等待,除非所等待的互斥对象:Mutex处于有信号状态,线程才能继续向下执行,即才能访问tickets变量,完成火车票销售工作。

当对所要保护的代码操作完成之后,应该调用ReleaseMutex函数释放当前线程对互斥象的所有权,这时,操作系统就会将该互斥对象的线程ID设置为0,然后将该互斥对设置为有信号状态。使得其他线程有机会获得该对象的所有权,从而获得对共享资源的访问。

f. 多线程实例6
#include<iostream>
#include <windows.h>

using namespace std;
//互斥对象
HANDLE hMutex;
DWORD WINAPI Fun1Proc(LPVOID);
DWORD WINAPI Fun2Proc(LPVOID);
int tickets = 20;
void main() {
	HANDLE hThread1;
	HANDLE hThread2;
	//创建线程
	hThread1 = CreateThread(NULL, 0, Fun1Proc, NULL, 0, NULL);
	hThread2 = CreateThread(NULL, 0, Fun2Proc, NULL, 0, NULL);
	CloseHandle(hThread1);
	CloseHandle(hThread2);

	//创建互斥对象
	hMutex = CreateMutex(NULL, FALSE, NULL);
	
	//主线程休息4s,此时运行别的线程
	Sleep(4000);
}
//线程1的入口函数
DWORD WINAPI Fun1Proc(LPVOID lp) {
	while (TRUE) {
		WaitForSingleObject(hMutex, INFINITE);
		if (tickets>0)
		{
			Sleep(1);
			cout << "thread 1 sells ticket:" << tickets-- << endl;
		}
		else
		{
			break;
		}
		ReleaseMutex(hMutex);
	}

	return 0;
}
//线程2的入口函数
DWORD WINAPI Fun2Proc(LPVOID lp) {
	while (TRUE) {
		WaitForSingleObject(hMutex, INFINITE);
		if (tickets > 0)
		{
			Sleep(1);
			cout << "thread 2 sells ticket:" << tickets-- << endl;
		}
		else
		{
			break;
		}
		ReleaseMutex(hMutex);
	}
	
	return 0;
}

下面,我们来分析一下程序的执行过程。在创建互斥对象时,第二个参数传递的是FALSE值,这表明当前没有线程拥有这个互斥对象,于是,操作系统就会将该互斥对象设置为有信号状态当第一个线程开始运行时,进入while循环后,调用WaitForSingleObject函数,因为这时互斥对象处于有信号状态,所以该线程就请求到了这个互斥对象,操作系统就会将该互斥对象的线程ID设置为线程1的ID接着,操作系统会将这个互斥对象设置为未通知状态。线程1继续往下运行,调用Sleep函数,于是暂停执行。操作系统就会选择线程2开始执行,该线程的执行过程也是一样的,进入到while循环之后,调用WaitForSingleObject函数,但这时该互斥对象已经被线程1所拥有,处于未通知状态,线程2没有获得互斥对象的所有权,因此WaitForSingleObject函数就会处于等待状态,从而导致线程2处于暂停执行状态。当线程1的睡眠时间到了之后,线程1将会继续执行,即销售一张火车票。这时线程1调用ReleaseMutex函数释放互斥对象的所有权,也就是让该对象处于已通知状态。如果这时轮到线程2执行了,那么该线程的WaitForSingleObject函数就可以得到互斥对象的所有权,线程2继续执行下面的代码。同样地,当线程2销售了一张票之后,也通过调用ReleaseMutex函数,释放它对互斥对象的所有权。

我们可以把互斥对象看成是一把房间钥匙,只有得到这把钥匙后,我们才能进入这个·房间,完成应做的工作。当我进入房间关上门后,因为钥匙在我手上,其他人拿不到该钥匙,因此就无法进入这个房间,只能等待。只有等我离开这个房间并交出钥匙,其他人才能进入该房间,完成应做的工作,最后离开房间,交出钥匙。

Build并运行程序,将会发现这时所销售的票号正常,没有看到销售了号码为0的票。

这就是通过互斥对象来保存多线程间的共享资源,本例是保护对全局变量的访问,使得当其中一个线程访问该资源时,其他线程不能访问同一种资源

读者应注意这时WaitForSingleObject函数的调用位置,如果我们把两个线程函数中调用WaitForSingleObject函数的代码放到while循环之前,并把ReleaseMutex函数的调用放在while循环结束之后,这时程序会出现什么情况呢?

g. 多线程实例7
#include<iostream>
#include <windows.h>

using namespace std;
//互斥对象
HANDLE hMutex;
DWORD WINAPI Fun1Proc(LPVOID);
DWORD WINAPI Fun2Proc(LPVOID);
int tickets = 20;
void main() {
	HANDLE hThread1;
	HANDLE hThread2;
	//创建线程
	hThread1 = CreateThread(NULL, 0, Fun1Proc, NULL, 0, NULL);
	hThread2 = CreateThread(NULL, 0, Fun2Proc, NULL, 0, NULL);
	CloseHandle(hThread1);
	CloseHandle(hThread2);

	//创建互斥对象
	hMutex = CreateMutex(NULL, FALSE, NULL);
	
	//主线程休息4s,此时运行别的线程
	Sleep(4000);
}
//线程1的入口函数
DWORD WINAPI Fun1Proc(LPVOID lp) {
	WaitForSingleObject(hMutex, INFINITE);
	while (TRUE) {
		
		if (tickets>0)
		{
			Sleep(1);
			cout << "thread 1 sells ticket:" << tickets-- << endl;
		}
		else
		{
			break;
		}
	}
	ReleaseMutex(hMutex);
	return 0;
}
//线程2的入口函数
DWORD WINAPI Fun2Proc(LPVOID lp) {
	WaitForSingleObject(hMutex, INFINITE);
	while (TRUE) {
		
		if (tickets > 0)
		{
			Sleep(1);
			cout << "thread 2 sells ticket:" << tickets-- << endl;
		}
		else
		{
			break;
		}
		
	}
	ReleaseMutex(hMutex);
	return 0;
}


或者
在这里插入图片描述
读者可以试着运行这时的程序,结果将会看到火车票的销售工作是没有问题的,但是发现这时只是其中一个线程在销售票,对于另一个线程来说,没有看到它销售任何一张票。

为什么有时候是线程1在运行,有时候是线程2在运行,这取决于哪个线程先获得互斥对象。这是系统分配的问题。

我们可以分析这时的程序执行过程:
1) 当线程1开始运行时,它就调用WaitForSingleObject函数请求互斥对象,由于这时互斥对象处于有信号状态,线程1可以请求到该对象,因此继续执行,进入while循环,
2)当执行到Sleep函数时,它会暂停执行;
3)于是,线程2开始执行,它也调用WaitForSingleObject函数请求互斥对象,但是该互斥对象当前已被线程1所拥有,于是线程2请求不到该对象的所有权,线程2只能等待。
4)当线程1暂停时间到了之后,将继续执行,销售一张票。之后它又进入下一次循环。也就是说,该互斥对象始终被线程1所拥有,线程1将在while循环内部不断地销售火车票,直到所有的20张火车票都被卖完之后,线程1才会退出while循环,调用ReleaseMutex函数释放对互斥对象的所有权。
5)这时,线程2才能获得该互斥对象的所有权,继续执行下面的代码,但是该线程判断出票号已经不大于0了,因此就没有执行if语句下的代码,直接退出while循环,调用ReleaseMutex函数释放互斥对象的所有权,线程2结束。于是,程序的结果就是线程2没有销售一张票。
通过本例是想提醒读者,一定要注意调用WaitForSingleObject函数的位置

如果在创建互斥对象时,将第二个参数设置为TRUE。

h. 多线程实例8
#include<iostream>
#include <windows.h>

using namespace std;
//互斥对象
HANDLE hMutex;
DWORD WINAPI Fun1Proc(LPVOID);
DWORD WINAPI Fun2Proc(LPVOID);
int tickets = 20;
void main() {
	HANDLE hThread1;
	HANDLE hThread2;
	//创建线程
	hThread1 = CreateThread(NULL, 0, Fun1Proc, NULL, 0, NULL);
	hThread2 = CreateThread(NULL, 0, Fun2Proc, NULL, 0, NULL);
	CloseHandle(hThread1);
	CloseHandle(hThread2);

	//创建互斥对象
	hMutex = CreateMutex(NULL, TRUE, NULL);
	
	//主线程休息4s,此时运行别的线程
	Sleep(4000);
}
//线程1的入口函数
DWORD WINAPI Fun1Proc(LPVOID lp) {
	WaitForSingleObject(hMutex, INFINITE);
	while (TRUE) {
		
		if (tickets>0)
		{
			Sleep(1);
			cout << "thread 1 sells ticket:" << tickets-- << endl;
		}
		else
		{
			break;
		}
	}
	ReleaseMutex(hMutex);
	return 0;
}
//线程2的入口函数
DWORD WINAPI Fun2Proc(LPVOID lp) {
	WaitForSingleObject(hMutex, INFINITE);
	while (TRUE) {
		
		if (tickets > 0)
		{
			Sleep(1);
			cout << "thread 2 sells ticket:" << tickets-- << endl;
		}
		else
		{
			break;
		}
		
	}
	ReleaseMutex(hMutex);
	return 0;
}

然后再次运行程序,结果将会看到线程1和线程2都没有销售票。

我们来分析这时的程序执行过程:
1)当调用CreateMutex函数创建互斥对象时,如果将第二个参数设置为TRUE,表明创建互斥对象的线程,本例即主线程,拥有该互斥对象,而我们在主线程中并没有释放该对象。
2)因此对于线程1和线程2来说,它们是无法获得该互斥对象的所有权的。它们只能等待,直到主线程结束,才会释放该互斥对象的所有权,但这时两个线程也已退出了。

那么如果我们这样做:在线程函数的while循环内部,在调用WaitForSingleObject函数之前,先调用ReleaseMutex函数释放互斥对象的所有权,之后,再调用WaitForSingleObject函数请求该互斥对象的所有权。这时线程1和线程2能否得到该互斥对象的所有权呢?

I. 多线程实例9
#include<iostream>
#include <windows.h>

using namespace std;
//互斥对象
HANDLE hMutex;
DWORD WINAPI Fun1Proc(LPVOID);
DWORD WINAPI Fun2Proc(LPVOID);
int tickets = 20;
void main() {
	HANDLE hThread1;
	HANDLE hThread2;
	//创建线程
	hThread1 = CreateThread(NULL, 0, Fun1Proc, NULL, 0, NULL);
	hThread2 = CreateThread(NULL, 0, Fun2Proc, NULL, 0, NULL);
	CloseHandle(hThread1);
	CloseHandle(hThread2);

	//创建互斥对象
	hMutex = CreateMutex(NULL, TRUE, NULL);
	
	//主线程休息4s,此时运行别的线程
	Sleep(4000);
}
//线程1的入口函数
DWORD WINAPI Fun1Proc(LPVOID lp) {
	
	while (TRUE) {
		ReleaseMutex(hMutex);
		WaitForSingleObject(hMutex, INFINITE);
		if (tickets>0)
		{
			Sleep(1);
			cout << "thread 1 sells ticket:" << tickets-- << endl;
		}
		else
		{
			break;
		}
		ReleaseMutex(hMutex);
	}
	
	return 0;
}
//线程2的入口函数
DWORD WINAPI Fun2Proc(LPVOID lp) {

	while (TRUE) {
		ReleaseMutex(hMutex);
		WaitForSingleObject(hMutex, INFINITE);
		if (tickets > 0)
		{
			Sleep(1);
			cout << "thread 2 sells ticket:" << tickets-- << endl;
		}
		else
		{
			break;
		}
		ReleaseMutex(hMutex);
	}
	return 0;
}

再次运行程序:

将会看到线程1和线程2仍未得到销售火车票的机会。

下面分析出现这种情况的原因:
对于互斥对象来说,它是惟一与线程相关的内核对象。
1)当主线程拥有互斥对象时,操作系统会将互斥对象的线程ID设置为主线程的ID,当在线程1中调用ReleaseMutex函数释放互斥对象的所有权时,操作系统会判断线程1的线程ID与互斥对象内部所维护的线程ID是否相等,只有相等才能完成释放操作。

2)正因为上述实现方法中,释放互斥对象的线程与互斥对象内部所维护的线程ID不相等,所以该互斥对象并没有被释放。
3)所以接下来请求该互斥对象的所有权操作就只能一直等待,因此线程1和线程2都没有执行if语句下的代码,从而就没有看到线程售票的信息。也就是说,对互斥对象来说,谁拥有谁释放

知道了这一原则,我们就可以这么做,在主线程中,当调用CreateMutex创建了互斥对象之后,调用ReleaseMutex函数释放主线程对该互斥对象的所有权:

j. 多线程实例10
#include<iostream>
#include <windows.h>

using namespace std;
//互斥对象
HANDLE hMutex;
DWORD WINAPI Fun1Proc(LPVOID);
DWORD WINAPI Fun2Proc(LPVOID);
int tickets = 20;
void main() {
	HANDLE hThread1;
	HANDLE hThread2;
	//创建线程
	hThread1 = CreateThread(NULL, 0, Fun1Proc, NULL, 0, NULL);
	hThread2 = CreateThread(NULL, 0, Fun2Proc, NULL, 0, NULL);
	CloseHandle(hThread1);
	CloseHandle(hThread2);

	//创建互斥对象
	hMutex = CreateMutex(NULL, TRUE, NULL);
	ReleaseMutex(hMutex);
	//主线程休息4s,此时运行别的线程
	Sleep(4000);

}
//线程1的入口函数
DWORD WINAPI Fun1Proc(LPVOID lp) {
	
	while (TRUE) {
		WaitForSingleObject(hMutex, INFINITE);
		if (tickets>0)
		{
			Sleep(1);
			cout << "thread 1 sells ticket:" << tickets-- << endl;
		}
		else
		{
			break;
		}
		ReleaseMutex(hMutex);
	}
	return 0;
}
//线程2的入口函数
DWORD WINAPI Fun2Proc(LPVOID lp) {

	while (TRUE) {
		WaitForSingleObject(hMutex, INFINITE);
		if (tickets > 0)
		{
			Sleep(1);
			cout << "thread 2 sells ticket:" << tickets-- << endl;
		}
		else
		{
			break;
		}
		ReleaseMutex(hMutex);
	}
	return 0;
}

再次运行程序,这时将会看到线程1和线程2交替销售火车票了。
在这里插入图片描述
说明这两个线程得到了互斥对象的所有权,从而执行了if语句下的代码。

对本程序的编写,还有一种情况需要说明,就是在主线程中,当调用CreateMutex函数创建互斥对象之后,调用WaitForSingleObject函数请求该互斥对象,然后再调用ReleaseMutex函数释放主线程对该互斥对象的所有权,这时程序的结果会是怎样的呢?

k. 多线程实例11
#include<iostream>
#include <windows.h>

using namespace std;
//互斥对象
HANDLE hMutex;
DWORD WINAPI Fun1Proc(LPVOID);
DWORD WINAPI Fun2Proc(LPVOID);
int tickets = 20;
void main() {
	HANDLE hThread1;
	HANDLE hThread2;
	//创建线程
	hThread1 = CreateThread(NULL, 0, Fun1Proc, NULL, 0, NULL);
	hThread2 = CreateThread(NULL, 0, Fun2Proc, NULL, 0, NULL);
	CloseHandle(hThread1);
	CloseHandle(hThread2);

	//创建互斥对象
	hMutex = CreateMutex(NULL, TRUE, NULL);
	WaitForSingleObject(hMutex, INFINITE);
	ReleaseMutex(hMutex);
	//主线程休息4s,此时运行别的线程
	Sleep(4000);

}
//线程1的入口函数
DWORD WINAPI Fun1Proc(LPVOID lp) {
	
	while (TRUE) {
		WaitForSingleObject(hMutex, INFINITE);
		if (tickets>0)
		{
			Sleep(1);
			cout << "thread 1 sells ticket:" << tickets-- << endl;
		}
		else
		{
			break;
		}
		ReleaseMutex(hMutex);
	}
	
	return 0;
}
//线程2的入口函数
DWORD WINAPI Fun2Proc(LPVOID lp) {

	while (TRUE) {
		WaitForSingleObject(hMutex, INFINITE);
		if (tickets > 0)
		{
			Sleep(1);
			cout << "thread 2 sells ticket:" << tickets-- << endl;
		}
		else
		{
			break;
		}
		ReleaseMutex(hMutex);
	}
	return 0;
}

运行这时的程序,将会发现线程1和线程2没有执行if语句下的代码。

我们分析这时的程序调用情况:
当调用WaitForSingleObject函数请求互斥对象时,操作系统需要判断当前请求互斥对象的线程的ID是否与互斥对象当前拥有者的线程ID相等,如果相等,即使该互斥对象处于未通知状态,调用线程仍然能够获得其所有权,然后WaitForSingleObject函数返回。

对于同一个线程多次拥有的互斥对象来说,该互斥对象内部的计数器记录了该线程拥有的次数

在本例中,

  1. 当第一次创建互斥对象时,主线程拥有这个互斥对象,除了将互斥对象的线程ID设置为主线程的ID以外,同时还将该互斥对象内部的计数器变为1。
  2. 这里应注意,当主线程拥有该互斥对象时,该对象就处于未通知状态了,但是当在主线程中调用WaitForSingleObject函数请求该互斥对象的所有权时,因为请求的线程的ID和该互斥对象当前所有者的线程ID是相同的,所以仍然能够请求到这个互斥对象,操作系统通过互斥对象内部的计数器来维护同一个线程请求到该互斥对象的次数,于是该计数器就会增加1,这时,互斥对象内部计数器的值为2,
  3. 当接下来调用ReleaseMutex函数释放该互斥对象的所有权时,实际上就是递减这个计数器,但此时该计数器的值仍为1,因此操作系统不会将这个互斥对象变为已通知状态。
  4. 当然,随后线程1和线程2请求这个互斥对象时,它们是得不到该对象的所有权的。

如果想让线程1和线程2能够执行if语句下的代码,只有在主线程中再次调用ReleaseMutex函数,这时该互斥对象内部维护的计数器就变成0了,操作系统就会将该互斥对象的线程ID设置为0,同时将该对象设置为有信号状态。之后,程1和线程2就可以请求到该互斥对象的所有权了。

l. 多线程实例12
#include<iostream>
#include <windows.h>

using namespace std;
//互斥对象
HANDLE hMutex;
DWORD WINAPI Fun1Proc(LPVOID);
DWORD WINAPI Fun2Proc(LPVOID);
int tickets = 20;
void main() {
	HANDLE hThread1;
	HANDLE hThread2;
	//创建线程
	hThread1 = CreateThread(NULL, 0, Fun1Proc, NULL, 0, NULL);
	hThread2 = CreateThread(NULL, 0, Fun2Proc, NULL, 0, NULL);
	CloseHandle(hThread1);
	CloseHandle(hThread2);

	//创建互斥对象
	hMutex = CreateMutex(NULL, TRUE, NULL);
	WaitForSingleObject(hMutex, INFINITE);
	ReleaseMutex(hMutex);
	ReleaseMutex(hMutex);
	//主线程休息4s,此时运行别的线程
	Sleep(4000);

}
//线程1的入口函数
DWORD WINAPI Fun1Proc(LPVOID lp) {
	
	while (TRUE) {
		WaitForSingleObject(hMutex, INFINITE);
		if (tickets>0)
		{
			Sleep(1);
			cout << "thread 1 sells ticket:" << tickets-- << endl;
		}
		else
		{
			break;
		}
		ReleaseMutex(hMutex);
	}
	
	return 0;
}
//线程2的入口函数
DWORD WINAPI Fun2Proc(LPVOID lp) {

	while (TRUE) {
		WaitForSingleObject(hMutex, INFINITE);
		if (tickets > 0)
		{
			Sleep(1);
			cout << "thread 2 sells ticket:" << tickets-- << endl;
		}
		else
		{
			break;
		}
		ReleaseMutex(hMutex);
	}
	return 0;
}

正是因为互斥对象具有与线程相关这一特点,所以在使用互斥对象时需要小心仔细,如果多次在同一个线程中请求同一个互斥对象,那么就需要相应地多次调用ReleaseMutex函数释放该互斥对象

下面我们再看一种情况,修改两个线程的入口函数:

m. 多线程实例13
#include<iostream>
#include <windows.h>

using namespace std;
//互斥对象
HANDLE hMutex;
DWORD WINAPI Fun1Proc(LPVOID);
DWORD WINAPI Fun2Proc(LPVOID);
int tickets = 20;
void main() {
	HANDLE hThread1;
	HANDLE hThread2;
	//创建线程
	hThread1 = CreateThread(NULL, 0, Fun1Proc, NULL, 0, NULL);
	hThread2 = CreateThread(NULL, 0, Fun2Proc, NULL, 0, NULL);
	CloseHandle(hThread1);
	CloseHandle(hThread2);

	//创建互斥对象
	hMutex = CreateMutex(NULL, TRUE, NULL);
	WaitForSingleObject(hMutex, INFINITE);
	ReleaseMutex(hMutex);
	ReleaseMutex(hMutex);
	//主线程休息4s,此时运行别的线程
	Sleep(4000);

}
//线程1的入口函数
DWORD WINAPI Fun1Proc(LPVOID lp) {
	
	WaitForSingleObject(hMutex, INFINITE);
	cout << "thread 1 is running......" << endl;
	return 0;
}
//线程2的入口函数
DWORD WINAPI Fun2Proc(LPVOID lp) {

	WaitForSingleObject(hMutex, INFINITE);
	cout << "thread 2 is running......" << endl;
	return 0;
}

线程1请求互斥对象之后输出一句话: thread 1 is running,接下来它并没有释放该互斥对象就退出了。线程2的函数实现代码是一样的。那么现在线程2能获得互斥对象的所有权吗?我们可以运行这时的程序:
在这里插入图片描述

可以看到,线程1和线程2都完整地运行了。在程序运行时,操作系统维护了线程的信息以及与该线程相关的互斥对象的信息,因此它知道哪个线程终止了。如果某个线程得到其所需互斥对象的所有权,完成其线程代码的运行,但没有释放该互斥对象的所有权就退出之后,操作系统一旦发现该线程已经终止,它就会自动将该线程所拥有的互斥对象的线程ID设为0,并将其计数器归0。因此,在本例中,一旦操作系统判断出线程2终止了,那么它就会将互斥对象的引用计数置为0,线程ID也置为0,这时线程1就可以得到互斥对象的所有权。

另外,可以根据WaitForSingleObject函数的返回值知道当前线程是如何得到互斥对象的所有权的,是正常得到的,还是因先前拥有该对象的线程退出后获得的

但是,如果判断其返回值为WATT_ABANDONED时,那就要小心了,由于不知道是因为先前拥有该对象的线程在终止之前没有调用ReleaseMutex函数释放所有权,还是先前拥有该对象的线程异常终止,这时在线程中,通过WaitForSingleObject函数所保护的代码,它们所访问的资源当前处于什么状态是不清楚的,如果这时进入这段代码对所保护的资源进行操作,结果将是未知的。因此,在程序中应该根据WaitForSingleObject函数的返回值进行一些相应处理。

五、保证应用程序只有一个实例运行

在使用金山词霸时,同时只能运行一个金山词霸程序的实例。当多次打开金山词霸程序时,它实际上是将先前已经运行的金山词霸实例激活,使其处于前台运行的程序。

对这种同时只能有应用程序的一个实例运行的功能,可以通过命名的互斥对象来实现。在调用CreateMutex函数创建一个命名的互斥对象后,如果其返回值是一个有效的句柄,那么可以接着调用GetLastError函数,如果该函数返回的是ERROR_ALREADYEXISTS,就表明先前已经创建了这个命名的互斥对象,因此就可以知道先前已经有该应用程序的一个实例在运行了。当然如果GetLastError函数返回的不是ERROR_ALREADY_EXISTS,就说明这个互斥对象是新创建的,从而也就知道当前启动的这个进程是应用程序的第一个实例。

下面,我们就为MultiThread程序添加代码,以实现同时只能启动该程序的一个实例:

n. 多线程实例14
#include<iostream>
#include <windows.h>

using namespace std;
//互斥对象
HANDLE hMutex;
DWORD WINAPI Fun1Proc(LPVOID);
DWORD WINAPI Fun2Proc(LPVOID);
int tickets =20;
void main() {
	HANDLE hThread1;
	HANDLE hThread2;
	//创建线程
	hThread1 = CreateThread(NULL, 0, Fun1Proc, NULL, 0, NULL);
	hThread2 = CreateThread(NULL, 0, Fun2Proc, NULL, 0, NULL);
	CloseHandle(hThread1);
	CloseHandle(hThread2);

	//创建互斥对象
	//TRUE: 创建的线程拥有该互斥对象的使用权
	hMutex = CreateMutex(NULL, TRUE, "tickets");
	if (hMutex)
	{
		if (ERROR_ALREADY_EXISTS==GetLastError())
		{
			cout << "只有一个实例能够运行" << endl;
			return ;
		}
	}
	//等待互斥对象有信号
	WaitForSingleObject(hMutex, INFINITE);
	ReleaseMutex(hMutex);
	ReleaseMutex(hMutex);
	//主线程休息4s,此时运行别的线程
	Sleep(4000);

}
//线程1的入口函数
DWORD WINAPI Fun1Proc(LPVOID lp) {
	
	WaitForSingleObject(hMutex, INFINITE);
	cout << "thread 1 is running......" << endl;
	return 0;
}
//线程2的入口函数
DWORD WINAPI Fun2Proc(LPVOID lp) {

	WaitForSingleObject(hMutex, INFINITE);
	cout << "thread 2 is running......" << endl;
	return 0;
}

若运行出现以下错误:

解决:
点击项目,将字符集替换成多字节。

不关闭调试控制台,多次运行项目:

因为该程序在第二次运行时,"它判断出先前已经有一个该程序的实例在运行了,所以它就打印出那行提示字符并立即退出。这就是利用命名的互斥对象实现同时只能有应用程序的一个实例在运行这一功能的方法。

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值