Winodws核心编程 多线程
目录
一、基本概念
二、线程创建函数
三、Windows内核对象与句柄
四、简单的多线程案例
五、线程同步 - 互斥对象
六、多线程实现群聊的服务端和客户端
七、线程同步 - 事件对象
八、事件对象 与 互斥对象区别
九、线程同步 - 信号量
十、线程同步 - 关键代码段
十一、线程死锁
十二、四种线程同步方式总结
一、基本概念
线程是在进程中产生的一个执行单元,是 CPU 调度和分配的最小单元,其在同一个进程中与其他线程并行运行,他们可以共享进程内的资源,比如内存、地址空间、打开的文件等等。
进程:正在运行的程序 --- 狭义,是处于执行期的程序以及它所管理的资源(如打开的文件、挂起的信号、进程状态、地址空间等等)的总称,从操作系统核心角度来说,进程是操作系统调度除CPU 时间片外进行的资源分配和保护的基本单位,它有一个独立的虚拟地址空间,用来容纳进程映像(如与进程关联的程序与数据),并以进程为单位对各种资源实施保护,如受保护地访问处理器、文件、外部设备及其他进程(进程间通信)
线程是 CPU 调度和分派的基本单位;进程是分配资源的基本单位。
如何理解进程与线程的关系?
计算机有很多资源组成,比如 CPU、内存、磁盘、鼠标、键盘等,就像一个工厂由电力系统、作业车间、仓库、管理办公室和工人组成,假定工厂的电力有限,一次只能供给一个或少量几个车间使用。也就是说,一部分车间开工的时候,其他车间都必须停工。背后的含义就是,单个CPU 一次只能运行一个任务,多个 CPU 能够运行少量任务。线程就好比车间里的工人。一个进程可以包括多个线程,他们协同完成某一个任务。
为什么要使用多线程?
- 避免阻塞:单个进程只有一个主线程,当主线程阻塞的时候,整个进程也就阻塞了,无法再去做其它的一些功能了。
- 避免CPU空转:应用程序经常会涉及到 RPC,数据库访问,磁盘 IO 等操作,这些操作的速度比CPU 慢很多,而在等待这些响应时,CPU 却不能去处理新的请求,导致这种单线程的应用程序性能很差。 cpu 》》 内存 》》 磁盘
- 提升效率:一个进程要独立拥有 4GB 的虚拟地址空间,而多个线程可以共享同一地址空间,线程的切换比进程的切换要快得多。
进程控件与线程空间:
二、线程创建函数
_beginthreadex函数,用于在 Windows 环境下创建一个新的线程。它的声明通常在 process.h 或 intrin.h 头文件中,函数原型为:
_ACRTIMP uintptr_t __cdecl _beginthreadex(_In_opt_ void* _Security,_In_ unsigned _StackSize,_In_ _beginthreadex_proc_type _StartAddress,_In_opt_ void* _ArgList,_In_ unsigned _InitFlag,_Out_opt_ unsigned* _ThrdAddr);
- _ACRTIMP 是 Microsoft Visual C++ 中的一个宏,用于标识函数或变量的导出属性。在该宏的作用下,函数或变量将被声明为在动态链接库(DLL)中导出,以便其他模块可以访问它们。
- uintptr_t 是 C/C++ 中的数据类型,表示无符号整数类型,其大小足以容纳指针的位数。在不同的编译器和平台上,其大小可能会有所不同,但通常被设计为足够大以容纳指针值。
- __cdecl 是一种调用约定(calling convention),它规定了函数参数的传递顺序、堆栈清理责任等规则。在 __cdecl 调用约定中,函数参数从右向左入栈,由调用者负责清理堆栈空间。
_ACRTIMP uintptr_t __cdecl 表示一个被导出的函数,其返回类型为 uintptr_t(无符号整数类型),采用 cdecl 调用约定。
参数说明如下:
- security:指向 SECURITY_ATTRIBUTES 结构的指针,用于指定新线程的安全特性,一般可以传入 NULL。
- stack_size:指定新线程的堆栈大小,一般可以传入 0 表示使用默认大小。
- start_address:指向线程函数的指针,即新线程将要执行的函数。
- arglist:传递给线程函数的参数。
- initflag:控制新线程的初始状态,一般可以传入 0。
- thrdaddr:指向一个无符号整数的指针,用于接收新线程的标识符。
代码案例
#include <stdio.h>
#include <windows.h>
#include <process.h>//Bingo老师 提了一个需求 : 打印
//每隔3秒叫焦老师俯卧撑 持续20次
//每隔4秒钟冯老师做一次甩头发 持续30次
//每隔2秒钟叫Bingo老师唱歌 持续50次
//unsigned WINAPI thread_main_hong(void* arg)
{int cnt = *((int*)arg);for (int i = 0; i < cnt; i++){printf("fuwocheng");Sleep(3000);}return 0;
}unsigned WINAPI thread_main_ming(void* arg)
{int cnt = *((int*)arg);for (int i = 0; i < cnt; i++){printf("shuaitoufa");Sleep(4000);}return 0;
}unsigned WINAPI thread_main_wang(void* arg)
{int cnt = *((int*)arg);for (int i = 0; i < cnt; i++){printf("sing");Sleep(2000);}return 0;
}int main()
{int xiaohong = 20, xiaoming = 30, laowang = 50;unsigned int hong_id, ming_id, wang_id;// 三个线程同时启动,不分先后顺序,谁先给出结果无法确定_beginthreadex(NULL, 0, &thread_main_hong, (void*)&xiaohong, 0, &hong_id);_beginthreadex(NULL, 0, &thread_main_ming, (void*)&xiaoming, 0, &ming_id);_beginthreadex(NULL, 0, &thread_main_wang, (void*)&laowang, 0, &wang_id);// 一旦主线程结束,子线程同样结束Sleep(100000);system("pause");return 0;
}
线程的运行顺序不可预测
CreateThread函数,是一种微软在 Windows API 中提供了建立新的线程的函数,该函数在主线程的基础上创建一个新线程。线程终止运行后,线程对象仍然在系统中,必须通过CloseHandle 函数来关闭该线程对象。
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);
参数说明如下:
- 第一个参数 lpThreadAttributes 表示线程内核对象的安全属性,一般传入NULL 表示使用默认设置。
- 第二个参数 dwStackSize 表示线程栈空间大小。传入0 表示使用默认大小(1MB)。
- 第三个参数 lpStartAddress 表示新线程所执行的线程函数地址,多个线程可以使用同一个函数地址。
- 第四个参数 lpParameter 是传给线程函数的参数。
- 第五个参数 dwCreationFlags 指定额外的标志来控制线程的创建,为0表示线程创建之后立即就可以进行调度,如果为 CREATE_SUSPENDED则表示线程创建后暂停运行,这样它就无法调度,直到调用ResumeThread()。
- 第六个参数 lpThreadId 将返回线程的 ID 号,传入NULL 表示不需要返回该线程 ID 号
需要注意的是,在使用完线程句柄后,需要通过 CloseHandle 函数关闭线程句柄,以释放系统资源。
代码案例
#include <stdio.h>
#include <windows.h>
#include <process.h>DWORD WINAPI ThreadFun(LPVOID p)
{int iMym = *((int*)p);printf("我是子线程,PID = %d,iMym = %d\n", GetCurrentThreadId(), iMym);return 0;
}int main()
{printf("main begin\n");HANDLE hThread;DWORD dwThreadID;int m = 100;hThread = CreateThread(NULL, 0, ThreadFun, &m, 0, &dwThreadID);printf("我是主线程,PID = %d\n", GetCurrentThreadId());CloseHandle(hThread);Sleep(2000);system("pause");return 0;
}
三、Windows内核对象与句柄
1. 内核对象:Windows 中每个内核对象都只是一个内存块,它由操作系统内核分配,并只能由操作系统内核进行访问,应用程序不能在内存中定位这些数据结构并直接更改其内容。这个内存块是一个数据结构,其成员维护着与对象相关的信息。少数成员(安全描述符和使用计数)是所有内核对象都有的,但大多数成员都是不同类型对象特有的。内核对象只是一个内存块,这块内存位于操作系统内核的地址空间,内存块中存放一个数据结构(此数据结构的成员有如:安全描述符、使用计数等)。
常见的内核对象 : 进程、线程、文件,存取符号对象、事件对象、文件对象、作业对象、互斥对象、管道对象、等待计时器对象,邮件槽对象,信号对象
内核对象:为了管理线程/文件等资源而由操作系统创建的数据块。
其创建的所有者肯定是操作系统。
2. 内核对象的使用技术与生命期:内核对象的所有者是操作系统内核,而非进程。换言之也就是说当进程退出,内核对象不一定会销毁。操作系统内核通过内核对象的使用计数,知道当前有多少个进程正在使用一个特定的内核对象。初次创建内核对象,使用计数为 1。当另一个进程获得该内核对象的访问权之后,使用计数加1。如果内核对象的使用计数递减为 0,操作系统内核就会销毁该内核对象。也就是说内核对象在当前进程中创建,但是当前进程退出时,内核对象有可能被另外一个进程访问。这时,进程退出只会减少当前进程对引用的所有内核对象的使用计数,而不会减少其他进程对内核对象的使用计数(即使该内核对象由当前进程创建)。那么内核对象的使用计数未递减为 0,操作系统内核不会销毁该内核对象。
(1) 进程 1 退出,2 不退出时。内核对象 A,B 的引用计数减为 0,被操作系统内核销毁,而进程1只减少自身对 C,D 的引用计数,不会影响进程 2 对 C,D 的引用计数,此时 C,D 引用计数不为0,不会被销毁。
(2) 进程 2 退出,1 不退出时。进程 2 减少自身对 C,D 的引用计数,不会影响进程1,故A,B,C,D都不会被销毁
(3) 进程 1,2 均退出时,只要 ABCD 不被别的进程使用,内核对象 A,B,C,D 的引用计数均递减为0,被内核销毁
(4) 进程 1 和 2 均为退出时,内核对象 A,B,C,D 的引用计数只要有一个递减为0,那么递减为0的内核对象便被内核销毁
3. 操作内核对象:Windows 提供了一组函数进行操作内核对象。成功调用一个创建内核对象的函数后,会返回一个句柄,它表示了所创建的内核对象,可由进程中的任何线程使用。在 32 位进程中,句柄是一个32 位值,在 64 位进程中句柄是一个 64 位值。我们可以使用唯一标识内核对象的句柄,调用内核操作函数对内核对象进行操作。
4. 内核对象和其他类型的对象:Windows 进程中除了内核对象还有其他类型的对象,比如窗口,菜单,字体等,这些属于用户对象和 GDI 对象。要区分内核对象与非内核对象,最简单的方式就是查看创建这个对象的函数,几乎所有创建内核对象的函数都有一个允许我们指定安全属性的参数。一个对象是不是内核对象,通常可以看创建此对象 API 的参数中是否需要:PSECURITY_ATTRIBUTES 类型的参数。
5. 每个进程中有一个句柄表(handle table),这个句柄表仅供内核对象使用,如下图:
深入理解对内核对象的引用:
hThread = CreateThread(... , &threadId);
当调用了 CreateThread CreateFile 等创建内核对象的函数后,就是相当于操作系统多了一个内存块,这个内存块就是内核对象也是此时内核对象被创建,其数据结构中的引用计数初始为1(这样理解:只要内核对象被创建,其引用计数被初始化为 1),这里实则发生两件事:创建了一个内核对象和创建线程的函数打开(访问)了此对象,所以内核对象的引用计数加1,这时引用计数就为 2 了。
调用 API CreateThread 的时候,不仅仅是创建了一个内核对象,引用计数+1,还打开了内核对象+1,所以引用计数变为 2
当调用 CloseHandle(hThread); 时发生这样的事情:系统通过hThread 计算出此句柄在句柄表中的索引,然后把那一项处理后标注为空闲可用的项,内核对象的引用计数减 1 即此时此内核对象的引用计数为 1,之后这个线程句柄与创建时产生的内核对象已经没有任何关系了。不能通过 hThread 句柄去访问内核对象了
只有当内核对象的引用计数为 0 时,内核对象才会被销毁,而此时它的引用计数为 1,那它什么时候会被销毁? 当此线程结束的时候,它的引用计数再减 1 即为 0,内核对象被销毁。此时又有一个新问题产生:我们已经关闭了线程句柄,也就是这个线程句柄已经和内核对象没有瓜葛了,那么那个内核对象是怎么又可以和此线程联系起来了呢?其实是创建线程时产生的那个线程 ID,代码如下:
#include <stdio.h>
#include <windows.h>
#include <WinBase.h>DWORD WINAPI ThreadProc(LPVOID lpParameter)
{printf("I am comming...");while (1) {}return 0;
}int main()
{HANDLE hThread;HANDLE headle2;DWORD threadId;hThread = CreateThread(NULL, 0, ThreadProc, NULL, 0, &threadId);// CloseHandle(hThread); // 关闭了线程句柄headle2 = OpenThread(THREAD_QUERY_INFORMATION, FALSE, threadId);headle2 = OpenThread(THREAD_QUERY_INFORMATION, FALSE, threadId);headle2 = OpenThread(THREAD_QUERY_INFORMATION, FALSE, threadId);return 0;
}
四、简单的多线程案例
主线程和子线程的结束时间:main 函数返回后,整个进程终止,同时终止其包含的所有线程
#include <stdio.h>
#include <windows.h>
#include <process.h>unsigned WINAPI ThreadFunc(void* arg)
{int i;int cnt = *((int*)arg);for (i = 0; i < cnt; i++){Sleep(1000); puts("running thread");}return 0;
}int main(int argc, char* argv[])
{HANDLE hThread;unsigned threadID;int param = 5;hThread = (HANDLE)_beginthreadex(NULL, 0, &ThreadFunc, (void*)¶m, 0, &threadID);if (hThread == 0){puts("_beginthreadex() error");return -1;}Sleep(3000);puts("end of main");return 0;
}
等待一个/多个内核对象变为 signaled 状态
DWORD WINAPI WaitForSingleObject(_In_ HANDLE hHandle, // 指明一个内核对象的句柄_In_ DWORD dwMilliseconds // 等待时间);
这个函数的作用是等待一个指定的内核对象(如线程、进程、互斥体、事件等)进入 signaled 状态。参数说明如下:
- hHandle:要等待的内核对象的句柄。
- dwMilliseconds:等待的时间,以毫秒为单位。如果设置为 INFINITE(-1),则表示无限等待,直到对象进入 signaled 状态或出错。
WaitForSingleObject 函数的返回值表示函数调用的结果,可能的取值包括:
- WAIT_OBJECT_0:指定的对象已经进入 signaled 状态。
- WAIT_TIMEOUT:等待超时。
- WAIT_FAILED:函数调用失败。
在操作系统中,"signaled"(也称为"激发"或"触发")是指内核对象进入一种特殊状态的标识。当内核对象处于 signaled 状态时,它表示该对象的某种条件已经满足,可以进行相应的操作。
具体来说,对于不同类型的内核对象,其 signaled 状态的含义可能有所不同:
- 互斥体(Mutex):表示互斥体当前未被锁定,可以被当前线程或其他线程锁定。
- 事件(Event):表示事件已经触发,通常用于线程间的同步和通信。
- 信号量(Semaphore):表示信号量的计数值大于零,可以允许一定数量的线程访问某个资源。
- 定时器(Timer):表示定时器已经到期,可以执行相应的定时操作。
- 进程、线程等:表示进程或线程已经终止,可以进行后续处理。
通过使用 WaitForSingleObject 函数等待内核对象进入 signaled 状态,我们可以实现线程的同步、事件的触发等功能。一旦内核对象进入 signaled 状态,WaitForSingleObject 函数将返回,并且我们可以根据返回值做出相应的处理。
#include <stdio.h>
#include <windows.h>
#include <process.h>unsigned int __stdcall ThreadFun(LPVOID p)
{int cnt = *((int*)p);for (int i = 0; i < cnt; i++){Sleep(1000);puts("running thread");}return 0;
}int main()
{printf("main begin\n");int iParam = 5;unsigned int dwThreadID;DWORD wr;HANDLE hThread = (HANDLE)_beginthreadex(NULL, 0, ThreadFun, (void*)&iParam, 0, &dwThreadID);if (hThread == NULL){puts("_beginthreadex() error");return -1;}//printf("WaitForSingleObject begin\n");// 阻塞当前线程,直到指定的线程(通过 hThread 句柄表示)结束或终止。if ((wr = WaitForSingleObject(hThread, INFINITE)) == WAIT_FAILED) // INFINITE 是一个常量,表示无限等待时间。{puts("thread wait error");return -1;}printf("WaitForSingleObject end\n");printf("main end\n");system("pause");return 0;
}
DWORD WINAPI WaitForMultipleObjects(_In_ DWORD nCount, // 要监测的句柄的组的句柄的个数_In_reads_(nCount) CONST HANDLE* lpHandles, //要监测的句柄的组_In_ BOOL bWaitAll, // TRUE 等待所有的内核对象发出信号, FALSE 任意一个内核对象发出信号_In_ DWORD dwMilliseconds //等待时间
);
每个部分的含义:
DWORD
:这是返回类型,表示函数返回一个无符号双字(32 位)整数作为结果。WINAPI
:这是函数调用约定的一部分,指示函数使用标准的 Windows API 调用约定。_In_ DWORD nCount
:这是一个输入参数,表示要等待的内核对象的数量。_In_reads_(nCount) CONST HANDLE* lpHandles
:这是一个输入参数,表示一个指向HANDLE
数组的指针,这些HANDLE
表示了要等待的内核对象。_In_ BOOL bWaitAll
:这是一个输入参数,用于指定是等待所有对象还是任意一个对象。如果设为TRUE
,则表示只有当所有的内核对象都变为 signaled 状态时,函数才会返回;如果设为FALSE
,则表示只要有任何一个内核对象变为 signaled 状态,函数就会返回。_In_ DWORD dwMilliseconds
:这是一个输入参数,指定了等待的时间,以毫秒为单位。如果设为INFINITE
,则表示无限等待。
测试代码
#include <stdio.h>
#include <windows.h>
#include <process.h>
#define NUM_THREAD 50unsigned WINAPI threadInc(void* arg);
unsigned WINAPI threadDes(void* arg);long long num = 0;int main(int argc, char* argv[])
{HANDLE tHandles[NUM_THREAD];int i;printf("sizeof long long: %d \n", sizeof(long long));for (i = 0; i < NUM_THREAD; i++){if (i % 2)tHandles[i] = (HANDLE)_beginthreadex(NULL, 0, threadInc, NULL, 0, NULL);elsetHandles[i] = (HANDLE)_beginthreadex(NULL, 0, threadDes, NULL, 0, NULL);}WaitForMultipleObjects(NUM_THREAD, tHandles, TRUE, INFINITE);printf("result: %lld \n", num);return 0;
}unsigned WINAPI threadInc(void* arg)
{int i;for (i = 0; i < 500000; i++)num += 1;return 0;
}unsigned WINAPI threadDes(void* arg)
{int i;for (i = 0; i < 500000; i++)num -= 1;return 0;
}
这个是有问题的
五、线程同步 - 互斥对象
互斥对象(mutex)属于内核对象,它能够确保线程拥有对单个资源的互斥访问权。
互斥对象包含一个使用数量,一个线程 ID 和一个计数器。其中线程ID 用于标识系统中的哪个线程当前拥有互斥对象,计数器用于指明该线程拥有互斥对象的次数。
创建互斥对象:调用函数 CreateMutex。调用成功,该函数返回所创建的互斥对象的句柄。
请求互斥对象所有权:调用函数 WaitForSingleObject 函数。线程必须主动请求共享对象的所有权才能获得所有权。
释放指定互斥对象的所有权:调用 ReleaseMutex 函数。线程访问共享资源结束后,线程要主动释放对互斥对象的所有权,使该对象处于已通知状态。
CreateMutexW
是一个用于创建互斥对象(Mutex)的 Windows API 函数。让我解释一下每个参数的含义:
-
LPSECURITY_ATTRIBUTES lpMutexAttributes
:这是一个可选参数,用于指定安全描述符,控制新创建的互斥对象的安全性。如果不需要特殊的安全设置,可以将该参数设为NULL
。 -
BOOL bInitialOwner
:这是一个布尔值参数,用于指定初始所有权的状态。如果设为TRUE
,表示调用线程拥有互斥对象的初始所有权;如果设为FALSE
,则互斥对象没有初始所有权。 -
LPCWSTR lpName
:这是一个可选参数,用于指定互斥对象的名称。如果这个参数为NULL
,则创建一个匿名的互斥对象;如果指定了一个名称,系统会尝试使用这个名称来创建或打开一个已存在的同名互斥对象。
CreateMutexW
函数返回一个 HANDLE
类型的值,该值表示了新创建的互斥对象的句柄。这个句柄可以用于后续操作和控制互斥对象。
HANDLE WINAPI CreateMutexW(_In_opt_ LPSECURITY_ATTRIBUTES lpMutexAttributes, //指向安全属性_In_ BOOL bInitialOwner, //初始化互斥对象的所有者 TRUE 立即拥有互斥体_In_opt_ LPCWSTR lpName //指向互斥对象名的指针 L“Bingo”
);
测试代码
#include <stdio.h>
#include <windows.h>
#include <process.h>#define NUM_THREAD 50unsigned WINAPI threadInc(void* arg);
unsigned WINAPI threadDes(void* arg);long long num = 0;HANDLE hMutex;int main(int argc, char* argv[])
{HANDLE tHandles[NUM_THREAD];int i;hMutex = CreateMutex(NULL, FALSE, NULL);for (i = 0; i < NUM_THREAD; i++){if (i % 2)tHandles[i] = (HANDLE)_beginthreadex(NULL, 0, threadInc, NULL, 0, NULL);elsetHandles[i] = (HANDLE)_beginthreadex(NULL, 0, threadDes, NULL, 0, NULL);}WaitForMultipleObjects(NUM_THREAD, tHandles, TRUE, INFINITE);CloseHandle(hMutex);printf("result: %lld \n", num);return 0;
}unsigned WINAPI threadInc(void* arg)
{int i;// 这些线程需要抢到 hMutex 才能执行WaitForSingleObject(hMutex, INFINITE);for (i = 0; i < 500000; i++)num += 1;ReleaseMutex(hMutex);return 0;
}unsigned WINAPI threadDes(void* arg)
{int i;// 这些线程需要抢到 hMutex 才能执行WaitForSingleObject(hMutex, INFINITE);for (i = 0; i < 500000; i++)num -= 1;ReleaseMutex(hMutex);return 0;
}
WaitForSingleObject(hMutex, INFINITE); 等待一个互斥对象的状态变化。
WaitForSingleObject
是一个 Windows API 函数,用于等待一个内核对象的状态变化,直到对象进入 signaled 状态。hMutex
是一个互斥对象的句柄,表示要等待的对象。INFINITE
表示等待时间,这里设为INFINITE
,表示无限等待,直到互斥对象的状态发生变化。
在这行代码中,程序将会一直停在这里,直到 hMutex
表示的互斥对象进入了 signaled 状态,或者在发生错误时才会继续执行后面的代码。这种操作通常用于确保对共享资源的互斥访问,比如在多线程编程中控制对临界区的访问。
六、多线程实现群聊的服务端和客户端
服务端:使用数组维护通讯的线程,增加,删除操作有可能触及多线程操作同一资源,就有可能造成问题,需要加锁限制。
#include <WinSock2.h>
#include <iostream>
#include <windows.h>
#include <process.h>#pragma comment(lib, "ws2_32.lib")#define MAX_CLNT 256
#define MAX_BUF_SIZE 256SOCKET clntSocks[MAX_CLNT];HANDLE hMutex;int clntCnt = 0; //当前连接的数目void SendMsg(char* szMsg, int iLen)
{int i = 0;WaitForSingleObject(hMutex, INFINITE);for (i = 0; i < clntCnt; i++){send(clntSocks[i], szMsg, iLen, 0);}ReleaseMutex(hMutex);
}//处理客户端连接的函数
unsigned WINAPI HandleCln(void* arg)
{//1 接收传递过来的参数SOCKET hClntSock = *((SOCKET*)arg);int iLen = 0, i;char szMsg[MAX_BUF_SIZE] = { 0 };//2 进行数据的收发 循环接收//接收到客户端的数据// while ((iLen = recv(hClntSock, szMsg, sizeof(szMsg),0)) != 0)// { //收到的数据立马发给所有的客户端// SendMsg(szMsg, iLen);// }while (1){iLen = recv(hClntSock, szMsg, sizeof(szMsg), 0);if (iLen != -1){//收到的数据立马发给所有的客户端SendMsg(szMsg, iLen);}else{break;}}printf("此时连接数目为 %d\n", clntCnt);//3 某个连接断开,需要处理断开的连接 遍历WaitForSingleObject(hMutex, INFINITE);for (i = 0; i < clntCnt; i++){if (hClntSock == clntSocks[i]){//移位while (i++ < clntCnt){clntSocks[i] = clntSocks[i + 1];}break;}}clntCnt--; //当前连接数的一个自减printf("断开此时连接数目 %d", clntCnt);ReleaseMutex(hMutex);closesocket(hClntSock);return 0;
}int main()
{// 加载套接字库WORD wVersionRequested;WSADATA wsaData;int err;HANDLE hThread;wVersionRequested = MAKEWORD(1, 1);// 初始化套接字库err = WSAStartup(wVersionRequested, &wsaData);if (err != 0){return err;}if (LOBYTE(wsaData.wVersion) != 1 || HIBYTE(wsaData.wVersion) != 1){WSACleanup();return -1;}//创建一个互斥对象hMutex = CreateMutex(NULL, FALSE, NULL);// 新建套接字SOCKET sockSrv = socket(AF_INET, SOCK_STREAM, 0);SOCKADDR_IN addrSrv;addrSrv.sin_addr.S_un.S_addr = htonl(INADDR_ANY);addrSrv.sin_family = AF_INET;addrSrv.sin_port = htons(9190);// 绑定套接字到本地IP地址,端口号9190if (bind(sockSrv, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR)) == SOCKET_ERROR){printf("bind ERRORnum = %d\n", GetLastError());return -1;}// 开始监听if (listen(sockSrv, 5) == SOCKET_ERROR){printf("listen ERRORnum = %d\n", GetLastError());return -1;}printf("start listen\n");SOCKADDR_IN addrCli;int len = sizeof(SOCKADDR);while (1){// 接收客户连接 sockConn此时来的客户端连接SOCKET sockConn = accept(sockSrv, (SOCKADDR*)&addrCli, &len);//每来一个连接,服务端起一个线程(安排一个工人)维护客户端的连接//每来一个连接,全局数组应该加一个成员,最大连接数加1WaitForSingleObject(hMutex, INFINITE);clntSocks[clntCnt++] = sockConn;ReleaseMutex(hMutex);hThread = (HANDLE)_beginthreadex(NULL, 0, HandleCln, (void*)&sockConn, 0, NULL);printf("Connect client IP: %s \n", inet_ntoa(addrCli.sin_addr));printf("Connect client num: %d \n", clntCnt);}closesocket(sockSrv);WSACleanup();return 0;
}
客户端:收发消息需要启多线程
#include <WinSock2.h>
#include <iostream>
#include <windows.h>
#include <process.h>#pragma comment(lib, "ws2_32.lib")
#define NAME_SIZE 32
#define BUF_SIZE 256char szName[NAME_SIZE] = "[DEFAULT]";
char szMsg[BUF_SIZE];//发送消息给服务端
unsigned WINAPI SendMsg(void* arg)
{//1 接收传递过来的参数SOCKET hClntSock = *((SOCKET*)arg);char szNameMsg[NAME_SIZE + BUF_SIZE]; //又有名字,又有消息//循环接收来自于控制台的消息while (1){fgets(szMsg, BUF_SIZE, stdin); //阻塞在这一句//退出机制 当收到q或Q 退出if (!strcmp(szMsg, "Q\n") || !strcmp(szMsg, "q\n")){closesocket(hClntSock);exit(0);}sprintf(szNameMsg, "%s %s", szName, szMsg);//字符串拼接send(hClntSock, szNameMsg, strlen(szNameMsg), 0);//发送}return 0;
}
//接收服务端的消息
unsigned WINAPI RecvMsg(void* arg)
{//1 接收传递过来的参数SOCKET hClntSock = *((SOCKET*)arg);char szNameMsg[NAME_SIZE + BUF_SIZE]; //又有名字,又有消息int iLen = 0;while (1){//recv阻塞iLen = recv(hClntSock, szNameMsg, NAME_SIZE + BUF_SIZE - 1, 0);//服务端断开if (iLen == -1){return -1;}// szNameMsg的0到iLen -1 都是收到的数据 iLen个szNameMsg[iLen] = 0;//接收到的数据输出到控制台fputs(szNameMsg, stdout);}return 0;
}
// 带参数的main函数,用命令行启动 在当前目录按下shift + 鼠标右键 cmd
int main(int argc, char* argv[])
{// 加载套接字库WORD wVersionRequested;WSADATA wsaData;int err;SOCKET hSock;SOCKADDR_IN servAdr;HANDLE hSendThread, hRecvThread;wVersionRequested = MAKEWORD(1, 1);// 初始化套接字库err = WSAStartup(wVersionRequested, &wsaData);if (err != 0){return err;}if (LOBYTE(wsaData.wVersion) != 1 || HIBYTE(wsaData.wVersion) != 1){WSACleanup();return -1;}sprintf(szName, "[%s]", argv[1]);//1 建立sockethSock = socket(PF_INET, SOCK_STREAM, 0);// 2 配置端口和地址memset(&servAdr, 0, sizeof(servAdr));servAdr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");servAdr.sin_family = AF_INET;servAdr.sin_port = htons(9190);// 3 连接服务器if (connect(hSock, (SOCKADDR*)&servAdr, sizeof(servAdr)) == SOCKET_ERROR){printf("connect error error code = %d\n", GetLastError());return -1;}// 4 发送服务端的消息 安排一个工人 起一个线程发送消息hSendThread = (HANDLE)_beginthreadex(NULL, 0, SendMsg,(void*)&hSock, 0, NULL);// 5 接收消息给服务端 安排一个工人 起一个线程接收消息hRecvThread = (HANDLE)_beginthreadex(NULL, 0, RecvMsg,(void*)&hSock, 0, NULL);//等待内核对象的信号发生变化WaitForSingleObject(hSendThread, INFINITE);WaitForSingleObject(hRecvThread, INFINITE);// 6 关闭套接字closesocket(hSock);WSACleanup();return 0;
}
是否要用多线程:有没有并发的必要,同时处理,不是一条线
是否线程同步:是否有可能多个线程操作同一变量的风险
七、线程同步 - 事件对象
线程同步是指多个线程之间协调执行的机制,以确保它们按照一定的顺序和时间间隔执行。事件对象是一种可用于实现线程同步的机制之一。
事件对象是一个操作系统提供的同步原语,它可以有两种状态:signaled(有信号)和nonsignaled(无信号)。线程可以等待一个事件对象进入 signaled 状态,或者通过设置事件对象为 signaled 状态来通知其他线程。
在使用事件对象进行线程同步时,通常会有以下步骤:
- 创建事件对象:使用
CreateEvent
函数创建一个事件对象,并指定初始状态(signaled 或 nonsignaled)。 - 等待事件对象:线程可以使用
WaitForSingleObject
或类似的函数等待事件对象进入 signaled 状态。如果事件对象当前处于 nonsignaled 状态,线程将被阻塞,直到事件对象进入 signaled 状态。 - 设置事件对象:某个线程可以使用
SetEvent
函数将事件对象设置为 signaled 状态,从而唤醒正在等待该事件的线程。 - 重置事件对象:使用
ResetEvent
函数将事件对象重置为 nonsignaled 状态,以便重新等待事件。
事件对象有两种类型:人工重置的事件对象和自动重置的事件对象。这两种事件对象的区别在于当人工重置的事件对象得到通知时,等待该事件对象的所有线程均变为可调度线程;而当一个自动重置的事件对象得到通知时,等待该事件对象的线程中只有一个线程变为可调度线程。
CreateEventW
是一个 Windows API 函数,用于创建事件对象。下面是各个参数的含义:
-
LPSECURITY_ATTRIBUTES lpEventAttributes
:这是一个可选参数,用于指定安全描述符,控制新创建的事件对象的安全性。如果不需要特殊的安全设置,可以将该参数设为NULL
。 -
BOOL bManualReset
:这是一个布尔值参数,用于指定事件对象的重置方式。如果设为TRUE
,表示事件对象是手动重置的;如果设为FALSE
,表示事件对象是自动重置的。 -
BOOL bInitialState
:这是一个布尔值参数,用于指定事件对象的初始状态。如果设为TRUE
,表示事件对象的初始状态为 signaled;如果设为FALSE
,表示事件对象的初始状态为非 signaled。 -
LPCWSTR lpName
:这是一个可选参数,用于指定事件对象的名称。如果这个参数为NULL
,则创建一个匿名的事件对象;如果指定了一个名称,系统会尝试使用这个名称来创建或打开一个已存在的同名事件对象。
CreateEventW
函数返回一个 HANDLE
类型的值,该值表示了新创建的事件对象的句柄。这个句柄可以用于后续操作和控制事件对象。
事件对象通常用于线程间的同步和通信,可以通过设置和重置事件来实现线程的等待和唤醒操作。
HANDLE WINAPI CreateEventW(_In_opt_ LPSECURITY_ATTRIBUTES lpEventAttributes,_In_ BOOL bManualReset,_In_ BOOL bInitialState,_In_opt_ LPCWSTR lpName);
SetEvent
是一个 Windows API 函数,用于将指定的事件对象设置为 signaled 状态。下面是参数的含义:
HANDLE hEvent
:这是要设置为 signaled 状态的事件对象的句柄。
SetEvent
函数将指定的事件对象的状态设置为 signaled,从而唤醒正在等待该事件的线程。一旦事件对象被设置为 signaled,所有当前正在等待该事件的线程将会继续执行。这是线程间同步和通信的重要手段之一。
BOOL WINAPI SetEvent(_In_ HANDLE hEvent);
每次触发后,必有一个或多个处于等待状态下的线程变成可调度状态。
ResetEvent
是一个 Windows API 函数,用于将指定的事件对象重置为非 signaled 状态。下面是参数的含义:
HANDLE hEvent
:这是要重置为非 signaled 状态的事件对象的句柄。
ResetEvent
函数将指定的事件对象的状态重置为非 signaled,从而使得正在等待该事件的线程进入等待状态。一旦事件对象被重置为非 signaled,后续调用 WaitForSingleObject
或类似函数的线程将会被阻塞。
通过使用 SetEvent
和 ResetEvent
函数结合,可以实现线程间的同步和通信,以及控制线程的执行顺序。
BOOL WINAPI ResetEvent(_In_ HANDLE hEvent);
测试代码
#include <stdio.h>
#include <windows.h>
#include <process.h>#define STR_LEN 100unsigned WINAPI NumberOfA(void* arg);
unsigned WINAPI NumberOfOthers(void* arg);static char str[STR_LEN];static HANDLE hEvent;int main(int argc, char* argv[])
{HANDLE hThread1, hThread2;fputs("Input string: ", stdout);fgets(str, STR_LEN, stdin);hEvent = CreateEvent(NULL, TRUE, FALSE, NULL); // 手动设置,初始为非signaledhThread1 = (HANDLE)_beginthreadex(NULL, 0, NumberOfA, NULL, 0, NULL);hThread2 = (HANDLE)_beginthreadex(NULL, 0, NumberOfOthers, NULL, 0, NULL);WaitForSingleObject(hThread1, INFINITE);WaitForSingleObject(hThread2, INFINITE);//直到2个线程执行完之后,再把事件设置为无信号状态ResetEvent(hEvent);CloseHandle(hEvent);system("pause");return 0;
}unsigned WINAPI NumberOfA(void* arg)
{int i, cnt = 0;//再没有执行fputs("Input string: ", stdout);//fgets(str, STR_LEN, stdin);SetEvent(hEvent);之前,卡在//WaitForSingleObjectWaitForSingleObject(hEvent, INFINITE);for (i = 0; str[i] != 0; i++){if (str[i] == 'A')cnt++;}printf("Num of A: %d \n", cnt);return 0;
}unsigned WINAPI NumberOfOthers(void* arg)
{int i, cnt = 0;//再没有执行fputs("Input string: ", stdout);//fgets(str, STR_LEN, stdin);SetEvent(hEvent);之前,卡在//WaitForSingleObject// WaitForSingleObject(hEvent, INFINITE);for (i = 0; str[i] != 0; i++){if (str[i] != 'A')cnt++;}printf("Num of others: %d \n", cnt - 1);//把事件对象设置为有信号状态SetEvent(hEvent);return 0;
}
事件对象模拟售票,利用通知的方式对共同资源的互斥控制
#include "windows.h"
#include "iostream" using namespace std;DWORD WINAPI FunProc1(LPVOID lpParameter);
DWORD WINAPI FunProc2(LPVOID lpParameter);
int ticket = 100;
HANDLE g_hEvent;void main()
{HANDLE hThread1;HANDLE hThread2;g_hEvent = CreateEvent(NULL, FALSE, TRUE, NULL); //创建事件对象 //SetEvent(g_hEvent); //设置为有信号(放弃钥匙) hThread1 = CreateThread(NULL, 0, FunProc1, NULL, 0, NULL);hThread2 = CreateThread(NULL, 0, FunProc2, NULL, 0, NULL);CloseHandle(hThread1);CloseHandle(hThread2);Sleep(1000); // 让主线程睡眠1秒 CloseHandle(g_hEvent);
}DWORD WINAPI FunProc1(LPVOID lpParameter)
{while (TRUE){WaitForSingleObject(g_hEvent, INFINITE); //申请事件对象 //ResetEvent(g_hEvent); //重置为无信号(申请钥匙,得带钥匙) if (ticket > 0){Sleep(1);cout << "ticket 1:" << ticket-- << endl;SetEvent(g_hEvent); //设置为有信号(放弃钥匙,不再拥有) }else{SetEvent(g_hEvent); //设置为有信号 break;}}return 0;
}DWORD WINAPI FunProc2(LPVOID lpParameter)
{while (TRUE){WaitForSingleObject(g_hEvent, INFINITE);//ResetEvent(g_hEvent); if (ticket > 0){Sleep(1);cout << "ticket 2:" << ticket-- << endl;SetEvent(g_hEvent); //设置为有信号 }else{SetEvent(g_hEvent); //设置为有信号 break;}}return 0;
}
执行顺序:开始的时候事件对象具有信号,当第一个线程申请获得事件对象后,进入if语句线程1会暂停1毫秒,于是第二根线程运行,因为此时g_hEvent已经无信号故无法申请并执行下面的程序,此时第一个线程睡醒开始执行自己的任务然后设置对象为有信号(可以被其他线程申请),于是第二个线程申请得到事件对象................与此往复!直到退出循环。
意:该程序实现的不是的同步控制!但是他可以实现同步,见下面!
下面这种事件通知方式就是严格的同步方式(先让线程1执行,再.....),两个事件对象进行同步控制:
#include "windows.h"
#include "iostream" using namespace std; DWORD WINAPI FunProc1(LPVOID lpParameter);
DWORD WINAPI FunProc2(LPVOID lpParameter);
int ticket = 100;
HANDLE g_hEvent1,g_hEvent2; void main()
{ HANDLE hThread1; HANDLE hThread2; g_hEvent1 = CreateEvent(NULL, FALSE, TRUE, NULL); //创建事件对象1,自动重置事件,已触发 g_hEvent2 = CreateEvent(NULL, FALSE, FALSE, NULL); //创建事件对象2,自动重置事件,没触发 //SetEvent(g_hEvent); //设置为有信号(放弃钥匙) hThread1 = CreateThread(NULL, 0, FunProc1, NULL, 0, NULL); hThread2 = CreateThread(NULL, 0, FunProc2, NULL, 0, NULL); CloseHandle(hThread1); CloseHandle(hThread2); Sleep(1000); // 让主线程睡眠1秒 CloseHandle(g_hEvent1); CloseHandle(g_hEvent2); getchar();
}
DWORD WINAPI FunProc1(LPVOID lpParameter)
{ while (TRUE) { WaitForSingleObject(g_hEvent1, INFINITE); //申请有信号的事件对象 //ResetEvent(g_hEvent); //重置为无信号(申请钥匙,得带钥匙) if (ticket>0) { Sleep(1); cout << "ticket 1:" << ticket-- << endl; SetEvent(g_hEvent2); //设置为有信号(放弃钥匙,不再拥有) } else { SetEvent(g_hEvent2); //设置为有信号,让线程2执行 break; } } return 0;
}
DWORD WINAPI FunProc2(LPVOID lpParameter)
{ while (TRUE) { WaitForSingleObject(g_hEvent2, INFINITE); //ResetEvent(g_hEvent); if (ticket>0) { Sleep(1); cout << "ticket 2:" << ticket-- << endl; SetEvent(g_hEvent1); //让线程1执行 } else { SetEvent(g_hEvent1); break; } } return 0;
}
八、事件对象 与 互斥对象区别
在线程同步中,事件对象和互斥对象是两种不同的同步机制,它们在实现线程之间协调和互斥访问共享资源时起着不同的作用。下面是它们的区别:
-
事件对象(Event):
- 事件对象通常用于线程之间的通信和协调,允许一个或多个线程等待某个事件的发生。
- 事件对象通常有两种状态:有信号和无信号。当事件处于有信号状态时,等待该事件的线程将被唤醒;当事件处于无信号状态时,等待该事件的线程将被阻塞。
- 事件对象可以通过Set方法将其状态设置为有信号,从而唤醒等待该事件的线程;也可以通过Reset方法将其状态设置为无信号。
- 典型的应用场景包括线程间的协作、通知和等待特定事件的发生。
-
互斥对象(Mutex):
- 互斥对象用于控制多个线程对共享资源的访问,确保在同一时刻只有一个线程可以访问共享资源,从而避免数据竞争和不一致性。
- 当一个线程获得了互斥对象的锁时,其他线程将被阻塞,直到持有锁的线程释放锁。
- 互斥对象通常由锁定、解锁等操作来实现对临界区的保护,防止多个线程同时访问导致的数据错误。
因此,事件对象和互斥对象在线程同步中有不同的作用:事件对象用于线程之间的通信和协作,而互斥对象用于保护共享资源,确保线程安全访问。在实际的多线程编程中,开发人员需要根据具体的需求选择合适的同步机制来实现线程之间的协调和资源访问的保护。
九、线程同步 - 信号量
内核对象的状态:
- 触发状态(有信号状态),表示有可用资源。
- 未触发状态(无信号状态),表示没有可用资源
工作原理:以一个停车场是运作为例。假设停车场只有三个车位,一开始三个车位都是空的。这时如果同时来了五辆车,看门人允许其中三辆不受阻碍的进入,然后放下车拦,剩下的车则必须在入口等待,此后来的车也都不得不在入口处等待。这时,有一辆车离开停车场,看门人得知后,打开车拦,放入一辆,如果又离开两辆,则又可以放入两辆,如此往复。这个停车系统中,每辆车就好比一个线程,看门人就好比一个信号量,看门人限制了可以活动的线程。假如里面依然是三个车位,但是看门人改变了规则,要求每次只能停两辆车,那么一开始进入两辆车,后面得等到有车离开才能有车进入,但是得保证最多停两辆车。对于 Semaphore 而言,就如同一个看门人,限制了可活动的线程数。
信号量的组成
- 计数器:该内核对象被使用的次数
- 最大资源数量:标识信号量可以控制的最大资源数量(带符号的32位)
- 当前资源数量:标识当前可用资源的数量(带符号的32 位)。即表示当前开放资源的个数(注意不是剩下资源的个数),只有开放的资源才能被线程所申请。但这些开放的资源不一定被线程占用完。比如,当前开放5 个资源,而只有3 个线程申请,则还有 2 个资源可被申请,但如果这时总共是7 个线程要使用信号量,显然开放的资源 5 个是不够的。这时还可以再开放2 个,直到达到最大资源数量。
信号量的规则如下:
- 如果当前资源计数大于 0,那么信号量处于触发状态(有信号状态),表示有可用资源。
- 如果当前资源计数等于 0,那么信号量属于未触发状态(无信号状态),表示没有可用资源。
- 系统绝对不会让当前资源计数变为负数
- 当前资源计数绝对不会大于最大资源计数
信号量与互斥量不同的地方是,它允许多个线程在同一时刻访问同一资源,但是需要限制在同一时刻访问此资源的最大线程数目。信号量对象对线程的同步方式与前面几种方法不同,信号允许多个线程同时使用共享资源。
创建信号量
HANDLE WINAPI CreateSemaphoreW(_In_opt_ LPSECURITY_ATTRIBUTES lpSemaphoreAttributes, // Null 安全属性_In_ LONG lInitialCount, //初始化时,共有多少个资源是可以用的。 0:未触发状//态(无信号状态),表示没有可用资源_In_ LONG lMaximumCount, //能够处理的最大的资源数量 3_In_opt_ LPCWSTR lpName //NULL 信号量的名称);
增加信号量
WINAPI ReleaseSemaphore(_In_ HANDLE hSemaphore, //信号量的句柄_In_ LONG lReleaseCount, //将lReleaseCount值加到信号量的当前资源计数上面0-> 1_Out_opt_ LPLONG lpPreviousCount //当前资源计数的原始值);
关闭句柄
CloseHandle(_In_ _Post_ptr_invalid_ HANDLE hObject
);
代码示例
#include <stdio.h>
#include <windows.h>
#include <process.h>unsigned WINAPI Read(void* arg);
unsigned WINAPI Accu(void* arg);static HANDLE semOne;
static HANDLE semTwo;
static int num;int main(int argc, char* argv[])
{HANDLE hThread1, hThread2;semOne = CreateSemaphore(NULL, 0, 1, NULL);//semOne 没有可用资源 只能表示0或者1的二进制信号量 无信号semTwo = CreateSemaphore(NULL, 1, 1, NULL);//semTwo 有可用资源,有信号状态 有信号hThread1 = (HANDLE)_beginthreadex(NULL, 0, Read, NULL, 0, NULL);hThread2 = (HANDLE)_beginthreadex(NULL, 0, Accu, NULL, 0, NULL);WaitForSingleObject(hThread1, INFINITE);WaitForSingleObject(hThread2, INFINITE);CloseHandle(semOne);CloseHandle(semTwo);system("pause");return 0;
}unsigned WINAPI Read(void* arg)
{int i;for (i = 0; i < 5; i++){fputs("Input num: ", stdout); // 1 5 11printf("begin read\n"); // 3 6 12//等待内核对象semTwo的信号,如果有信号,继续执行;如果没有信号,等待WaitForSingleObject(semTwo, INFINITE);printf("beginning read\n"); //4 10 16scanf("%d", &num);// 信号量semTwo的计数将增加1。ReleaseSemaphore(semOne, 1, NULL);}return 0;
}
unsigned WINAPI Accu(void* arg)
{int sum = 0, i;for (i = 0; i < 5; i++){printf("begin Accu\n"); //2 9 15//等待内核对象semOne的信号,如果有信号,继续执行;如果没有信号,等待WaitForSingleObject(semOne, INFINITE);printf("beginning Accu\n"); //7 13sum += num;printf("sum = %d \n", sum); // 8 14// 信号量semTwo的计数将增加1。ReleaseSemaphore(semTwo, 1, NULL);}printf("Result: %d \n", sum);return 0;
}
十、线程同步 - 关键代码段
关键代码段,也称为临界区,工作在用户方式下。它是指一个小代码段,在代码能够执行前,它必须独占对某些资源的访问权。通常把多线程中访问同一种资源的那部分代码当做关键代码段。
1. 初始化关键代码段:调用 InitializeCriticalSection 函数初始化一个关键代码段。
该函数只有一个指向CRITICAL_SECTION结构体的指针。在调用InitializeCriticalSection函数之前,首先需要构造一个CRITICAL_SCTION结构体类型的对象,然后将该对象的地址传递给InitializeCriticalSection函数。
InitializeCriticalSection(_Out_ LPCRITICAL_SECTION lpCriticalSection
);
2. 进入关键代码段
调用EnterCriticalSection函数,以获得指定的临界区对象的所有权,该函数等待指定的临界区对象的所有权,如果该所有权赋予了调用线程,则该函数就返回;否则该函数会一直等待,从而导致线程等待。
VOID WINAPI EnterCriticalSection(_Inout_ LPCRITICAL_SECTION lpCriticalSection
);
3. 退出关键代码段
线程使用完临界区所保护的资源之后,需要调用LeaveCriticalSection函数,释放指定的临界区对象的所有权。之后,其他想要获得该临界区对象所有权的线程就可以获得该所有权,从而进入关键代码段,访问保护的资源。
VOID WINAPI LeaveCriticalSection(_Inout_ LPCRITICAL_SECTION lpCriticalSection
);
4. 删除临界区
当临界区不再需要时,可以调用DeleteCriticalSection函数释放该对象,该函数将释放一个没有被任何线程所拥有的临界区对象的所有资源。
WINBASEAPI VOID WINAPI DeleteCriticalSection(_Inout_ LPCRITICAL_SECTION lpCriticalSection
);
代码示例
#include <stdio.h>
#include <windows.h>
#include <process.h> int iTickets = 5000;
CRITICAL_SECTION g_cs;// A窗口 B窗口DWORD WINAPI SellTicketA(void* lpParam)
{while (1){EnterCriticalSection(&g_cs);//进入临界区if (iTickets > 0){Sleep(1);iTickets--;printf("A remain %d\n", iTickets);LeaveCriticalSection(&g_cs);//离开临界区}else{LeaveCriticalSection(&g_cs);//离开临界区break;}}return 0;
}DWORD WINAPI SellTicketB(void* lpParam)
{while (1){EnterCriticalSection(&g_cs);//进入临界区if (iTickets > 0){Sleep(1);iTickets--;printf("B remain %d\n", iTickets);LeaveCriticalSection(&g_cs);//离开临界区}else{LeaveCriticalSection(&g_cs);//离开临界区break;}}return 0;
}int main()
{HANDLE hThreadA, hThreadB;hThreadA = CreateThread(NULL, 0, SellTicketA, NULL, 0, NULL); //2hThreadB = CreateThread(NULL, 0, SellTicketB, NULL, 0, NULL); //2CloseHandle(hThreadA); //1CloseHandle(hThreadB); //1InitializeCriticalSection(&g_cs); //初始化关键代码段Sleep(40000);DeleteCriticalSection(&g_cs);//删除临界区system("pause");return 0;
}
十一、线程死锁
死锁是指多个线程因竞争资源而造成的一种僵局(互相等待),若无外力作用,这些进程都将无法向前推进。
#include <stdio.h>
#include <windows.h>
#include <process.h>
int iTickets = 5000;
CRITICAL_SECTION g_csA;
CRITICAL_SECTION g_csB;
// A窗口 B窗口
DWORD WINAPI SellTicketA(void* lpParam)
{while (1){EnterCriticalSection(&g_csA);//进入临界区ASleep(1);EnterCriticalSection(&g_csB);//进入临界区Bif (iTickets > 0){Sleep(1);iTickets--;printf("A remain %d\n", iTickets);LeaveCriticalSection(&g_csB);//离开临界区BLeaveCriticalSection(&g_csA);//离开临界区A}else{LeaveCriticalSection(&g_csB);//离开临界区BLeaveCriticalSection(&g_csA);//离开临界区Abreak;}}return 0;
}
DWORD WINAPI SellTicketB(void* lpParam)
{while (1){EnterCriticalSection(&g_csB);//进入临界区BSleep(1);EnterCriticalSection(&g_csA);//进入临界区Aif (iTickets > 0){Sleep(1);iTickets--;printf("B remain %d\n", iTickets);LeaveCriticalSection(&g_csA);//离开临界区ALeaveCriticalSection(&g_csB);//离开临界区B}else{LeaveCriticalSection(&g_csA);//离开临界区ALeaveCriticalSection(&g_csB);//离开临界区Bbreak;}}return 0;
}
int main()
{HANDLE hThreadA, hThreadB;hThreadA = CreateThread(NULL, 0, SellTicketA, NULL, 0, NULL); //2hThreadB = CreateThread(NULL, 0, SellTicketB, NULL, 0, NULL); //2CloseHandle(hThreadA); //1CloseHandle(hThreadB); //1InitializeCriticalSection(&g_csA); //初始化关键代码段AInitializeCriticalSection(&g_csB); //初始化关键代码段BSleep(40000);DeleteCriticalSection(&g_csA);//删除临界区DeleteCriticalSection(&g_csB);//删除临界区system("pause");return 0;
}
十二、四种线程同步方式总结
Windows线程同步的方式主要有四种:互斥对象Mutex,事件对象event,关键代码段criticalSection,信号量。
对于上面介绍的三种线程同步的方式,它们之间的区别如下所述:
用户级别的:关键代码段,只能本进程中;内核级别的:互斥量/事件/信号量,可以跨进程
- 互斥对象和事件以及信号量都属于内核对象,利用内核对象进行线程同步时,速度较慢,但利用互斥对象和事件对象这样的内核对象,可以在多个进程中的各个线程间进行同步。
- 关键代码段工作在用户方式下,同步速度较快,但在使用关键代码段时,很容易进入死锁状态,因为在等待进入关键代码段时无法设定超时值。
通常,在编写多线程程序并需要实现线程同步时,首选关键代码段,由于它的使用比较简单,如果是在MFC程序中使用的话,可以在类的构造函数Init中调用InitializeCriticalSection函数,在该类的析构函数中调用DeleteCriticalSection函数,在所需保护的代码前面调用EnterCriticalSection函数,在访问完所需保护的资源后,调用LeaveCriticalSection函数。可见,关键代码段在使用上是非常方便的,但有几点需要注意:
- 在程序中调用了EnterCriticalSection后,要相应的调用LeaveCriticalSection函数,否则其他等待该临界区对象所有权的线程将无法执行。
- 如果访问关键代码段时,使用了多个临界区对象,就要注意防止线程死锁的发生。另外,如果需要在多个进程间的各个线程间实现同步的话,可以使用互斥对象和事件对象或者信号量。
完结撒花!!!
相关文章:

Winodws核心编程 多线程
目录 一、基本概念 二、线程创建函数 三、Windows内核对象与句柄 四、简单的多线程案例 五、线程同步 - 互斥对象 六、多线程实现群聊的服务端和客户端 七、线程同步 - 事件对象 八、事件对象 与 互斥对象区别 九、线程同步 - 信号量 十、线程同步 - 关键代码段 十一…...

旺店通·企业版对接打通金蝶云星空查询调拨单接口与分布式调入单新增接口
旺店通企业版对接打通金蝶云星空查询调拨单接口与分布式调入单新增接口 源系统:旺店通企业版 旺店通是北京掌上先机网络科技有限公司旗下品牌,国内的零售云服务提供商,基于云计算SaaS服务模式,以体系化解决方案,助力零售企业数字化…...
关于对Java中volatile关键字的理解与简述
【版权声明】未经博主同意,谢绝转载!(请尊重原创,博主保留追究权) https://blog.csdn.net/m0_69908381/article/details/134430096 出自【进步*于辰的博客】 启发之作:Java volatile关键字最全总结…...

37 _ 贪心算法:如何用贪心算法实现Huffman压缩编码?
基础的数据结构和算法我们基本上学完了,接下来几节,我会讲几种更加基本的算法。它们分别是贪心算法、分治算法、回溯算法、动态规划。更加确切地说,它们应该是算法思想,并不是具体的算法,常用来指导我们设计具体的算法和编码等。 贪心、分治、回溯、动态规划这4个算法思想…...

Unity中Shader矩阵的逆矩阵
文章目录 前言一、逆矩阵的表示二、逆矩阵的作用四、逆矩阵的计算五、顺序的重要性六、矩阵的逆总结1、求矩阵的逆前,这个矩阵必须得是个方阵2、只有 A x A ^-1^ A^-1^ x A 1时,A的逆才是A^-1^3、求2x2矩阵的逆:交换 a 和 b 的位置…...
我给网站做公安备案年度安全评估
我是卢松松,点点上面的头像,欢迎关注我哦! 差不多从2020年开始,我们的网站每年11月左右就要去公安备案做一次年度的安全评估,而现在又新增了APP和小程序备案。如下图所示: 评估的内容也很简单,…...
iceoryx(冰羚)-通信中间件解析
iceoryx(冰羚)-简介 iceoryx(冰羚)-Architecture iceoryx(冰羚)-Service Discovery iceoryx(冰羚)-examples-callbacks iceoryx(冰羚)-Listener设计 [iceoryx(冰羚)-ipc消息通信] [iceoryx(冰羚)-共享内存实现]...

Windows系统CMake+VS编译protobuf
目录 一些名词CMake构建VS工程下载protobuf源码下载CMake编译QT中使用 方案二失败:CMakeQT自带的Mingw编译参考链接 一些名词 lib dll lib库实际上分为两种,一种是静态链接lib库或者叫做静态lib库,另一种叫做动态链接库dll库的lib导入库或称…...

HarmonyOS开发(三):ArkTS基础
1、ArkTS演进 Mozilla创建了JS ---> Microsoft创建了TS ----> Huawei进一步推出ArkTS 从最初的基础逻辑交互(JS),到具备类型系统的高效工程开发(TS),再到融合声明式UI、多维状态管理等丰富的应用开发能力&…...

Java排序算法之堆排序
图解 堆排序是一种常见的排序算法,它借助了堆这种数据结构。堆是一种完全二叉树,它可以分为两种类型:最大堆和最小堆。在最大堆中,每个结点的值都大于等于它的子结点的值,而在最小堆中,每个结点的值都小于等…...

『GitHub项目圈选02』一款可实现视频自动翻译配音为其他语言的开源项目
🔥🔥🔥本周GitHub项目圈选****: 主要包含视频翻译、正则填字游戏、敏感词检测、聊天机器人框架、AI 换脸、分布式数据集成平台等热点项目。 1、pyvideotrans pyvideotrans 是一个视频翻译工具,可将一种语言的视频翻译为另一种语…...

Unity - Cinemachine
动态获取Cinemachine的内部组件 vCam.GetCinemachineComponent<T>() 动态修改Cinemachine的Transposer属性 var vCamComp transfrom.GetComponent<CinemachineVirtualCamera>(); var transposerComp vCamComp.GetCinemachineComponent<CinemachineTransposer&…...

准备搞OpenStack了,先装一台最新的Ubuntu 23.10
正文共:1113 字 25 图,预估阅读时间:2 分钟 依稀记得前面发了一篇Ubuntu的安装文档(66%的经验丰富开发者和69%的学生更喜欢的Ubuntu的安装初体验),当时安装的是20.04.3的版本,现在看来已经是非常…...

Android 12 客制化修改初探-Launcher/Settings/Bootanimation
Android 12 使用 Material You 打造的全新系统界面,富有表现力、活力和个性。使用重新设计的微件、AppSearch、游戏模式和新的编解码器扩展您的应用。支持隐私信息中心和大致位置等新的保护功能。使用富媒体内容插入功能、更简便的模糊处理功能、经过改进的原生调试…...

【JavaEE初阶】 HTML基础详解
文章目录 🎋什么是HTML?🍀HTML 结构🚩认识标签🚩HTML 文件基本结构🚩快速生成代码框架 🎄HTML 常见标签🚩注释标签🚩标题标签: h1-h6🚩段落标签: pǶ…...

C# Socket通信从入门到精通(10)——如何检测两台电脑之间的网络是否通畅
前言: 我们在完成了socket通信程序开发以后,并且IP地址也设置好以后,可以先通过一些手段来测试两台电脑之间的网络是否通畅,如果确认了网络通畅以后,我们再测试我们编写的Socket程序。 1、同时按下键盘的windows键+"R"键,如下图: 下面两张图是两种键盘的情…...

python科研绘图:P-P图与Q-Q图
目录 什么是P-P图与Q-Q图 分位数 百分位数 Q-Q图步骤与原理 Shapiro-Wilk检验 绘制Q-Q图 绘制P-P图 什么是P-P图与Q-Q图 P-P图和Q-Q图都是用于检验样本的概率分布是否服从某种理论分布。 P-P图的原理是检验实际累积概率分布与理论累积概率分布是否吻合。若吻合…...

浅尝:iOS的CoreGraphics和Flutter的Canvas
iOS的CoreGraphic 基本就是创建一个自定义的UIView,然后重写drawRect方法,在此方法里使用UIGraphicsGetCurrentContext()来绘制目标图形和样式 #import <UIKit/UIKit.h>interface MyGraphicView : UIView endimplementation MyGraphicView// Onl…...

网络安全黑客技术自学
前言 一、什么是网络安全 网络安全可以基于攻击和防御视角来分类,我们经常听到的 “红队”、“渗透测试” 等就是研究攻击技术,而“蓝队”、“安全运营”、“安全运维”则研究防御技术。 无论网络、Web、移动、桌面、云等哪个领域,都有攻与防…...

【文件读取/包含】任意文件读取漏洞 afr_3
1.1漏洞描述 漏洞名称任意文件读取漏洞 afr_3漏洞类型文件读取/包含漏洞等级⭐⭐⭐⭐⭐漏洞环境docker攻击方式 1.2漏洞等级 高危 1.3影响版本 暂无 1.4漏洞复现 1.4.1.基础环境 靶场docker工具BurpSuite 1.4.2.环境搭建 1.创建docker-compose.yml文件 version: 3.2 servi…...
DockerHub与私有镜像仓库在容器化中的应用与管理
哈喽,大家好,我是左手python! Docker Hub的应用与管理 Docker Hub的基本概念与使用方法 Docker Hub是Docker官方提供的一个公共镜像仓库,用户可以在其中找到各种操作系统、软件和应用的镜像。开发者可以通过Docker Hub轻松获取所…...

STM32F4基本定时器使用和原理详解
STM32F4基本定时器使用和原理详解 前言如何确定定时器挂载在哪条时钟线上配置及使用方法参数配置PrescalerCounter ModeCounter Periodauto-reload preloadTrigger Event Selection 中断配置生成的代码及使用方法初始化代码基本定时器触发DCA或者ADC的代码讲解中断代码定时启动…...
电脑插入多块移动硬盘后经常出现卡顿和蓝屏
当电脑在插入多块移动硬盘后频繁出现卡顿和蓝屏问题时,可能涉及硬件资源冲突、驱动兼容性、供电不足或系统设置等多方面原因。以下是逐步排查和解决方案: 1. 检查电源供电问题 问题原因:多块移动硬盘同时运行可能导致USB接口供电不足&#x…...

Mac软件卸载指南,简单易懂!
刚和Adobe分手,它却总在Library里给你写"回忆录"?卸载的Final Cut Pro像电子幽灵般阴魂不散?总是会有残留文件,别慌!这份Mac软件卸载指南,将用最硬核的方式教你"数字分手术"࿰…...
linux 下常用变更-8
1、删除普通用户 查询用户初始UID和GIDls -l /home/ ###家目录中查看UID cat /etc/group ###此文件查看GID删除用户1.编辑文件 /etc/passwd 找到对应的行,YW343:x:0:0::/home/YW343:/bin/bash 2.将标红的位置修改为用户对应初始UID和GID: YW3…...
css的定位(position)详解:相对定位 绝对定位 固定定位
在 CSS 中,元素的定位通过 position 属性控制,共有 5 种定位模式:static(静态定位)、relative(相对定位)、absolute(绝对定位)、fixed(固定定位)和…...

深入解析C++中的extern关键字:跨文件共享变量与函数的终极指南
🚀 C extern 关键字深度解析:跨文件编程的终极指南 📅 更新时间:2025年6月5日 🏷️ 标签:C | extern关键字 | 多文件编程 | 链接与声明 | 现代C 文章目录 前言🔥一、extern 是什么?&…...
uniapp中使用aixos 报错
问题: 在uniapp中使用aixos,运行后报如下错误: AxiosError: There is no suitable adapter to dispatch the request since : - adapter xhr is not supported by the environment - adapter http is not available in the build 解决方案&…...
全面解析各类VPN技术:GRE、IPsec、L2TP、SSL与MPLS VPN对比
目录 引言 VPN技术概述 GRE VPN 3.1 GRE封装结构 3.2 GRE的应用场景 GRE over IPsec 4.1 GRE over IPsec封装结构 4.2 为什么使用GRE over IPsec? IPsec VPN 5.1 IPsec传输模式(Transport Mode) 5.2 IPsec隧道模式(Tunne…...
是否存在路径(FIFOBB算法)
题目描述 一个具有 n 个顶点e条边的无向图,该图顶点的编号依次为0到n-1且不存在顶点与自身相连的边。请使用FIFOBB算法编写程序,确定是否存在从顶点 source到顶点 destination的路径。 输入 第一行两个整数,分别表示n 和 e 的值(1…...