windows 线程同步问题 代码详解

线程

进程是操作系统分配资源的单位
线程是执行任务的单元
一个进程至少有一个线程。
线程还可以创建线程,不过每一个线程都是独立的执行单元,相互之间没有从属关系。

创建线程最核心要素: CreateThread 函数

HANDLE CreateThread(
LPSECURITY_ATTRIBUTES 安全属性,
SIZE_T 栈空间大小,
LPTHREAD_START_ROUTINE 执行起始地址,
LPVOID lpParameter, 参数
DWORD dwCreationFlags,
LPDWORD 线程ID
);

创建一个最简单的线程

int g_number = 0;
DWORD WINAPI ThreadProc(
LPVOID lpParameter
)
{
for (int i = 0; i < 100; i++)
{
g_number++;
}
printf("g_number: %d\n", g_number);
return 0;
}
int main()
{
HANDLE FirstThread = CreateThread(NULL,
NULL,
ThreadProc,
NULL,
NULL,
NULL
);
WaitForSingleObject(FirstThread, -1);
return 0;
}

注意:

  • 我们使用CreateThread创建子线程,对我们当前有用的参数只有第三个第四个
  • 第三个参数: ThreadProc的回调函数,用于作为处理子线程的函数。
  • 第四个参数: 传递给子线程函数的参数,如果为void,则传递NULL即可。
  • 子线程的参数:lpParameter 作为PVOID的指针,可以转换为任意一个类型的参数。

如果需要传递参数: 比如我们要传递一个int类型的变量,在线程中改变。
第四个参数可以传递一个地址,然后在子线程中解引用操作

//子线程:
(*(int*)lpParameter)++ //完成++ 操作
  • WaitForSingleObject函数: 主线程结束会导致其他线程结束,所以在我们创建一个线程之后,要紧接着调用此函数来等待子线程执行完毕再结束。否则,就直接return 0,就不会进入子线程了。

最后我们的程序运行如下:
在这里插入图片描述


线程状态

  • 线程结束: 激发态(有信号状态) :
  • 正在运行的线程:非激发态(无信号状态)

WaitForSingleObject 就是等待线程处于激发态

CreateThread的倒数第二个参数: dwCreationFlags 他是一个标记,表示线程创建后首先处于什么状态。
在这里插入图片描述

  • 如果为 0(NULL):线程一开始就处于非激发态,线程会立刻运行。然后Wait等待线程处于激发态(即线程执行完毕,结束)。
  • 如果为 CREATE_SUSPENDED:则线程在一开始就处于激发态,线程不会立刻执行,如果我们Wait线程,则会发现线程卡住了! 我们需要显示的使用 ResumeThread函数来使线程处于非激发态,即线程开始,然后Wait线程结束。
HANDLE FirstThread = CreateThread(NULL,
NULL,
ThreadProc,
NULL,
CREATE_SUSPENDED, //线程在之后不会立刻执行
NULL
);
ResumeThread(FirstThread); //显式指定线程开始
WaitForSingleObject(FirstThread, -1); //等待线程结束

线程常见API

线程相关API作用
CreateThread创建
OpenThread打开
ExitThread退出
TerminateThread结束
SuspendThread暂停
ResumeThread恢复
GetCurrentProcess获取主程序进程
GetCurrentThread获取主程序线程
GetCurrentProcessId获取主程序进程ID
GetCurrentThreadId获取主程序线程ID

线程与时间戳

GetThreadTimes: 获取线程创建时候的时间戳,这不是一个我们认识的时间,而是一个秒数。
FileTimeToLocalFileTime: 时间戳转换为本地文件时间
FileTimeToSystemTime: 本地文件时间转换为系统时间

int main()
{
//获取主程序线程
HANDLE MyWindowThread = GetCurrentThread();
FILETIME CreationTime{ 0 };
FILETIME ExitTime{ 0 };
FILETIME KerneTime{ 0 };
FILETIME UserTime{ 0 };
FILETIME LocalTime{ 0 };
SYSTEMTIME sysTime{ 0 };
//获取线程的当前时间
GetThreadTimes(MyWindowThread, &CreationTime, &ExitTime, &KerneTime, &UserTime);
//转换时间为本地文件时间
FileTimeToLocalFileTime(&CreationTime,&LocalTime);
//转换时间为系统时间
FileTimeToSystemTime(&LocalTime, &sysTime);
return 0;
}

