飞道的博客

10001.C++学习笔记——windows环境下多线程编程

520人阅读  评论(0)

10001.C++学习笔记——Windows环境下多线程编程


一、什么是多线程

1. 什么是进程

  • 进程指在系统中正在运行的一个应用程序,每个进程之间是独立的,每个进程均运行在其专门切受保护的内容空间内。

2. 什么是线程

  • 线程是进程的基本执行单位,一个进程的所有任务都在线程中执行。

2.1 线程的特点

2.1.1 线程内核对象

  • 操作系统对进程和线程的管理都是通过相应的内核对象实现的。它会为每个线程都分配并初始化该种数据结构。在该数据结构中包含一组对线程进行描述的属性。数据结构中还包含所谓的线程上下文,上下文是一个内存块,其中包含了CPU的寄存器集合。

2.1.2 线程控制块

  • 一个线程实体包含程序,数据和线程控制块TCB(Thread Control Block)。线程是动态的概念,TCB包含以下信息:
    • 线程状态
    • 当线程不运行时,被保存的现场资源。
    • 一组执行堆栈。
    • 存在每个线程的局部变量主存区。
    • 访问同一个进程的主存和其它资源。

2.1.3 独立调度和分派的基本单位

  • 线程是能独立运行的基本单位,因而也是独立调度和分派的基本单位。CPU是以线程为单位执行,创建出来的线程不一定马上执行,调用后允许线程允许一个“时间片”,单核处理器一个时间片只执行一个线程,而多核处理器可允许一个进程中所有线程都能并发执行(多处理器允许线程是真正的并发)。

2.1.4 上下文切换

  • 线程是由CPU进行调度的,CPU的一个时间片内只执行一个线程上下文内的线程,当CPU由执行线程A切换到执行线程B的过程会发生一系列的操作。如:“保存线程A的执行现场”然后“载入线程B的执行现场”,这个过程称为“上下文切换”,这个上下文切换过程会消耗资源,应该尽量减少上下文切换的发生。
  • 引起上下文切换的原因
    • 时间片用完,CPU正常调度下一个任务。
    • 被其它优先级更高的任务抢占。
    • 执行任务碰到IO阻塞,调度器挂起当前任务,切换执行下一个任务。
    • 用户代码主动挂起当前任务让出CPU时间。
    • 多任务抢占资源,由于没有抢到被挂起。
    • 硬件中断。

2.1.5 共享进程资源

  • 在同一个进程中的各个线程,都可以共享该进程所拥有的资源,这首先表现在:所有线程都具有相同的地址空间(进程的地址空间),这意味着,线程可以访问该地址空间的每个虚地址;此外,还可以访问进程所拥有的已打开文件、定时器、信号量机构等。由于同一个进程的线程共享内存和文件,所以线程之间互相通信不必调用内核。

3. 线程状态图


二、C++多线程编程

1. 头文件

  • 头文件: #include <windows.h>

2. API函数介绍

2.1 创建线程函数:CreateThread

  • 函数原型:HANDLE CreateThread(
               LPSECURITY_ATTRIBUTES   lpThreadAttributes,   //线程安全属性
               DWORD  dwStackSize,  //堆栈大小
               LPTHREAD_START_ROUTINE lpStartAddress, //线程函数
               LPVOID lpParameter, //线程参数
               DWORD dwCreationFlags, //线程创建属性
               LPDWORD lpThreadId //线程ID
           );
  • 参数讲解:
    • lpThreadAttributes :指向LPSECURITY_ATTRIBUTES类型结构的指针。在Windows NT中,设为NULL使用默认的安全属性;
    • dwStackSize :设置初始栈的大小,以字节为单位,如果为0,那么默认将使用与调用该函数的线程相同的栈空间大小;
    • lpStartAddress:指向线程函数的指针,函数名称没有限制,但是必须以下列形式声明:DWORD WINAPI ThreadProc(LPVOID IpParam);
    • lpParameter:向线程函数传递的参数,是一个void*的指针,不需传递参数时为NULL;
    • dwCreationFlags:线程标志,可取值如下:
      • CREATE_SUSPENDED:创建一个挂起的线程,它无法运行直到调用ResumeThread();
      • 0:表示创建后立即激活;
      • STACK_SIZE_PARAM_IS_A_RESERVATION:如果未使用该标记,则使用dwStackSize指定提交的大小。
    • lpThreadId:保存新线程的ID,若不想返回线程的ID,设置值为NULL;
  • 返回值:
    • 创建成功返回线程的句柄,失败返回NULL,调用GetLastError()获知失败原因。
  • 注意事项:
    • CreateThread是Windows API中在主线程的基础上创建的一个新进程。创建成功之后会返回一个hThread的handle,且内核对象的计数加1,CloseHandle之后,引用计数减1,当变成0时,系统删除内核对象。handle仅仅是线程的一个“标识”。

