目录
一、基本概念
二、线程创建函数
三、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 50
unsigned 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);
else
tHandles[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 50
unsigned 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);
else
tHandles[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 256
SOCKET 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地址,端口号9190
if (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);
//每来一个连接,服务端起一个线程(安排一个工人)维护客户端的连接//每来一个连接,全局数组应该加一个成员,最大连接数加1
WaitForSingleObject(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 256
char 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 建立socket
hSock = 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 100
unsigned 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); // 手动设置,初始为非signaled
hThread1 = (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);之前,卡在
//WaitForSingleObject
WaitForSingleObject(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 11
printf("begin read\n"); // 3 6 12
//等待内核对象semTwo的信号,如果有信号,继续执行;如果没有信号,等待
WaitForSingleObject(semTwo, INFINITE);
printf("beginning read\n"); //4 10 16
scanf("%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 13
sum += 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); //2
hThreadB = CreateThread(NULL, 0, SellTicketB, NULL, 0, NULL); //2
CloseHandle(hThreadA); //1
CloseHandle(hThreadB); //1
InitializeCriticalSection(&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);//进入临界区A
Sleep(1);
EnterCriticalSection(&g_csB);//进入临界区B
if (iTickets > 0)
{
Sleep(1);
iTickets--;
printf("A remain %d\n", iTickets);
LeaveCriticalSection(&g_csB);//离开临界区B
LeaveCriticalSection(&g_csA);//离开临界区A
}
else
{
LeaveCriticalSection(&g_csB);//离开临界区B
LeaveCriticalSection(&g_csA);//离开临界区A
break;
}
}
return 0;
}
DWORD WINAPI SellTicketB(void* lpParam)
{
while (1)
{
EnterCriticalSection(&g_csB);//进入临界区B
Sleep(1);
EnterCriticalSection(&g_csA);//进入临界区A
if (iTickets > 0)
{
Sleep(1);
iTickets--;
printf("B remain %d\n", iTickets);
LeaveCriticalSection(&g_csA);//离开临界区A
LeaveCriticalSection(&g_csB);//离开临界区B
}
else
{
LeaveCriticalSection(&g_csA);//离开临界区A
LeaveCriticalSection(&g_csB);//离开临界区B
break;
}
}
return 0;
}
int main()
{
HANDLE hThreadA, hThreadB;
hThreadA = CreateThread(NULL, 0, SellTicketA, NULL, 0, NULL); //2
hThreadB = CreateThread(NULL, 0, SellTicketB, NULL, 0, NULL); //2
CloseHandle(hThreadA); //1
CloseHandle(hThreadB); //1
InitializeCriticalSection(&g_csA); //初始化关键代码段A
InitializeCriticalSection(&g_csB); //初始化关键代码段B
Sleep(40000);
DeleteCriticalSection(&g_csA);//删除临界区
DeleteCriticalSection(&g_csB);//删除临界区
system("pause");
return 0;
}
十二、四种线程同步方式总结
Windows线程同步的方式主要有四种:互斥对象Mutex,事件对象event,关键代码段criticalSection,信号量。
对于上面介绍的三种线程同步的方式,它们之间的区别如下所述:
用户级别的:关键代码段,只能本进程中;内核级别的:互斥量/事件/信号量,可以跨进程
- 互斥对象和事件以及信号量都属于内核对象,利用内核对象进行线程同步时,速度较慢,但利用互斥对象和事件对象这样的内核对象,可以在多个进程中的各个线程间进行同步。
- 关键代码段工作在用户方式下,同步速度较快,但在使用关键代码段时,很容易进入死锁状态,因为在等待进入关键代码段时无法设定超时值。
通常,在编写多线程程序并需要实现线程同步时,首选关键代码段,由于它的使用比较简单,如果是在MFC程序中使用的话,可以在类的构造函数Init中调用InitializeCriticalSection函数,在该类的析构函数中调用DeleteCriticalSection函数,在所需保护的代码前面调用EnterCriticalSection函数,在访问完所需保护的资源后,调用LeaveCriticalSection函数。可见,关键代码段在使用上是非常方便的,但有几点需要注意:
- 在程序中调用了EnterCriticalSection后,要相应的调用LeaveCriticalSection函数,否则其他等待该临界区对象所有权的线程将无法执行。
- 如果访问关键代码段时,使用了多个临界区对象,就要注意防止线程死锁的发生。另外,如果需要在多个进程间的各个线程间实现同步的话,可以使用互斥对象和事件对象或者信号量。
完结撒花!!!