在这里插入图片描述

我们获取的是主程序的时间,同样的,我们也可以创建一个线程,然后获取时间。

线程同步问题

问题导入

多个线程访问相同资源的时候会产生冲突:
看下面的示例,按道理来说,每个线程进行10w次自增操作,最后它的结果应该是20W。
但是真的结果是这样吗?

#include <Windows.h>
#include <iostream>
HANDLE FirstThread = NULL;
HANDLE SecondThread = NULL;
int g_num1 = 0;
DWORD WINAPI ThreadProc1(
LPVOID lpParameter
)
{
for (int i = 0; i < 100000; i++)
{
g_num1++;
}
return 0;
}
DWORD WINAPI ThreadProc2(
LPVOID lpParameter
)
{
for (int i = 0; i < 100000; i++)
{
g_num1++;
}
return 0;
}
int main()
{
FirstThread = CreateThread(NULL,
NULL,
ThreadProc1,
NULL,
NULL,
NULL
);
SecondThread = CreateThread(NULL,
NULL,
ThreadProc2,
NULL,
NULL,
NULL
);
WaitForSingleObject(FirstThread, -1);
WaitForSingleObject(SecondThread, -1);
printf("g_num=%d\n", g_num1);
CloseHandle(FirstThread);
CloseHandle(SecondThread);
return 0;
}

可以看到结果远远不到20w。
在这里插入图片描述
在这里插入图片描述

为什么会产生这样的结果?

因为在并发的情况下,指令执行的先后顺序由内核决定。同一个线程的内部,指令按照先后顺序执行,但不同线程之间的指令很难说清楚哪一个会先执行。如果运行的结果依赖于不同线程执行的先后的话,那么就会造成竞争条件,在这样的状况下,计算机的结果很难预知,所以应该尽量避免竞争条件的形成。最常见的解决竞争条件的方法是将原先分离的两个指令构成不可分割的一个原子操作,而其他任务不能插入到原子操作中。


方式一: 原子操作

所谓的原子操作,就是该操作绝不会在执行完毕前被任何其他任务或事件打断,也就是说,它的最小的执行单位,不能有比它更小的执行单元,因此这里的原子实际是使用了物理学里物质微粒的概念。

使用原子操作函数:
自增函数: interlockedIncrement();
InterlockedDecrement();
InterlockedAnd();
InterlockedOr();
InterlockedXor();
InterlockedAdd();
InterlockedExchange();
InterlockedCompareExchange();
我们在线程中使用自增函数:

DWORD WINAPI ThreadProc1(
LPVOID lpParameter
)
{
for (int i = 0; i < 100000; i++)
{
InterlockedIncrement(&g_num1);
}
return 0;
}

原子可以保证我们在线程执行这次操作时中不会有任何打扰。不会被任何事件打断,只有它自己完成后,才进行下一个事件。
运行如下:
在这里插入图片描述


方式二:临界区

临界区:
原子操作仅仅能够解决某一个变量的问题,只能使得一个整数型数据做简单数据数据运算的时候是原子的,但是大部分时候我们想要的是一整段代码是原子操作,使用临界区就能解决这个问题。

1、什么是临界区?
答:每个进程中访问临界资源的那段程序称为临界区(临界资源是一次仅允许一个进程使用的共享资源)。每次只准许一个进程进入临界区,进入后不允许其他进程进入。

进入临界区

  • EnterCriticalSection

离开临界区

  • LeaveCriticalSection

初始化临界区

  • InitializeCriticalSection

销毁临界区

  • DeleteCriticalSection

critical_selction :一个作为临界区的结构体

DWORD WINAPI ThreadProc2(
LPVOID lpParameter
)
{
for (int i = 0; i < 100000; i++)
{
//进入临界区
EnterCriticalSection(&critical_selction);
g_num1++;
//退出临界区
LeaveCriticalSection(&critical_selction);
}
return 0;
}
int main()
{
//初始化临界区
InitializeCriticalSection(&critical_selction);
....
//销毁临界区
DeleteCriticalSection(&critical_selction);
return 0;
}