代码如下(示例):

#include <Windows.h>
#include <iostream>
using namespace std;
DWORD WINAPI ThreadFunc(LPVOID IpThreadParame); // 线程处理函数,_stdcall 代表window标准调用函数

int main() {
   
	std::cout << "主线程开始!" << std::endl;
	DWORD pThreadId = 0; // 定义线程id
	
	char cStr[] = "heallo";
	HANDLE hThread = CreateThread(NULL, 0, ThreadFunc, cStr, 0, &pThreadId);

	if (!hThread) {
   
		std::cout << "线程创建失败:" << GetLastError() << std::endl;
	}
	else {
   
		std::cout << "线程的句柄:" << hThread << std::endl;
		std::cout << "线程的ID:" << pThreadId << std::endl;
	}
	CloseHandle(hThread);
	std::cout << "主线程结束!" << std::endl;
	return 0;
}

DWORD WINAPI ThreadFunc(LPVOID IpThreadParame)
{
   
	std::cout << "子线程开始!" << std::endl;
	char* str = (char*)IpThreadParame;
	std::cout << "线程处理函数中:" << str << std::endl;
	std::cout << "子线程的ID:" << GetCurrentThreadId() << std::endl; // 获取当前线程的ID
	std::cout << "子线程结束!" << std::endl;
	return 0;
}

2.2 获取当前线程ID函数:GetCurrentThread

  • 函数原型:DWORD WINAPI GetCurrentThreadId(VOID);
  • 返回值:返回获取当前的线程ID。

2.3 关闭线程句柄:CloseHandle

  • 函数原型:BOOL CloseHandle(HANDLE hObject);
  • 作用:只是关闭一个线程句柄,引用计数减1,标识我不对这个句柄对于的线程做任何干预(诸如:WaitForSingleObject之类),但是没有结束线程。
  • 返回值:成功返回True,失败返回False,调用GetLastError()获取失败原因。

2.4 挂起指定线程:SuspendThread

  • 函数原型:DWORD WINAPI SuspendThread(HANDLE hThread);
  • 参数:
    • hThread:需要挂起的句柄;
  • 作用:只是关闭一个线程句柄,引用计数减1,标识我不对这个句柄对于的线程做任何干预(诸如:WaitForSingleObject之类),但是没有结束线程。
  • 返回值:成功返回True,失败返回False,调用GetLastError()获取失败原因。

代码如下(示例):

#include <Windows.h>
#include <iostream>
using namespace std;
DWORD WINAPI ThreadFunc(LPVOID IpThreadParame); // 线程处理函数,_stdcall 代表window标准调用函数

int main() {
   
	std::cout << "主线程开始!" << std::endl;

	DWORD pThreadId = 0; // 定义线程id
	
	char cStr[] = "heallo";
	HANDLE hThread = CreateThread(NULL, 0, ThreadFunc, cStr, 0, &pThreadId);

	if (!hThread) {
   
		std::cout << "线程创建失败:" << GetLastError() << std::endl;
	}
	else {
   
		std::cout << "线程的句柄:" << hThread << std::endl;
		std::cout << "线程的ID:" << pThreadId << std::endl;
	}
	std::cout << "主线程id:" << GetCurrentThreadId() << std::endl;
	std::cout << "主线程结束!" << std::endl;
	return 0;
}

DWORD WINAPI ThreadFunc(LPVOID IpThreadParame)
{
   
	std::cout << "子线程开始!" << std::endl;
	char* str = (char*)IpThreadParame;

	while (TRUE) {
   
		std::cout << "线程处理函数中:" << str << std::endl;
		std::cout << "子线程的ID:" << GetCurrentThreadId() << std::endl;
		Sleep(1000);
	}

	std::cout << "子线程结束!" << std::endl;
	return 0;
}

