C++ 线程操作
Windows 线程是可以执行的代码的实例,系统是以线程为单位调度程序;一个程序当中可以有多个线程,实现多任务的处理。每个线程都具有一个 ID ,每个线程具有自己的内存栈,同一进程中的线程使用同一个地址空间。程序将 CPU 执行时间划分成时间片,一次根据时间片执行不同的线程。
创建线程
线程函数定义在 windows.h
头文件中,有两个重要的线程函数:
// 创建线程,返回线程句柄
HANDLE CreateThread(
LPSECURITY_ATTRIBUTES lpThreadAttributes, // 安全属性
SIZE_T dwStackSize, // 线程栈大小,为 0 则自动分配
LPTHREAD_START_ROUTINE lpStartAddress, // 线程处理函数
LPVOID lpParameter, // 传递给线程处理函数的参数
DWORD dwCreationFlags, // 线程的创建方式, 0 立即启动,CREATE_SUSPENDED
LPDWORD lpThreadId // 创建成功,返回线程 ID
);
// 定义线程处理函数
DWORD WINAPI ThreadProc(
LPVOID lpParameter // 创建线程时,传递给线程的参数
);
其中 CreateThread 需要传入一个线程处理函数,它返回一个线程句柄
使用范例
#include <windows.h>
#include <iostream>
using namespace std;
// 线程处理函数
DWORD WINAPI ThreadProc(
LPVOID pParam // 创建线程时,传递给线程的参数
)
{
// 将获得的参数转换为传入参数的类型
char* pszText = (char*)pParam;
// 循环打印字符串
while (true)
{
cout << pszText << endl;
Sleep(1000);
}
return 0;
}
int main()
{
DWORD nID = 0;
char* pszText = "Hello";
HANDLE hThread = CreateThread(NULL, 0, TestProc, pszText, 0, &nID);
return 0;
}
线程函数
线程有众多的处理函数,我们列举其中常见的几种:
线程挂起,通过线程句柄暂停线程
DWORD SuspendThread(
HANDLE hThread
);
反之,可以通过线程句柄恢复线程
DWORD ResumeThread(
HANDLE hThread
);
当不需要线程时,可以在线程外部结束线程
BOOL TerminateThread(
HANDLE hThread, // 线程句柄
DWORD dwExitCode // 退出代码
);
当然,也可以在线程处理函数内部结束线程
VOID ExitThread(
DWORD dwExitCode // 当前进程的退出代码
);
获取当前线程 ID
DWORD GetCurrentThreadId();
获取当前线程句柄
HANDLE GetCurrentThread();
线程信号
线程句柄执行时无信号,执行结束时有信号,这是一种便于根据线程执行情况进行操作的方法。
通过以下函数,我们可以实现阻断主程序运行的目的。等待单个句柄有信号,即需要等该线程执行完毕,或者到达等待时间后才能执行该函数之后的代码:
// 等候单个句柄(可等候句柄)有信号
VOID WaitForSingleObject(
HANDLE handle, // 句柄 BUFF 地址
DWORD dwMilliseconds // 最长等候时间(毫秒)INFINITE(无限大)
);
同时等候多个句柄有信号,意味着只有当符合等候方式的情况出现才能执行之后的代码:
DWORD WaitForMultipleObjects(
DWORD nCount, // 句柄数量
CONST HANDLE *lpHandles, // 句柄 BUFF 地址
BOOL bWaitAll, // 等候方式
DWORD dwMilliseconds // 最长等候时间(毫秒)
);
等候方式:
- TRUE:所有句柄有信号则返回
- FALSE:一个句柄有信号则返回
线程同步
原子锁
多个线程对同一个数据进行原子操作,会产生结果丢失。例如,当线程 A 执行 g_value++ 时,如果线程切换时间正好是在线程 A 将值保存到 g_value 之前,线程 B 继续执行 g_vlaue++ ,那么当线程 A 再次被切换回来之后,会将原来线程 A 保存的值保存到 g_value 上,线程 B 进行的加法操作将被覆盖。因此需要使用原子锁函数解决这一问题。
原子锁直接对数据所在的内存操作,并且在任何一个瞬间只能有一个线程访问。例如加减法的原子锁:
InterlockedIncrement(LONG *Addend); // 锁定用于加法的给定的地址,已锁定的地址可以重复锁定
InterlockedDecrement(LONG *Subend); // 锁定用于减法的给定的地址
原子锁非常繁琐,每一种运算都有一个单独的原子锁函数,但相应的原子锁效率较高
互斥
多线程下代码或资源的可能需要共享使用,这时就需要使用互斥。首先创建并存储互斥句柄
// 创建成功返回互斥句柄
HANDLE CreateMutex(
LPSECURITY_ATTRIBUTES lpMutexAttributes, // 安全属性
BOOL bInitialOwner, // 是否是初始拥有者 TRUE/FALSE
LPCTSTR lpName, // 命名
);
然后通过之前的等待函数,遵循谁先等候谁先获取的原则,从而使得它们能够同步执行。当需要释放互斥
BOOL ReleaseMutex(
HANDLE hMutex // 互斥句柄
);
最后关闭互斥句柄
BOOL CloseHandle(
HANDLE hObject //句柄
);
互斥的特点
- 任何一个时间点只有一个互斥
- 当线程拥有互斥,互斥句柄无信号;当任何线程没有互斥,互斥句柄有信号
我们举一个使用互斥的例子:通过互斥,我们实现两个线程依次执行的目的。需要注意,在 main 函数中添加 getchar 是为了防止 main 函数提前返回,导致子线程终止;输入一个字符终止程序
#include <windows.h>
#include <iostream>
using namespace std;
HANDLE g_hMutex; // 接收互斥句柄
DWORD CALLBACK TestProc1(LPVOID pParam)
{
while (TRUE)
{
// 等待互斥有信号,否则阻塞
WaitForSingleObject(g_hMutex, INFINITE);
// 通过阻塞,同时线程 1 拥有互斥
cout << (char*)pParam << endl;
Sleep(1000);
// 释放互斥
ReleaseMutex(g_hMutex);
}
return 0;
}
DWORD CALLBACK TestProc2(LPVOID pParam)
{
while (TRUE)
{
// 等待互斥有信号,否则阻塞
WaitForSingleObject(g_hMutex, INFINITE);
// 通过阻塞,同时线程 2 拥有互斥
cout << (char*)pParam << endl;
Sleep(1000);
// 释放互斥
ReleaseMutex(g_hMutex);
}
return 0;
}
int main()
{
// 所有线程不拥有互斥,互斥有信号
g_hMutex = CreateMutex(NULL, FALSE, NULL);
DWORD nID = 0;
char* pszText1 = "Hello";
char* pszText2 = "World";
HANDLE hThread1 = CreateThread(NULL, 0, TestProc1, pszText1, 0, &nID);
HANDLE hThread2 = CreateThread(NULL, 0, TestProc2, pszText2, 0, &nID);
getchar();
// 关闭互斥
CloseHandle(g_hMutex);
return 0;
}
事件
多线程运行不可能是各做各的,因此程序之间需要进行互相通知,进行通知的方法就是事件。首先创建事件
// 创建成功返回事件句柄
HANDLE CreateEvent(
LPSECURITY_ATTRIBUTES lpEventAttributes, // 安全属性
BOOL bManualReset, // 事件重置(复位)方式
BOOL bInitialState, // 事件初始状态,TRUE 有信号
LPCTSTR lpName // 事件名
);
复位方式
- TRUE 手动
- FALSE 自动
同样需要通过等待函数同步事件,然后对事件进行触发
BOOL SetEvent(
HANDLE hEvent // 事件句柄
);
触发事件会将事件设置成有信号状态;相应的,当需要重新使用事件,需要进行复位
BOOL ResetEvent(
HANDLE hEvent // 事件句柄
);
复位事件会将事件设置成无信号状态。最后关闭事件
BOOL CloseHandle(
HANDLE hObject //句柄
);
我们举一个使用事件的例子:其中线程 2 会每隔 1 秒发送一次事件,事件有信号,因而线程 1 得以执行;输入一个字符退出程序
#include <windows.h>
#include <iostream>
using namespace std;
HANDLE g_hEvent; //接收事件句柄
DWORD CALLBACK TestProc1(LPVOID pParam)
{
while (TRUE)
{
// 等待事件有信号,否则阻塞
WaitForSingleObject(g_hEvent, INFINITE);
// 触发事件后复位事件(如果设置自动复位则不需要调用)
ResetEvent(g_hEvent);
cout << (char*)pParam << endl;
}
return 0;
}
DWORD CALLBACK TestProc2(LPVOID pParam)
{
while (TRUE)
{
Sleep(1000);
// 每隔一秒触发事件,事件有信号
SetEvent(g_hEvent);
}
return 0;
}
int main()
{
// 手动复位的事件,初始无信号
g_hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
DWORD nID = 0;
char* pszText1 = "Hello";
char* pszText2 = "World";
HANDLE hThread1 = CreateThread(NULL, 0, TestProc1, pszText1, 0, &nID);
HANDLE hThread2 = CreateThread(NULL, 0, TestProc2, pszText2, 0, &nID);
getchar();
// 关闭互斥
CloseHandle(g_hEvent);
return 0;
}
信号量
我们也可以利用信号量解决通知问题,但提供一个计数器,可以设置次数。首先创建信号量
// 成功返回信号量句柄
HANDLE CreateSemaphore(
LPSECURITY_ATTRIBUTES lpSemaphoreAttributes, // 安全属性
LONG lInitialCount, // 初始化信号量数量
LONG lMaximumCount, // 信号量的最大值
LPCTSTR lpName // 命名
);
然后等待信号量,每等候通过一次,信号量的信号减 1 ,给信号量指定计数值;最后关闭句柄
BOOL ReleaseSemaphore(
HANDLE hSemaphore, // 信号量句柄
LONG lReleaseCount, // 指定信号量释放数量
LPLONG lpPreviousCount // 释放前原来信号量的数量,可以为 NULL(这个参数实际是一个“返回值”)
);
我们举一个使用信号量的例子:首先线程会根据信号量执行 3 次,当输入一个字符后,我们重新设定信号量为 5 ,这样线程又会执行 5 次,最后输入一个字符退出程序
#include <iostream>
#include <windows.h>
using namespace std;
HANDLE g_hSema; // 信号量句柄
DWORD CALLBACK TestProc(LPVOID pParam)
{
while (TRUE)
{
// 等待信号量信号
WaitForSingleObject(g_hSema, INFINITE);
cout << "Hello" << endl;
}
return 0;
}
int main()
{
//创建信号量,子线程会执行 3 次
g_hSema = CreateSemaphore(NULL, 3, 10, NULL);
DWORD nID = 0;
HANDLE hThread = CreateThread(NULL, 0, TestProc, NULL, 0, &nID);
getchar();
ReleaseSemaphore(g_hSema, 5, NULL); // 重新指定信号量数量为 5,这样子线程会再执行 5 次
getchar();
CloseHandle(g_hSema); //关闭信号量
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)