win32api之线程知识梳理(四)
什么是线程
线程是附属在进程上的执行实体, 是代码的执行流程
一个进程可以包含多个线程, 但一个进程至少要包含一个线程
进程与线程的关系
可以将进程比作一个房子,它是一个容器,可以包含很多个线程(居住者)同时工作。线程可以在进程中进行交互和共享资源(房间、厨房等)。与居住在房子里的人一样,线程需要执行某些任务,并且它们可以使用相同的内存和资源来完成它们的工作
线程涉及API
CreateThread
CreateThread
函数用于创建一个新线程,该线程在进程空间内独立运行。该函数返回新线程的句柄,以及线程的唯一标识符
在调用该函数后,需要通过 CloseHandle
函数关闭线程句柄,否则会导致资源泄漏
HANDLE CreateThread(
LPSECURITY_ATTRIBUTES lpThreadAttributes, //线程安全属性,可设置为 NULL
SIZE_T dwStackSize, //新线程的栈大小,若为 0 则使用默认大小
LPTHREAD_START_ROUTINE lpStartAddress, //线程函数的地址,即新线程所要执行的函数
LPVOID lpParameter, //传递给线程函数的参数
DWORD dwCreationFlags, //控制线程创建的标志,如是否立即启动线程等
LPDWORD lpThreadId //返回值,指向接收线程标识符的变量
);
线程函数
线程函数是线程执行的代码,它会在调用CreateThread
函数创建线程后被调用, 其返回值为DOWRD类型, 此值会传递给GetExitCodeThread
函数。如果线程函数执行完毕后不返回任何值,则默认返回0
DWORD WINAPI ThreadProc(LPVOID lpParameter)
{
return 0;
}
以下是一个创建线程并给先传递参数的实例,CreateThread
函数创建了一个新的线程并传递了一个 int
类型的参数 count
。新线程的入口点是 ThreadProc
函数,该函数接受一个 LPVOID
类型的参数 lpParam
,在这里将其转换为 int*
类型的指针,然后使用该参数进行迭代计数
include <Windows.h>
include <stdio.h>
DWORD WINAPI ThreadProc(LPVOID lpParam) {
int* pCount = (int*)lpParam;
for (int i = 0; i < *pCount; i++) {
printf("Thread: %d\n", i + 1);
Sleep(1000);
}
return 0;
}
int main() {
int count = 5;
HANDLE hThread;
DWORD threadId;
hThread = CreateThread(NULL, 0, ThreadProc, &count, 0, &threadId);
if (hThread == NULL) {
printf("Failed to create thread (%d)\n", GetLastError());
return 1;
}
WaitForSingleObject(hThread, INFINITE);
CloseHandle(hThread);
return 0;
}
GetExitCodeThread
GetExitCodeThread函数用于获取指定线程的退出代码, 其语法格式如下所示:
BOOL GetExitCodeThread(
HANDLE hThread, //要查询退出代码的线程句柄
LPDWORD lpExitCode //指向一个变量的指针,用于接收线程的退出代码
);
此函数的使用实例如下:
include <Windows.h>
include <stdio.h>
DWORD WINAPI ThreadProc(LPVOID lpParameter)
{
for (int i = 0; i < 5; i++)
{
Sleep(500);
printf("%d\n",i);
}
//printf("Hello from new thread!\n");
return 1;
}
int main()
{
DWORD i; //用于接收线程的退出代码
hThread = CreateThread(NULL, 0, ThreadProc, NULL, 0, NULL);
WaitForSingleObject(hThread, INFINITE);
GetExitCodeThread(hThread, &i); //获取线程的退出代码
printf("线程退出代码是:%d", i);
return 0;
}
线程参数的生命周期
创建线程时需要注意向线程传递的参数的生命周期。一般情况下,线程创建后会立即运行,并且在运行过程中可能需要使用传递进来的参数。如果传递的参数的生命周期比线程短,当线程需要使用参数时,参数可能已经失效了,导致程序出错。因此,一般的做法是将参数拷贝一份给线程,在线程中使用拷贝的参数,确保参数的有效性
假设我们要创建一个线程来打印一个字符串,我们需要将该字符串作为参数传递给线程。但是,如果该字符串是在主线程中声明并初始化的局部变量,那么当主线程完成时,该字符串将被销毁,这可能会导致在线程中引用该字符串时出现问题
为了避免这种情况,可以通过以下方式解决:在主线程中使用动态内存分配函数(例如malloc)为字符串分配内存,将字符串的指针作为参数传递给线程,然后在线程完成后手动释放内存。这样,即使主线程完成并销毁了该字符串,线程仍然可以访问该字符串所在的内存空间。下面是一个示例代码
include <Windows.h>
include <stdio.h>
DWORD WINAPI PrintString(LPVOID lpParam)
{
char* str = (char*)lpParam;
printf("%s\n", str);
return 0;
}
int main()
{
char* str = (char*)malloc(sizeof(char) * 20);
strcpy_s(str, 20, "Hello, World!");
HANDLE hThread = CreateThread(NULL, 0, PrintString, str, 0, NULL);
if (hThread == NULL)
{
printf("Failed to create thread, error code: %d\n", GetLastError());
return 1;
}
WaitForSingleObject(hThread, INFINITE);
free(str);
CloseHandle(hThread);
return 0;
}
线程控制函数
SuspendThread
SuspendThread
函数用于暂停线程, 其语法格式如下
SuspendThread(
_In_ HANDLE hThread //线程句柄
);
ResumeThread
ResumeThread
函数用于恢复线程, 其语法格式如下:
ResumeThread(
_In_ HANDLE hThread //线程句柄
);
WaitForSingleObject
WaitForSingleObject函数是一个Windows API函数,它可以等待一个指定的内核对象变为可用。它的作用是使当前线程暂停执行,直到指定的内核对象变为有信号(signaled)状态,或者直到超时时间已过。简单来说就是等待指定线程执行结束后当前线程才能恢复执行
其语法格式如下所示:
DWORD WaitForSingleObject(
HANDLE hHandle, //句柄
DWORD dwMilliseconds //超时时间
);
WaitForMutipleObjects
与WaitForSingleObjects函数不同的是, WaitForMutipleObjects函数可支持等待多个线程执行结束, 或者等待多个线程中其中一个执行结束
其语法格式如下:
DWORD WaitForMultipleObjects(
DWORD nCount, //等待的句柄数量,即lphandles数组中句柄的个数
const HANDLE *lpHandles, //要等待的对象的句柄数组
BOOL bWaitAll, //该值为TRUE时,只有在所有对象都变为可用之后才返回;当该值为FALSE时,只要有一个对象变为可用就返回
DWORD dwMilliseconds //当该值为零时,函数不等待并立即返回。当该值为 INFINITE 时,函数无限期地等待直到句柄数组中有一个对象变为可用,或者等待失败
);
以下是WaitForMutipleObjects
函数的使用实例
include <Windows.h>
include <stdio.h>
DWORD WINAPI ThreadProc(LPVOID lpParameter)
{
for (int i = 0; i < 5; i++)
{
Sleep(500);
printf("%d\n",i);
}
return 1;
}
int main()
{
DWORD i;
HANDLE arrThread[2];
arrThread[0] = CreateThread(NULL, 0, ThreadProc, NULL, 0, NULL);
arrThread[1] = CreateThread(NULL, 0, ThreadProc, NULL, 0, NULL);
WaitForMultipleObjects(2,arrThread,TRUE,INFINITE); //等待以上2个线程执行结束
printf("两个线程执行完毕 \n");
// 关闭线程句柄
CloseHandle(arrThread[0]);
CloseHandle(arrThread[1]);
return 0;
}
线程上下文
什么是线程上下文
线程上下文(Thread Context)是指在一个线程中,当前执行代码的相关信息集合,包括寄存器状态、程序计数器、线程优先级、线程的上下文安全堆栈等等。
要注意的是,若要获取线程上下文信息,需要先将线程挂起。
CONTEXT结构
CONTEXT
是Windows API中用于保存线程上下文的结构体,包含了处理器的寄存器、标志和其他与处理器相关的状态信息。
由于CONTEXT
结构体成员太多了, 可以通过设置ContextFlags
成员的值来获取指定范围的寄存器, 以下是常用的寄存器集的描述:
CONTEXT_INTEGER
: 包含通用寄存器集(如EAX、EBX等)和指令指针EIP。CONTEXT_CONTROL
: 包含指令指针EIP、代码段寄存器CS、栈指针ESP和栈段寄存器SS。CONTEXT_SEGMENTS
: 包含数据段寄存器DS、源段寄存器SS、堆栈段寄存器SS和附加段寄存器ES、FS和GS。CONTEXT_FLOATING_POINT
: 包含浮点寄存器集。CONTEXT_DEBUG_REGISTERS
: 包含调试寄存器集。
可以通过GetThreadContext
和SetThreadContext
函数来获取和设置线程上下文
GetThreadContext
用于获取指定线程的上下文信息,调用成功后会将获取到的上下文信息存储在CONTEXT结构体中,其语法格式如下所示:
BOOL GetThreadContext(
_In_ HANDLE hThread, //线程句柄
_Inout_ LPCONTEXT lpContext //Context结构体指针
);
SetThreadContext
用于设置指定线程的上下文,即线程寄存器和指令指针等信息,其语法结构如下所示:
BOOL SetThreadContext(
_In_ HANDLE hThread, //线程句柄
_In_ CONST CONTEXT* lpContext //Context结构体指针
);
使用实例
include <Windows.h>
include <stdio.h>
DWORD WINAPI ThreadProc(LPVOID lpParameter)
{
printf("Hello from new thread!\n");
return 0;
}
int main()
{
HANDLE hThread = CreateThread(
NULL, // 默认安全性描述符
0, // 默认堆栈大小
ThreadProc, // 线程函数
NULL, // 线程参数
0, // 立即启动线程
NULL // 不返回线程标识符
);
if (hThread == NULL)
{
printf("Failed to create thread (%d)\n", GetLastError());
return 1;
}
SuspendThread(hThread); //暂停
CONTEXT context; //定义线程上下文结构体
context.ContextFlags = CONTEXT_INTEGER; //设置线程上下文的寄存器值为CONTEXT_INTEGER
GetThreadContext(hThread, &context); //获取线程上下文
printf("eax的值为%x", context.Eax); //输出寄存器eax的值
ResumeThread(hThread); //恢复线程
// 等待线程结束
WaitForSingleObject(hThread, INFINITE);
// 关闭线程句柄
CloseHandle(hThread);
return 0;
}
临界区
什么是临界区
当多个线程同时使用同一个资源(全局变量)时, 很可能会出现某些错误, 这时我们可以将这个资源变成临界资源, 然后通过临界区来使用这个临界资源
临界区(critical section)是操作系统用于同步线程之间访问共享资源的一种机制,是一段被保护的代码区域。同一时刻只允许一个线程进入临界区,其他线程需要等待当前线程退出临界区才能进入执行。
当线程进入临界区时,它会对临界资源进行加锁,这时其他线程无法访问该资源。只有当当前线程完成对临界资源的访问并释放锁后,其他线程才能进入临界区访问临界资源
什么是线程锁
线程锁实际上就是基于临界区实现的,通过在临界区代码块前加锁,在代码块结束后释放锁来保证临界区的互斥性
以下是实现线程锁的代码流程:
1.定义一个临界区
CRITICAL_SECTION cs;
2.初始化临界区
InitializeCriticalSection(&cs)
3.定义临界区范围
EnterCriticalSection(&cs);
//使用临界资源
LeaveCriticalSection(&cs);
使用实例
以下代码是关于线程锁使用的实例, 线程1的代码执行完毕后线程2才能执行自己的代码
include <Windows.h>
include <stdio.h>
include "psapi.h"
define ARRAY_SIZE 1024
int Tickets = 10; //定义一个全局变量,表示票数
CRITICAL_SECTION cs; //定义临界区
DWORD WINAPI ThreadProc(LPVOID IpParameter) {
EnterCriticalSection(&cs); //进入临界区
while (Tickets>0)
{
printf("还有%d张票,", Tickets);
Tickets--;
printf("卖出去一张,还剩%d张\n", Tickets);
}
LeaveCriticalSection(&cs); //离开临界区
return 0;
}
int main()
{
InitializeCriticalSection(&cs); //初始化临界区
HANDLE arrThread[2];
arrThread[0] = CreateThread(NULL, 0, ThreadProc, NULL, 0, NULL); //创建线程1
arrThread[1] = CreateThread(NULL, 0, ThreadProc, NULL, 0, NULL); //创建线程2
WaitForMultipleObjects(2, arrThread, TRUE, INFINITE);
CloseHandle(arrThread[0]);
CloseHandle(arrThread[1]);
return 0;
}
互斥体
什么是互斥体
如下图所示, 互斥体是一种用于控制线程同步的对象, 它能确保同一时刻只有一个线程进入临界区访问临界资源。
互斥体通过两种状态来控制对共享资源的访问, 分别是已锁定(0)和未锁定(1), 当一个线程从互斥体中获取锁(令牌), 其他线程就无法访问共享资源, 只有线程释放了锁(令牌), 其他线程才能继续竞争获取锁
在Windows系统中, 互斥体的实现是一个内核对象, 因此它可以跨进程使用
涉及API
CreateMutex
CreateMutex函数用于创建或者打开一个互斥体对象,其语法格式如下:
HANDLE CreateMutex(
LPSECURITY_ATTRIBUTES lpMutexAttributes, //一个指向SECURITY_ATTRIBUTES结构体的指针,用于指定新的互斥对象是否可以被继承
BOOL bInitialOwner, //指定互斥体对象的初始状态,若为Flase,则创建的互斥体是有信号的(其他进程的等待线程可以使用它);若为True则是无信号的(其他进程的等待线程必须等待互斥体被释放后才能使用它)
LPCTSTR lpName
);
ReleaseMutex
ReleaseMutex函数用来释放进程持有的互斥体对象,其语法格式如下:
BOOL ReleaseMutex(
HANDLE hMutex //互斥体句柄
);
使用实例
以下代码使用互斥体实现跨进程线程同步, 首先是进程A的代码, 此处为了方便测试, 我利用getchar()
来阻塞代码执行(相当于断点), 这样互斥体就没有释放锁
//进程A
include <iostream>
include <windows.h>
int main()
{
//创建互斥体
HANDLE hMutex = CreateMutex(NULL,FALSE,TEXT("mutex"));
//获取锁
WaitForSingleObject(hMutex, INFINITE);
for (int i = 0; i < 5; i++)
{
printf("进程A的x线程\n");
}
getchar();
//释放锁
ReleaseMutex(hMutex);
}
下面是进程B的代码, 由于进程A的代码还没有释放锁, 因此进程B的代码无法执行
//进程B
include <iostream>
include <windows.h>
int main()
{
//创建互斥体
HANDLE hMutex = CreateMutex(NULL, FALSE, TEXT("mutex"));
//获取锁
WaitForSingleObject(hMutex, INFINITE);
for (int i = 0; i < 5; i++)
{
printf("进程B的y线程\n");
}
//释放锁
ReleaseMutex(hMutex);
}
互斥体和线程锁的区别
- 互斥体可以用于跨进程的线程同步,线程锁只能用于同一进程内的线程同步
- 互斥体可以设置等待超时, 而线程锁不行。当一个线程在执行过程中因为异常情况(例如程序崩溃)而突然终止时,如果它持有了某个共享资源的互斥体,其他线程在等待这个资源时可能会进入无限等待状态,因为该互斥体没有被释放。为了避免这种情况,互斥体通常会在创建时指定一个超时时间,一旦等待时间超过了这个时间,等待线程就会放弃等待并执行其他任务,从而避免了无限等待的情况发生
- 互斥体的效率没有线程锁的高
事件
什么是事件
事件是一种同步对象,用于线程之间的通信和协调。事件对象有两种状态:有信号状态和无信号状态。当事件对象处于有信号状态时,等待该事件的线程可以被唤醒并继续执行。当事件对象处于无信号状态时,等待该事件的线程将被阻塞,直到事件被信号化为止
通常,一个线程使用 SetEvent 函数将事件对象信号化,而另一个或多个线程使用 WaitForSingleObject 或 WaitForMultipleObjects 函数等待该事件对象的信号状态
涉及api
CreateEvent
CreateEvent
函数用于创建一个事件对象, 事件对象是内核对象的一种,可用于同步进程和线程,或者通知线程事件的发生
其语法如下所示:
HANDLE CreateEvent(
LPSECURITY_ATTRIBUTES lpEventAttributes, //安全描述符
BOOL bManualReset, //指定事件对象的重置类型,TRUE表手动重置,FALSE表自动重置
BOOL bInitialState, //事件创建出来是否是有信号的,True表示有信号,False表示无信号
LPCTSTR lpName //指定事件的名称
);
关于第二个参数的描述:当事件对象处于非信号状态时,一个线程调用WaitForSingleObject等待该事件;如果事件已经处于信号状态,则WaitForSingleObject返回。在自动重置模式下,当WaitForSingleObject返回时,事件会自动返回到非信号状态。而在手动重置模式下,事件会一直保持在信号状态,直到由调用ResetEvent
显式重置
SetEvent
SetEvent
函数于设置事件对象为有信号状态, 若事件对象处于无信号状态, 则将其设置为有信号状态, 若事件对象处于有信号状态,则函数不起作用。此函数通常与CreateEvent
函数一起使用来实现线程同步, 在线程同步中,此函数的作用就是把自己的线程挂起, 同时唤醒其他线程
其语法格式如下:
BOOL SetEvent(
HANDLE hEvent //要设置事件对象的句柄
);
使用实例
这段代码实现了一个生产者-消费者的解决方案,其中两个线程分别为生产者线程和消费者线程,通过共享的仓库(Storage变量)实现数据交互。
生产者线程不断地生产产品,存储到仓库中,并唤醒消费者线程,消费者线程不断地从仓库中消耗产品,并唤醒生产者线程
include <iostream>
include <windows.h>
int ProductMax = 10; //生产数量
int Storage = 0; //产品仓库,每次只能存储一个产品
HANDLE EventProduct, EventConsume; //定义事件
//生产者线程
DWORD WINAPI ThreadProduct(LPVOID Parameter) {
for (int i = 0; i < ProductMax; i++)
{
WaitForSingleObject(EventProduct,INFINITE);
Storage = 1; //仓库置1
printf("生产者生产了1个产品\n");
SetEvent(EventConsume); //挂起生产者线程,唤醒消费者线程
}
return 0;
}
//消费者线程
DWORD WINAPI ThreadConsume(LPVOID Parameter) {
for (int i = 0; i < ProductMax; i++)
{
WaitForSingleObject(EventConsume, INFINITE);
Storage = 0; //仓库置0
printf("消费者消耗了1个产品\n");
SetEvent(EventProduct); //挂起消费者线程,唤醒生产者线程
}
return 0;
}
int main()
{
//创建事件
EventProduct = CreateEvent(NULL, FALSE, TRUE, NULL);
EventConsume = CreateEvent(NULL, FALSE, FALSE, NULL);
//创建线程
HANDLE hThread[2];
hThread[0] = CreateThread(NULL, 0, ThreadProduct, NULL, 0, NULL);
hThread[1] = CreateThread(NULL, 0, ThreadConsume, NULL, 0, NULL);
WaitForMultipleObjects(2, hThread, TRUE, INFINITE);
//释放线程
CloseHandle(hThread[0]);
CloseHandle(hThread[1]);
}
由下述执行结果可看出, 两个线程有序的完成了任务, 生产者每生产一个产品, 消费者就消耗一个产品。两个线程交替执行, 而不会出现某个线程同时执行多次
线程互斥和线程同步
- 线程同步是指协调多个线程的执行顺序,以避免在并发执行时出现不一致的结果或冲突的情况。线程同步可以通过多种机制实现,如互斥锁、信号量、事件等。在线程同步中,通常会存在一些共享资源,多个线程需要协调访问这些共享资源,以避免访问的冲突
- 线程互斥是一种线程同步机制,用于确保同一时间只有一个线程能够访问共享资源。线程互斥可以使用互斥体、临界区、信号量等机制来实现