代码结果如下:

可以发现由于子线程和主线程是并发执行的,所以会导致主线程在子线程之间执行完。为了保证主线程在子线程之后完事,可以在主线程前添加getchar()

主线程在子线程前完成代码:

#include <Windows.h>
#include <iostream>
using namespace std;
DWORD WINAPI ThreadFunc(LPVOID IpThreadParame); // 线程处理函数,_stdcall 代表window标准调用函数

int main() {
   
	std::cout << "主线程开始!" << std::endl;

	DWORD pThreadId = 0; // 定义线程id
	
	char cStr[] = "heallo";
	HANDLE hThread = CreateThread(NULL, 0, ThreadFunc, cStr, 0, &pThreadId);

	if (!hThread) {
   
		std::cout << "线程创建失败:" << GetLastError() << std::endl;
	}
	else {
   
		std::cout << "线程的句柄:" << hThread << std::endl;
		std::cout << "线程的ID:" << pThreadId << std::endl;
	}
	std::cout << "主线程id:" << GetCurrentThreadId() << std::endl;
	getchar();
	std::cout << "主线程结束!" << std::endl;
	return 0;
}

DWORD WINAPI ThreadFunc(LPVOID IpThreadParame)
{
   
	std::cout << "子线程开始!" << std::endl;
	char* str = (char*)IpThreadParame;
	int iCount = 0;
	while (++iCount < 6) {
   
		std::cout << "线程处理函数中:" << str << std::endl;
		std::cout << "子线程的ID:" << GetCurrentThreadId() << std::endl;
		Sleep(1000);
	}

	std::cout << "子线程结束!" << std::endl;
	return 0;
}

主线程在子线程前完成代码结果:

2.5 恢复被挂起的线程的执行:ResumeThread

  • 函数原型:DWORD WINAPI ResumeThread(HANDLE hThread);
  • 参数:
    • hThread:需要恢复的挂起线程句柄;

2.6 休眠线程的执行:Sleep

  • 函数原型: VOID WINAPI Sleep(DWORD dwMilliseconds)
  • 参数:
    • dwMilliseconds:休眠时长,单位为毫秒,1s = 1000ms;

线程挂起,恢复代码演示:

#include <Windows.h>
#include <iostream>
using namespace std;
DWORD WINAPI ThreadFunc(LPVOID IpThreadParame); // 线程处理函数,_stdcall 代表window标准调用函数

int main() {
   
	std::cout << "主线程开始!" << std::endl;

	DWORD pThreadId = 0; // 定义线程id
	
	char cStr[] = "heallo";
	HANDLE hThread = CreateThread(NULL, 0, ThreadFunc, cStr, 0, &pThreadId);

	if (!hThread) {
   
		std::cout << "线程创建失败:" << GetLastError() << std::endl;
	}
	else {
   
		std::cout << "线程的句柄:" << hThread << std::endl;
		std::cout << "线程的ID:" << pThreadId << std::endl;
	}
	std::cout << "主线程id:" << GetCurrentThreadId() << std::endl;
	getchar();
	SuspendThread(hThread); // 挂起线程,必须保证其中的句柄没有被关闭。
	getchar();
	ResumeThread(hThread);
	getchar();
	std::cout << "主线程结束!" << std::endl;
	return 0;
}

DWORD WINAPI ThreadFunc(LPVOID IpThreadParame)
{
   
	std::cout << "子线程开始!" << std::endl;
	char* str = (char*)IpThreadParame;
	int iCount = 0;
	while (++iCount < 6) {
   
		std::cout << "线程处理函数中:" << str << std::endl;
		std::cout << "子线程的ID:" << GetCurrentThreadId() << std::endl;
		Sleep(1000);
	}

	std::cout << "子线程结束!" << std::endl;
	return 0;
}

线程挂起,恢复代码演示:

2.7 等待一个内核对象变为已通知状态:WaitForSingleObject

  • 函数原型:DWORD WaitForSingleObject(
               HANDLE   hObject,   //指明一个内核对象句柄
               DWORD  dwMilliseconds,  //等待时间
           );
  • 参数:
    • hObject:指明一个内核对象句柄
    • dwMilliseconds://等待时间
  • 说明:
    • 该函数需要传递一个内核对象句柄,如果该内核对象处于未通知状态,则该函数导致线程进入阻塞状态;如果该内核对象处于已通知状态,则该状态立即返回WAIT_OBJECT_0。第二个参数指明要等待的时间(毫秒),INFINITE表示无限等待,如果第二个参数为0,那么函数立即返回。如果等待超时,该函数返WAIT_TIMEOUT。如果该函数失败,返回WAIT_FAILED。
      • 未通知状态:该内核句柄关联的线程还未结束,未退出。
      • 已通知状态: 该函数句柄已经完成。