我们在每次执行我们操作的时候,都进入一次临界区,当我们执行完事件之后,再次退出临界区,我们不仅可以处理单个原子,还可以处理多条语句

运行如下:
在这里插入图片描述


方式三:互斥体

互斥体:
内核对象同步-等待函数原型

DWORD WaitForSingleObject(
HANDLE hHandle,内核对象句柄
DWORD dwMilliseconds等待时间
)
DWORD WaitForMultipleObjects(
DWORD nCount,//等待句柄数量 最大MAXIMUM_WAIT_OBJECTS=64
const HANDLE * lpHandles,//句柄数组
BOOL bWaitAll,//是否全部等待
DWORD dwMilliseconds//等待多久
);

互斥体有两个状态:激发态、非激发态。
互斥体有一个线程拥有权的概念。

  • 当一个互斥体没有被任何一个线程拥有时,它处于激发态,也可以说锁是打开的

  • 当一个线程调用了WaitForSingleObject函数会立刻返回,并将互斥体设置为非激发态互斥体被锁住,该线程获得拥有权。

  • 其它线程调用WaitForSingleObject函数的线程无法获得拥有权,只能一直等待互斥体,他们全部被阻塞。

  • 当线程A调用ReleaseMutex函数,将互斥体释放,即为解锁,此时互斥体不被任何对象拥有,被设置为激发态,会在等待它的线程中随机选择一个重复前面的步骤。

互斥体优点:

  1. 互斥体是内核对象,可以跨进程访问
  2. 互斥体比较安全,一旦拥有者崩溃,互斥体会立即处于激发态(可以被其他线程立刻调用)

示例:

DWORD WINAPI ThreadProc1(
LPVOID lpParameter
)
{
for (int i = 0; i < 100000; i++)
{
//等待处于激发态时,进入
WaitForSingleObject(MutexHandle, -1);
//执行 锁住,非激发态
g_num1++;
//解锁,重新设置为激发态
ReleaseMutex(MutexHandle);
}
return 0;
}
DWORD WINAPI ThreadProc2(
LPVOID lpParameter
)
{
for (int i = 0; i < 100000; i++)
{
WaitForSingleObject(MutexHandle, -1);
g_num1++;
ReleaseMutex(MutexHandle);
}
return 0;
}
int main()
{
//互斥体 全局变量MutexHandle
MutexHandle = CreateMutex(NULL, FALSE, NULL);
//未拥有所有权,处于激发态,锁是打开的,等待线程调用
....
return 0;
}

当我们创建一个互斥体时,它处于FALSE(第二个参数),初始时是激发状态(解锁),我们的两个线程会Wait,等待处于激发态(解锁状态),我们就可以拥有这个事件的所有权,并且设置为非激发态(锁住),执行我们在这个线程中的代码。
当我们执行完毕后,调用ReleaseMetux函数,重新设置为激发状态(解锁),然后等待下一个线程重新执行这个过程。

即:

解锁状态 ------ 锁住 ------ 解锁

运行如下:
在这里插入图片描述


方式四:事件处理

事件:
事件(Event)是在线程同步中最常使用的一种同步对象。


事件参数:

  • 使用计数
  • 表示自动重置/手动重置的布尔值
  • 事件有没有触发

事件对象有两种状态:1、手动状态。2、自动状态

手动状态事件对象的激发态和非激发态是由我们来控制自动状态与互斥体类似


事件与互斥体区别:

  1. 事件对象没有拥有者的概念,谁都可以操作事件对象的状态。
  2. 事件可以控制线程的执行顺序。

创建事件对象

  • CreateEventA

设置事件对象。

  • BOOL SetEvent(HANDLE hEvent);

案例如下:

