10001.C++学习笔记——Windows环境下多线程编程
- 一、什么是多线程
- 二、C++多线程编程
-
- 1. 头文件
- 2. API函数介绍
-
- 2.1 创建线程函数:CreateThread
- 2.2 获取当前线程ID函数:GetCurrentThread
- 2.3 关闭线程句柄:CloseHandle
- 2.4 挂起指定线程:SuspendThread
- 2.5 恢复被挂起的线程的执行:ResumeThread
- 2.6 休眠线程的执行:Sleep
- 2.7 等待一个内核对象变为已通知状态:WaitForSingleObject
- 2.8 终止线程一(建议避免使用):ExitThread
- 2.9 终止线程一(必须避免使用):TerminateThreadThread
- 2.10 获取进程结束码:GetExitCodeThread
- 2.11 等待多个内核对象变为已通知状态:WaitForMultipleObjects
- 2.12 运行期库函数创建线程:_beginthread
- 2.13 运行期库函数终止线程:_endthread
- 三、C++多线程同步
一、什么是多线程
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。
- 未通知状态:该内核句柄关联的线程还未结束,未退出。
- 已通知状态: 该函数句柄已经完成。
- 该函数需要传递一个内核对象句柄,如果该内核对象处于未通知状态,则该函数导致线程进入阻塞状态;如果该内核对象处于已通知状态,则该状态立即返回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。运用这个函数,线程能够迅速的查看它是否可以访问某个共享资源,如果不能访问,那么它可以继续执行其它操作,而不必等待。
- if(TryEnterCriticalSection(&Section)){
- 删除临界区
- 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