等待对象变为已通知状态:

#include <Windows.h>
#include <iostream>
#include <stdio.h>

DWORD WINAPI ThreadFunc(LPVOID IpThreadParame); // 线程处理函数,_stdcall 代表window标准调用函数

int main() {
   
	std::cout << "主线程开始!" << std::endl;

	DWORD pThreadId = 0; // 定义线程id
	
	char cStr[] = "heallo";
	HANDLE hThread = CreateThread(NULL, 0, ThreadFunc, cStr, 0, &pThreadId);

	if (!hThread) {
   
		std::cout << "线程创建失败:" << GetLastError() << std::endl;
	}
	else {
   
		std::cout << "线程的句柄:" << hThread << std::endl;
		std::cout << "线程的ID:" << pThreadId << std::endl;
	}
	std::cout << "主线程id:" << GetCurrentThreadId() << std::endl;
	
	if (hThread != NULL) {
   
		WaitForSingleObject(hThread, INFINITE);
	}
	
	std::cout << "主线程结束!" << std::endl;
	return 0;
}

DWORD WINAPI ThreadFunc(LPVOID IpThreadParame)
{
   
	std::cout << "子线程开始!" << std::endl;
	char* str = (char*)IpThreadParame;
	int iCount = 0;
	while (++iCount < 6) {
   
		std::cout << "线程处理函数中:" << str << std::endl;
		std::cout << "子线程的ID:" << GetCurrentThreadId() << std::endl;
		printf("第%d次打印hello!\n\n", iCount);
		Sleep(1000);
	}

	std::cout << "子线程结束!" << std::endl;
	return 0;
}

等待对象变为已通知状态结果:

可以发现通过WaitForSingleObject函数确保子线程结束后,主线程才结束。

2.8 终止线程一(建议避免使用):ExitThread

  • 函数原型:VOID ExitThread(DWORD dwExitCode);
  • 参数:
    • dwExitCode:退出码,根据自己的需求定义;
  • 作用:
    • 可以让线程调用ExitThread函数,以便强制线程终止运行:使用该方法不会调用线程函数的return语句,所以就不会调用线程函数作用域内申请的类对象的析构函数,会造成内存泄漏。

ExitThread案例演示:

#include <Windows.h>
#include <iostream>
#include <stdio.h>

DWORD WINAPI ThreadFunc(LPVOID IpThreadParame); // 线程处理函数,_stdcall 代表window标准调用函数

int main() {
   
	std::cout << "主线程开始!" << std::endl;

	DWORD pThreadId = 0; // 定义线程id
	
	char cStr[] = "heallo";
	HANDLE hThread = CreateThread(NULL, 0, ThreadFunc, cStr, 0, &pThreadId);

	if (!hThread) {
   
		std::cout << "线程创建失败:" << GetLastError() << std::endl;
	}
	else {
   
		std::cout << "线程的句柄:" << hThread << std::endl;
		std::cout << "线程的ID:" << pThreadId << std::endl;
	}
	std::cout << "主线程id:" << GetCurrentThreadId() << std::endl;
	
	if (hThread != NULL) {
   
		WaitForSingleObject(hThread, INFINITE);
	}
	
	std::cout << "主线程结束!" << std::endl;
	return 0;
}

DWORD WINAPI ThreadFunc(LPVOID IpThreadParame)
{
   
	std::cout << "子线程开始!" << std::endl;
	char* str = (char*)IpThreadParame;
	int iCount = 0;
	while (++iCount < 6) {
   
		std::cout << "线程处理函数中:" << str << std::endl;
		std::cout << "子线程的ID:" << GetCurrentThreadId() << std::endl;
		printf("第%d次打印hello!\n\n", iCount);
		Sleep(1000);

		if (3 == iCount) {
   
			ExitThread(666);
		}
	}

	std::cout << "子线程结束!" << std::endl;
	return 0;
}