#include <Windows.h>
#include <iostream>
HANDLE hEvent1;
HANDLE hEvent2;
HANDLE hEvent3;
DWORD WINAPI ThreadProc1(
LPVOID lpParameter
)
{
if (WAIT_OBJECT_0 == WaitForSingleObject(hEvent3, -1))
{
printf("线程一执行了\n");
SetEvent(hEvent1);
}
return 1;
}
DWORD WINAPI ThreadProc2(
LPVOID lpParameter
)
{
if (WAIT_OBJECT_0 == WaitForSingleObject(hEvent1, -1))
{
printf("线程二执行了\n");
SetEvent(hEvent2);
}
return 1;
}
DWORD WINAPI ThreadProc3(
LPVOID lpParameter
)
{
if (WAIT_OBJECT_0 == WaitForSingleObject(hEvent2, -1))
{
printf("线程三执行了\n");
SetEvent(hEvent3);
}
return 1;
}
int main()
{
hEvent1 = CreateEventW(NULL, FALSE, TRUE, NULL);
hEvent2 = CreateEventW(NULL, FALSE, FALSE, NULL);
hEvent3 = CreateEventW(NULL, FALSE, FALSE, NULL);
//初始1为有信号状态(解锁状态)
HANDLE Handle1 = CreateThread(NULL, NULL, ThreadProc1, NULL, NULL, NULL);
HANDLE Handle2 = CreateThread(NULL, NULL, ThreadProc2, NULL, NULL, NULL);
HANDLE Handle3 = CreateThread(NULL, NULL, ThreadProc3, NULL, NULL, NULL);
WaitForSingleObject(Handle1, -1);
WaitForSingleObject(Handle2,-1);
WaitForSingleObject(Handle3,-1);
CloseHandle(Handle1);
CloseHandle(Handle2);
CloseHandle(hEvent1);
CloseHandle(hEvent2);
CloseHandle(hEvent3);
return 0;
}

我们想让线程执行顺序: 一 二 三

  • 首先解锁一,二三为上锁状态,执行完后,设置下一个为解锁,重复这一过程:
    CreateEvent的第三个参数可以设置锁的初始状态:
    TRUE: 初始有信号状态,激发态,解锁状态
    FALSE: 初始无信号状态,非激发态,上锁状态
Event1 = CreateEvent(NULL, FALSE, TRUE, NULL);
Event2 = CreateEvent(NULL, FALSE, FALSE, NULL);
Event3 = CreateEvent(NULL, FALSE, FALSE, NULL);

在这里插入图片描述


我们想让线程执行顺序: 三 二 一
我们只需要修改线程中的接收顺序即可,即线程三Wait一个Event1,解锁Event2;线程二Wait一个Event2,解锁Event3;线程一Wait一个Event3,解锁Event1.
在这里插入图片描述


方式五:信号量

信号量创建:

  • CreateSemaphoreW

释放:

  • ReleaseSemaphore

打开信号量:

  • OpenSemaphoreW(

信号量也没有拥有者的概念,但是他有数量。
信号量有一个当前信号数,只要这个数不为0,信号量就处于激发态(解锁状态)
当有线程调用WaitForSingleObject后,信号数减1,如果不为0的话,再有线程调,WaitForSingleObject会继续上一把锁。
相反调用ReleaseSemaphoore会将信号量加1,。如果信号量为0,当有线程调用WaitForSingleObject时,线程会被阻塞。

HANDLE Semphore;
DWORD WINAPI ThreadProc1(
LPVOID lpParameter
)
{
if (WAIT_OBJECT_0 == WaitForSingleObject(Semphore, -1))
{
for (int i = 0; i < 10000; i++)
{
num++;
}
if (!ReleaseSemaphore(Semphore, 1, NULL))
{
return 0;
}
}
return 1;
}
DWORD WINAPI ThreadProc2(
LPVOID lpParameter
)
{
if (WAIT_OBJECT_0 == WaitForSingleObject(Semphore, -1))
{
for (int i = 0; i < 10000; i++)
{
num++;
}
if (!ReleaseSemaphore(Semphore, 1, NULL))
{
return 0;
}
}
return 1;
}
int main()
Semphore = CreateSemaphore(NULL, 1, 1, NULL);
HANDLE Handle[2]{NULL};
Handle[0] = CreateThread(NULL, NULL, ThreadProc1, NULL, NULL, NULL);
Handle[1] = CreateThread(NULL, NULL, ThreadProc2, NULL, NULL, NULL);
WaitForMultipleObjects(1, Handle, TRUE, -1);
return 0;
}

在这里插入图片描述

posted @   hugeYlh  阅读(38)  评论(0编辑  收藏  举报  
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具
点击右上角即可分享
微信分享提示