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;
}
posted @   Bluemultipl  阅读(342)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)
点击右上角即可分享
微信分享提示
主题色彩