ExitThread案例结果:

可以发现子线程执行3次后,终止了进程。

2.9 终止线程一(必须避免使用):TerminateThreadThread

  • 函数原型:BOOL TerminateThreadThread(HANDLE hThread, DWORD dwExitCode);
  • 参数:
    • hThread:被终止线程的句柄;
    • dwExitCode:退出码,根据自己的需求定义;
  • 作用:
    • 可以让线程调用ExitThread函数,以便强制线程终止运行:使用该方法不会调用线程函数的return语句,所以就不会调用线程函数作用域内申请的类对象的析构函数,会造成内存泄漏。
  • 返回值:
    • 成功返回非0,失败返回0,调用GetLastError()获知原因。

2.10 获取进程结束码:GetExitCodeThread

  • 函数原型:BOOL GetExitCodeThread(HANDLE hThread, LPDWORD IpExitCode);
  • 参数:
    • hThread:被终止线程的句柄;
    • IpExitCode:指向一个DWORD,用于接收结束代码(exit code);
  • 注意:
    • 在调用GetExitCodeThread()之前,请注意不要调用CloseHandle关闭掉线程句柄。
    • GetExitCodeThread()可以在调用WaitForSingleObject()等待线程结束之后调用。
  • 返回值:
    • 成功返回TRUE,失败返回FALSE,调用GetLastError()获知原因。如果线程已结束,那么线程的结束码会被放在IpExitCode参数中带回来。如果线程尚未结束,IpExitCode参数带回的值是STILL_ACTIVE。

2.11 等待多个内核对象变为已通知状态:WaitForMultipleObjects

  • 函数原型:DWORD WaitForMultipleObjects(
               DWORD   dwCount,   // 等待的内核对象的个数
               CONST HANDLE*  phObjects,  // 一个存放被等待的内核对象句柄的数组
               BOOL  bWaitAll,  // 是否等待所有内核对象为已通知状态才返回
               DWORD dwMilliseconds,  // 等待时间
           );
  • 参数:
    • dwCount:等待的内核对象的个数;
    • phObjects:一个存放被等待的内核对象句柄的数组;
    • bWaitAll:是否等待所有内核对象为已通知状态才返回 ;
    • dwMilliseconds:等待时间;
  • 注意:
    • 该函数的第一个参数指明等待的内核对象的个数,可以是0到MAXIMUM_WAIT_OBJECTS(64)中的一个值。
    • bWaitAll参数如果为TRUE,则只有当等待的所有内核对象为已通知状态时函数才返回,如果为FALSE,则只要一个内核对象为已通知状态,则该函数回。
  • 返回值:
    • 成功返回WAIT_OBJECT_0,失败返回WAIT_FAILED,如果超时返回WAIT_TIMEOUT。如果bWaitAll为FALSE,函数成功则返回数组的索引指明是哪个内核对象收到的通知。

等待多个内核对象变为已通知状态案例:

#include <Windows.h>
#include <iostream>
#include <stdio.h>

DWORD WINAPI ThreadFunc(LPVOID IpThreadParame); // 线程处理函数,_stdcall 代表window标准调用函数

int main() {
   
	std::cout << "主线程开始!" << std::endl;

	DWORD pThreadId = 0; // 定义线程id
	
	HANDLE hThread1 = CreateThread(NULL, 0, ThreadFunc, (LPVOID) "A", 0, &pThreadId);
	HANDLE hThread2 = CreateThread(NULL, 0, ThreadFunc, (LPVOID) "B", 0, &pThreadId);
	HANDLE hThread3 = CreateThread(NULL, 0, ThreadFunc, (LPVOID) "C", 0, &pThreadId);

	std::cout << "主线程id:" << GetCurrentThreadId() << std::endl;
	
	HANDLE hThread[] = {
    hThread1, hThread2,hThread3 };
	DWORD ret = WaitForMultipleObjects(3, hThread, true, INFINITE); // 等待所有进程结束
	printf("%d!\n", ret);
	
	std::cout << "主线程结束!" << std::endl;
	return 0;
}

DWORD WINAPI ThreadFunc(LPVOID IpThreadParame)
{
   
	std::cout << "子线程开始!" << std::endl;
	char* name = (char*)IpThreadParame;
	if (strcmp(name, "A") == 0) {
   
		Sleep(10000);
	}
	else if (strcmp(name, "B") == 0) {
   
		Sleep(3000);
	}
	else if(strcmp(name, "C") == 0) {
   
		Sleep(8000);
	}
	printf("%s!\n", name);
	std::cout << "子线程结束!" << std::endl;
	return 0;
}

等待多个内核对象变为已通知状态案例结果:

2.12 运行期库函数创建线程:_beginthread

  • 头文件:

    • #include <process.h>
  • 函数原型:uintptr_t _beginthread(
               void (*start_address)(void*),
               unsigned stack_size,
               void *arglist,
            );

  • 参数:

    • start_address:线程函数的入口地址。对于_beginthread,线程函数的调用约定是_cdecl;
    • stack_size:堆栈大小,设置0为系统默认值;
    • arglist:传递给线程的参数列表,无参数时为NULL;
  • 返回值:

    • 成功返回新创建的线程的句柄,需reinterpret_cast<HANDLE>强制转换,但如果新创建的线程退出的速度太快,可能无法返回一个有效句柄。失败返回-1,比如资源不足或者堆栈大小不正确,errno设置为EINVAL。
  • 说明:

    • CreateThread函数是用来创建线程的Windows函数。不过如果你正在编写C/C++代码,绝不应该调用CreateThread。相反,应该使用Visual C++运行期库函数_beginthread.
    • 在CreateThread API创建的线程中使用sprintf,malloc,strcat等涉及CRT存储的堆操作的CRT库函数是很危险的,容易造成线程的意外中止。而使用_beginthread和_beginthreadex创建的线程中可以安全的使用CRT函数,但是必须在线程结束的时候相应调用_endthread或_endthreadex。
    • 如果在线程函数中进行以下操作,你就应该使用_beginthread和_endthread:
      • 使用malloc()和free(),或是new和delete;
      • 使用stdio.h或io.h里面声明的任何函数;
      • 使用浮点变量或浮点运算函数;
      • 调用任何一个使用静态缓存区的runtime函数,比如asctime(), strtok()或rand();

2.13 运行期库函数终止线程:_endthread

  • 头文件:
    • #include <process.h>
  • 函数原型:void _endthread(void);
  • 说明:
    • _endthread终止由——beginthread创建的线程,对于使用Libcmt.lib链接的可执行文件,不要调用Win32API的ExitThread,它无法释放已分配的资源,而_endthread回收线程资源后调用ExitThread。_endthread自动关闭线程句柄。

_beginthread创建,_endthread关闭线程案例代码:

#include <process.h>
#include <stdio.h>
#include <Windows.h> // HANDLE位于该头文件下。

void ThreadFunc(void* parame) {
   
	printf("子线程开始\n");
	char* p = (char*)parame;
	int n = 0;
	while (++n < 6) {
   
		Sleep(1000);
		printf("第%d次打印,%s\n", n, p);
		if (3 == n) {
   
			_endthread();
		}
	}
	printf("子线程结束\n");
}

int main() {
   
	printf("主线程开始\n");
	HANDLE hThread = (HANDLE)_beginthread(ThreadFunc, 0,(void*) "hello");
	WaitForSingleObject(hThread, INFINITE);
	printf("主线程关闭\n");
	return 0;
}

_beginthread创建,_endthread关闭线程案例代码结果:


三、C++多线程同步

1. 问题引出:多线程模拟火车站售票

  • 如火车站有100张票,有2个售票口售票,采用多线程模拟该售票过程。

模拟售票过程代码如下:

#include <process.h>
#include <Windows.h>
#include <stdio.h>

int tickets = 100;

void hShellThread1(void* param) {
   
	char* name = (char*)param;
	while (tickets > 0) {
   
		Sleep(1000);
		printf("窗口 %s 售出车票%d\n", name, tickets--);
		
	}
}

void hShellThread2(void* param) {
   
	char* name = (char*)param;
	while (tickets > 0) {
   
		Sleep(1000);
		printf("窗口 %s 售出车票%d\n", name, tickets--);
	}
}

int main01() {
   
	
	HANDLE hThread1 = (HANDLE)_beginthread(hShellThread1, 0, (void*)"A"); 
	HANDLE hThread2 = (HANDLE)_beginthread(hShellThread1, 0, (void*)"B");
	HANDLE hThreads[] = {
    hThread1, hThread2 };
	WaitForMultipleObjects(2, hThreads, true, INFINITE);
	return 0;
}

int main() {
   
	 main01();
	return 0;
}

模拟售票过程代码结果:

可以发现出现了售出车票0的情况。这是由于多线程可能导致最后AB都进入了Sleep语句中,此时过了判断了,所以会出现输出为0的情况。

2. 多线程的同步和互斥

2.1 常见的概念介绍

  • 异步:如100米赛跑,各跑各的。
  • 同步:如接力赛,必须接棒后才能跑。也就是说一个线程需要等待另外一个线程的到来。
  • 互斥:如一个独木桥,一次只能过一个。

2.2 在WIN32中同步机制主要的方式

2.2.1 用户模式下的方法
2.2.1.1 临界区
  • 临界区: 通过对多线程串行化来访问公共资源或一段代码,速度快,适合控制数据访问。每次只允许一个线程进入临界区,进入后不允许其它进程进入,多个线程必须互斥访问该区域。
  • 例子:比如一个电话亭,同时只能有一位用户使用。
  • 临界区结构对象:
    • CRITICAL_SECTION Section;
    • 临界区域使用时以CRITICAL_SECTION结构对象保护共享资源,如果有多个线程同时访问临界区,那么在有一个线程进入后,其它试图访问此临界区的线程将被挂起,并一直持续到进入临界区的线程离开。临界区在被释放后,其它线程可以继续抢占,并以此达到用原子方式操作贡献资源的目的。
  • 初始化临界区
    • InitializeCriticalSection(&Section);
  • 进入临界区
    • EnterCriticalSection(&Section);
  • 离开临界区
    • LeaveCriticalSection(&Section);
  • 尝试进入临界区
    • if(TryEnterCriticalSection(&Section)){
       // 临界区
       LeaveCriticalSection(&Section);
      }
    • 如果EnterCriticalSection的临界区资源已经被占用,会使调用线程置于阻塞状态,会导致线程很长时间不会被调用。
    • 然而TryEnterCriticalSection函数绝不允许调用线程进入阻塞状态。他的返回值能够指明调用线程是否能够获得对资源的访问权。换而言之,即TryEnterCriticalSection发现该临界资源已经被另外一个线程访问,它就返回FALSE,否则返回TRUE。运用这个函数,线程能够迅速的查看它是否可以访问某个共享资源,如果不能访问,那么它可以继续执行其它操作,而不必等待。
  • 删除临界区
    • DeleteCriticalSection(&Section);

线程临界区案例代码(采用EnterCriticalSection()):

#include <process.h>
#include <Windows.h>
#include <stdio.h>

int tickets = 100;

CRITICAL_SECTION Section; // 临界区CS1

void ShellThread1(void* param) {
   
	char* name = (char*)param;
	while (tickets > 0) {
   
		EnterCriticalSection(&Section);
		if (tickets > 0) {
   
			Sleep(1000);
			printf("窗口%s售出第%d张票\n", name, tickets--);
		}
		LeaveCriticalSection(&Section);
	}
}

void ShellThread2(void* param) {
   
	char* name = (char*)param;
	while (tickets > 0) {
   
		EnterCriticalSection(&Section);
		if (tickets > 0) {
   
			Sleep(1000);
			printf("窗口%s售出第%d张票\n", name, tickets--);
		}
		LeaveCriticalSection(&Section);
	}
}

int main() {
   
	InitializeCriticalSection(&Section);
	printf("开始售票\n");
	HANDLE WindowThread1 = (HANDLE)_beginthread(ShellThread1, 0, (void*)"A");
	HANDLE WindowThread2 = (HANDLE)_beginthread(ShellThread2, 0, (void*)"B");

	HANDLE Windows[] = {
    WindowThread1, WindowThread2 };
	WaitForMultipleObjects(2, Windows, true, INFINITE);
	DeleteCriticalSection(&Section);
	printf("结束售票\n");
	return 0;
}

线程临界区案例代码效果(采用EnterCriticalSection()):

可以发现解决了售出第0票的问题。
线程临界区案例代码(采用TryEnterCriticalSection()):

#include <process.h>
#include <Windows.h>
#include <stdio.h>

int tickets = 100;

CRITICAL_SECTION Section; // 临界区CS1

void ShellThread1(void* param) {
   
	char* name = (char*)param;
	while (tickets > 0) {
   
		if (TryEnterCriticalSection(&Section)) {
   
			if (tickets > 0) {
    // 二次验证必须需要
				Sleep(1000);
				printf("窗口%s售出第%d张票\n", name, tickets--);
			}
			LeaveCriticalSection(&Section);
		}
	}
}

void ShellThread2(void* param) {
   
	char* name = (char*)param;
	while (tickets > 0) {
   
		if (TryEnterCriticalSection(&Section)) {
   
			if (tickets > 0) {
   
				Sleep(1000);
				printf("窗口%s售出第%d张票\n", name, tickets--);
			}
			LeaveCriticalSection(&Section);
		}
	}
}

int main() {
   
	InitializeCriticalSection(&Section);
	printf("开始售票\n");
	HANDLE WindowThread1 = (HANDLE)_beginthread(ShellThread1, 0, (void*)"A");
	HANDLE WindowThread2 = (HANDLE)_beginthread(ShellThread2, 0, (void*)"B");

	HANDLE Windows[] = {
    WindowThread1, WindowThread2 };
	WaitForMultipleObjects(2, Windows, true, INFINITE);
	DeleteCriticalSection(&Section);
	printf("结束售票\n");
	return 0;
}

线程临界区案例代码效果(采用EnterCriticalSection()):

可以发现解决了售出第0票的问题。

2.2.1.2 原子操作
2.2.2 内核模式下的方法
2.2.2.1 信号量
  • 为控制一个具有有限数量用户资源而设计。
2.2.2.1 互斥量
  • 为协调共同对一个共享资源的的单独访问而设计。
2.2.2.1 事件

2.3 线程死锁

2.3.1 线程死锁相关概念
  • 死锁:指多个线程因为竞争资源而造成的一种僵局(互相等待),若无外力的作用,这些线程都将无法向前推进。
2.3.2 线程死锁产生的必要条件
  • 互斥条件:一个资源每次只能被一个进程使用。
  • 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
  • 不可剥夺条件:进程已获得的资源,在未使用2完之前,不能强行剥夺。
  • 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源的关系。

线程死锁的案例演示:

#include <process.h>
#include <Windows.h>
#include <stdio.h>
CRITICAL_SECTION CS1; // 临界区CS1
CRITICAL_SECTION CS2; // 临界区CS2

void ThreadFun1(void* param) {
   
	char* name = (char*)param;
	printf("子线程%s开始!\n", name);
	EnterCriticalSection(&CS1);
	printf("子线程%s占用CS1资源,想请求CS2资源!\n", name);
	Sleep(1000);
	EnterCriticalSection(&CS2);
	printf("子线程%s占用CS2资源!\n", name);
	Sleep(2000);
	LeaveCriticalSection(&CS2);

	LeaveCriticalSection(&CS1);

	printf("子线程%s结束! \n", name);
}

void ThreadFun2(void* param) {
   
	char* name = (char*)param;
	printf("子线程%s开始!\n", name);
	EnterCriticalSection(&CS2);
	printf("子线程%s占用CS2资源,想请求CS1资源!\n", name);
	Sleep(2000);
	EnterCriticalSection(&CS1);
	printf("子线程%s占用CS2资源!\n", name);
	Sleep(2000);
	LeaveCriticalSection(&CS1);
	LeaveCriticalSection(&CS2);
	printf("子线程%s结束! \n", name);
}

int main() {
   
	InitializeCriticalSection(&CS1);
	InitializeCriticalSection(&CS2);
	printf("主线程开始!\n");

	HANDLE Thread1 = (HANDLE)_beginthread(ThreadFun1, 0, (void*)"A");
	
	HANDLE Thread2 = (HANDLE)_beginthread(ThreadFun2, 0, (void*)"B");
	
	HANDLE Threads[] = {
    Thread1, Thread2 };
	WaitForMultipleObjects(2, Threads, true, INFINITE);

	DeleteCriticalSection(&CS1);
	DeleteCriticalSection(&CS2);

	printf("主线程结束!\n");
	return 0;
}

线程死锁的案例效果

由图可以发现,线程A和线程B进入了相互无线等待的环境中,导致程序的无法进行。

2.3.3 解决死锁问题的经典算法——银行家算法

转载:https://blog.csdn.net/qq_29086043/article/details/116195775
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场