Windows—线程基本知识和线程同步

2024-09-01 01:04

本文主要是介绍Windows—线程基本知识和线程同步,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

线程

线程的组成

  1. 线程的内核对象,操作系统用它来对线程实施管理。内核对象也是系统用来存放线程统计信息的地方。
  2. 线程堆栈,它用于维护线程在执行代码时需要的所有函数参数和局部变量

线程的进入点

每个线程必须拥有一个进入点函数,线程从这个进入点开始运行。

  1. 主线程的进入点函数:即main函数。
  2. 如果想要在你的进程中创建一个辅助线程,它必定也是个进入点函数,类似下面:
DWORD WINAPI ThreadFunc(PVOID pvParam) {DWORD dwResult = 0;cout << "hello thank you!" << endl;return dwResult;
}

你的线程函数可以执行你想要它做的任何任务。最终,线程函数到达它的结尾处并且返回。 这时,线程终止运行,该堆栈的内存被释放,同时,线程的内核对象的使用计数被递减。如果使用计数降为0,线程的内核对象就被撤消 。

使用线程注意事项

:::info

  1. 线程函数可以使用任何名字。如果在应用程序中拥有多个线程函数,必须为它们赋予不同的名字。
  2. 线程函数必须返回一个值,它将成为该线程的退出代码。
  3. 线程函数(实际上是你的所有函数)应该尽可能使用函数参数和局部变量。当使用静态变量和全局变量时,多个线程可以同时访问这些变量,可能破坏变量的内容。参数和局部变量是在线程堆栈中创建的,因此它们不太可能被另一个线程破坏。
    :::

CreateThread函数

HANDLE WINAPI CreateThread(_In_opt_ LPSECURITY_ATTRIBUTES lpThreadAttributes,//线程内核对象的默认安全属性,传递NULL。_In_ SIZE_T dwStackSize,//将多少地址空间用于线程自己的堆栈_In_ LPTHREAD_START_ROUTINE lpStartAddress,//线程函数地址,函数名_In_opt_ __drv_aliasesMem LPVOID lpParameter,//线程函数的参数_In_ DWORD dwCreationFlags,//0创建线程后立即调度_Out_opt_ LPDWORD lpThreadId //线程ID);
  1. 如果要创建一个或多个辅助函数,让一个已经在运行的线程调用CreateThread 。CreateThread被调用时,系统创建一个线程内核对象。该线程内核对象不是线程本身, 是操作系统用来管理线程的较小的数据结构。
  2. 系统从进程的地址空间中分配内存,供线程的堆栈使用。新线程运行的进程环境与创建线程的环境相同。新线程可以访问进程的内核对象的所有句柄、进程中的所有内存和在这个相同的进程中的所有其他线程的堆栈。
DWORD WINAPI ThreadFunc(PVOID pvParam) {DWORD dwResult = 0;cout << "hello thank you!" << endl;return dwResult;
}int main(){CreateThread(NULL,0,ThreadFunc,NULL,0,0); cout << "Hello World!"<<endl;
}

image.png

终止线程的运行

:::info

  1. 线程函数返回(最好使用这种方法)。
  2. 通过调用ExitThread函数,线程将自行撤消(最好不要使用这种方法)。
  3. 同一个进程或另一个进程中的线程调用TerminateThread函数(应该避免使用这种方法)。
  4. 包含线程的进程终止运行(应该避免使用这种方法)。
    :::

线程的初始化

image.png
:::info

  1. 调用CreateThread可使系统创建一个线程内核对 象。该对象的初始使用计数是2(在线程停止运行和从返回的句柄关闭之前,线程内核对象不会被撤消)。线程的内核对象的其他属性也被初始化,暂停计数被设置为1,退出代 码始终为STILL _ ACTIVE(0 x103),该对象设置为未通知状态.。
  2. 一旦内核对象创建完成,系统就分配用于线程的堆栈的内存。该内存是从进程的地址空间 分配而来的,因为线程并不拥有它自己的地址空间。然后系统将两个值写入新线程的堆栈的上端(线程堆栈总是从内存的高地址向低地址建立)。
  3. 写入堆栈的第一个值是传递给CreateThread 的pvParam参数的值。紧靠它的下面是传递给CreateThread的pfnStartAddr参数的值。
  4. 每个线程都有它自己的一组CPU寄存器,称为线程的上下文。该上下文反映了线程上次运 行时该线程的C PU寄存器的状态 。
  5. 指令指针和堆栈指针寄存器是线程上下文中两个最重要的寄存器。线程总是在进程 的上下文中运行的。因此,这些地址都用于标识拥有线程的进程地址空间中的内存。
    :::

线程的调度

sleep函数

  1. 可使线程自愿放弃它剩余的时间片。
  2. 系统将在大约的指定毫秒数内使线程不可调度。

线程优先级

image.png
:::info
一般来说,大多数时候高优先级的线程不应该处于可调度状态。当线程要进行某种操作时,它能迅速获得CPU时间。这时线程应该尽可能少地执行CPU指令,并返回睡眠状态,等待再次变成可调度状态。相反,低优先级的线程可以保持可调度状态, 执行大量的CPU指令来进行它的操作。如果按照这些原则来办,整个操作系统就能正确地对用户作出响应。
:::

进程优先级

image.png

线程同步

什么时候需要线程同步

  1. 当有多个线程访问共享资源而不使资源被破坏时。
  2. 当一个线程需要将某个任务已经完成的情况通知另外一个或多个线程时。

什么是句柄hHandle

  1. Windows之所以要设立句柄,根本上源于内存管理机制的问题,即虚拟地址。数据的地址需要变动,变动以后就需要有人来记录、管理变动,因此系统用句柄来记载数据地址的变更
  2. 句柄是一种特殊的智能指针,当一个应用程序要引用其他系统(如数据库、操作系统)所管理的内存块或对象时,就要使用句柄。
  3. 句柄一般是指获取另一个对象的方法—一个广义的指针,它的具体形式可能是一个整数、一个对象或就是一个真实的指针,目的就是建立起与被访问对象之间的唯一的联系 。在C++中,要访问一个对象,通常可以建立一个指向对象的指针。但是在很多具体的应用中,直接用指针代表对象并不是一个好的解决方案。
  4. 是一个用来标识对象或者项目的标识符,可以用来描述窗体、文件等,值得注意的是句柄不能是常量 。句柄是Windows系统中对象或实例的标识,这些对象包括模块、应用程序实例、窗口、控件、位图、GDI对象、资源、文件等。
  5. 从数据类型上来看它只是一个16位的无符号整数。应用程序总是通过调用Windows API获得一个句柄,之后其他 Windows函数就可以使用该句柄,以引用和操作相应的内核对象。句柄可以像指针那样置空,那样句柄就没有任何意义,不代表任何内核对象。
  6. 在 Windows程序中并不是用物理地址来标识一个内存块、文件、任务或动态装入模块的。Windows API给这些项目分配确定的句柄,并将句柄返回给应用程序,然后通过句柄来进行操作

线程同步的方式

:::info

  1. 原子操作
  2. 临界区
  3. 互斥锁
  4. 信号量
  5. 条件变量
  6. 事件
  7. 读写锁
  8. 自旋锁
  9. 共享内存
  10. 屏障
    :::

实例

不同步例子

输出居然x=1

int x = 0;DWORD WINAPI FirstThread(PVOID pvParam) { x++;return 0;
}DWORD WINAPI SecondThread(PVOID pvParam) {x++;return 0;
}int main(){//主线程,线程1,线程2谁先运行完不知道CreateThread(NULL,0, FirstThread,NULL,0,0);CreateThread(NULL, 0, SecondThread, NULL, 0, 0);cout << "x="<<x<<endl;return 0;
}

原子操作

原子访问:指线程在访问资源时能够确保所有其他线程都不在同一时间内访问相同的资源。

long x = 0;DWORD WINAPI FirstThread(PVOID pvParam) { InterlockedExchangeAdd(&x, 1);return 0;
}DWORD WINAPI SecondThread(PVOID pvParam) {InterlockedExchangeAdd(&x, 1);return 0;
}int main(){CreateThread(NULL,0, FirstThread,NULL,0,0);CreateThread(NULL, 0, SecondThread, NULL, 0, 0);Sleep(1);//主线程等等子线程cout << "x="<<x<<endl;return 0;
}

临界区

:::info
保证在某一时刻只有一个线程能访问数据的简便办法。在任意时刻只允许一个线程对共享资源进行访问。如果有多个线程试图同时访问临界区,那么在有一个线程进入后其他所有试图访问此临界区的线程将被挂起,并一直持续到进入临界区的线程离开。临界区在被释放后,其他线程可以继续抢占,以此达到用原子方式操作共享资源的目的。
:::

临界区和互斥量的区别

:::info
1、临界区只能用于对象在同一进程里线程间的互斥访问;互斥体可以用于对象进程间或线程间的互斥访问。
2、临界区是非内核对象,只在用户态进行锁操作,速度快;互斥体是内核对象,在核心态进行锁操作,速度慢。
3、临界区和互斥体在Windows平台下都可用;Linux下只有互斥体可用。
4、临界区:通过对多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问。
5、互斥量:为协调共同对一个共享资源的单独访问而设计的。
:::

临界区的使用步骤

:::info
1 . 申请一个临界区变量 CRITICAL_SECTION gSection;
2. 初始化临界区 InitializeCriticalSection(&gSection);
3. 使用临界区 EnterCriticalSection(&gSection); …省略代码…LeaveCriticalSection(&gSection);
4.释放临界区 DeleteCriticalSection(&gSection);
:::

形象化解释

:::info

  1. 临界区就像飞机上的厕所,抽水马桶是要保护的数据。厕所很小, 每次只能一个人(线程)进入厕所(关键代码段)使用抽水马桶(受保护的资源)。 如果有多个资源被一道使用,可以将它们全部放在一个厕所里,也就是说可以创建一 个CRITICAL_SECTION结构来保护所有的资源 。
  2. 如果有多个不是一道使用的资源,比如线程1和线程2访问一个资源,而线程1和线程3访问 另一个资源,那么应该为每个资源创建一个独立的厕所,即CRITICAL_SECTION结构。
  3. 无论在何处拥有需要访问资源的代码,都必须调用CRITICAL_SECTION函数,为它传递用于标识该资源的CRITICAL_SECTION结构的地址。当一个线程需要访问一 个资源时,它首先必须检查厕所门上的“有人”标志。CRITICAL_SECTION结构用于标识线 程想要进入哪个厕所,而EnterCriticalSection函数则是线程用来检查“有人”标志的函数 。
  4. 如果EnterCriticalSection函数发现厕所中没有任何别的线程(门上的标志显示“无人”), 那么调用线程就可以使用该资源。如果EnterCriticalSection发现厕所中有另一个线程正在使用, 那么调用函数必须在厕所门的外面等待,直到厕所中的另一个线程离开厕所。
  5. 当一个线程不再执行需要访问资源的代码时,它应该调用LeaveCriti calSection函数。告诉系统它准备离开包含该资源的厕所。如果忘记调用LeaveCriti calSection,系统将认 为该线程仍然在厕所中,因此不允许其他正在等待的线程进入厕所。这就像离开了厕所但没有 换上“无人”的标志 。
    :::
long x = 0;CRITICAL_SECTION cs;DWORD WINAPI FirstThread(PVOID pvParam) {EnterCriticalSection(&cs);x++;LeaveCriticalSection(&cs);return 0;
}DWORD WINAPI SecondThread(PVOID pvParam) {EnterCriticalSection(&cs);x++;LeaveCriticalSection(&cs);return 0;
}int main() {//初始化临界区InitializeCriticalSection(&cs);//创建线程,绑定入口函数HANDLE handle1=CreateThread(NULL, 0, FirstThread, NULL, 0, 0);HANDLE handle2=CreateThread(NULL, 0, SecondThread, NULL, 0, 0);//主线程等待子线程结束,就不用特意Sleep了WaitForSingleObject(handle1, INFINITE);WaitForSingleObject(handle2, INFINITE);//关闭线程句柄CloseHandle(handle1);CloseHandle(handle2);//销毁DeleteCriticalSection(&cs);cout << "x=" << x << endl;return 0;
}
临界区的优点

:::info

  1. 描述: 临界区是Windows系统中的轻量级同步对象,允许多个线程互斥访问共享资源。与互斥锁类似,但性能较好,适用于单进程中的线程同步。
  2. 优点: 比互斥锁更快,不涉及内核对象的开销。
  3. 缺点: 只能用于同一进程内的线程同步,不能跨进程使用
    :::
不关闭线程句柄的影响

:::info
如果不关闭线程句柄,可能会导致系统资源泄漏,特别是在创建大量线程的情况下。这是因为每个线程句柄占用系统资源,并且在不再需要该线程时,应该通过CloseHandle来释放这些资源。如果你不关闭线程句柄,资源会一直占用,直到你的进程结束。
:::

互斥锁

:::info

  1. 描述: 互斥锁是一种保护共享资源的同步原语。在同一时刻,只允许一个线程持有互斥锁,从而保证对共享资源的互斥访问。
  2. 优点: 简单易用,适合对资源进行独占式访问的场景。
  3. 缺点: 可能导致死锁(如果多个线程互相等待对方释放锁)
    :::
#include<mutex>
long x = 0;
mutex mx;DWORD WINAPI FirstThread(PVOID pvParam) {mx.lock();x++;mx.unlock();return 0;
}DWORD WINAPI SecondThread(PVOID pvParam) {mx.lock();x++;mx.unlock();return 0;
}int main() {//创建线程,绑定入口函数HANDLE handle1=CreateThread(NULL, 0, FirstThread, NULL, 0, 0);HANDLE handle2=CreateThread(NULL, 0, SecondThread, NULL, 0, 0);//主线程等待子线程结束WaitForSingleObject(handle1, INFINITE);WaitForSingleObject(handle2, INFINITE);//关闭线程句柄CloseHandle(handle1);CloseHandle(handle2);cout << "x=" << x << endl;return 0;
}

信号量

:::info

  1. 描述: 信号量允许多个线程访问共享资源,可以控制访问资源的线程数目。计数信号量用于控制同时访问特定资源的线程数量,而二进制信号量相当于一个可以在两种状态之间切换的锁。
  2. 优点: 可以限制资源的并发访问,适合用于限流或生产者-消费者模式。
  3. 缺点: 比互斥锁复杂,需要小心管理信号量的计数。
    :::
#include <iostream>
#include <windows.h>using namespace std;long x = 0;
HANDLE semaphore;DWORD WINAPI FirstThread(PVOID pvParam) {WaitForSingleObject(semaphore, INFINITE);  // 等待信号量,进入临界区x++;  // 修改共享资源ReleaseSemaphore(semaphore, 1, NULL);  // 释放信号量,离开临界区return 0;
}DWORD WINAPI SecondThread(PVOID pvParam) {WaitForSingleObject(semaphore, INFINITE);  // 等待信号量,进入临界区x++;  // 修改共享资源ReleaseSemaphore(semaphore, 1, NULL);  // 释放信号量,离开临界区return 0;
}int main() {semaphore = CreateSemaphore(NULL, 1, 1, NULL);  // 创建信号量,初始值为1,最大值为1// 创建线程,绑定入口函数HANDLE handle1 = CreateThread(NULL, 0, FirstThread, NULL, 0, 0);HANDLE handle2 = CreateThread(NULL, 0, SecondThread, NULL, 0, 0);// 主线程等待子线程结束WaitForSingleObject(handle1, INFINITE);WaitForSingleObject(handle2, INFINITE);// 关闭线程句柄CloseHandle(handle1);CloseHandle(handle2);// 关闭信号量句柄CloseHandle(semaphore);cout << "x = " << x << endl;return 0;
}

:::info

  • CreateSemaphore: 初始化信号量,初始值和最大值都为1,这使得信号量在同一时刻只允许一个线程进入临界区,起到了类似互斥锁的作用。
  • WaitForSingleObject: 等待信号量,信号量计数减1,如果当前值为0则线程阻塞,直到其他线程释放信号量。
  • ReleaseSemaphore: 释放信号量,信号量计数加1,允许其他等待的线程进入临界区。
  • CloseHandle: 在使用完信号量后,关闭信号量句柄,释放资源。
    :::
CreateSemaphore
  1. lInitialCount:信号量的初始计数值,表示信号量的可用资源数量。当该值为0时,任何调用WaitForSingleObject的线程都会立即被阻塞,直到其他线程调用ReleaseSemaphore。
  2. lMaximumCount:信号量计数的最大值,表示信号量允许的最大资源数量。ReleaseSemaphore调用时,计数值不能超过这个值,否则函数调用失败。
HANDLE CreateSemaphore(LPSECURITY_ATTRIBUTES lpSemaphoreAttributes, // 安全属性 (可选)LONG lInitialCount,                          // 初始计数LONG lMaximumCount,                          // 最大计数LPCWSTR lpName                               // 信号量名称 (可选)
);
ReleaseSemaphore

lReleaseCount: 释放的计数值,即增加信号量计数器的值。此值必须大于0,表示你希望释放的资源数量。例如,如果设置为1,信号量计数增加1,释放一个资源 。

BOOL ReleaseSemaphore(HANDLE hSemaphore,        // 信号量句柄LONG lReleaseCount,       // 释放的计数LPLONG lpPreviousCount    // 上一个计数 (可选)
);

条件变量

:::info

  1. 描述: 条件变量允许线程在某个条件不满足时等待,条件满足时通知其他线程继续执行。条件变量通常与互斥锁配合使用。
  2. 优点: 适合用于需要等待某个条件发生的场景,如线程间的复杂协调。
  3. 缺点: 使用复杂度较高,尤其是在处理多个条件时。
    :::
#include <iostream>
#include <windows.h>
#include <condition_variable>
#include <mutex>using namespace std;long x = 0;
mutex mtx;
condition_variable cv;
bool ready = true;  // 初始状态为true, 第一个线程可以进入临界区DWORD WINAPI FirstThread(PVOID pvParam) {unique_lock<mutex> lock(mtx);cv.wait(lock, [] { return ready; });  // 等待条件变量,条件满足时继续执行ready = false;  // 进入临界区后将条件设为false,阻止其他线程进入x++;  // 修改共享资源ready = true;  // 修改完共享资源后恢复条件,允许其他线程进入lock.unlock();  // 解锁互斥锁cv.notify_one();  // 通知其他等待线程return 0;
}DWORD WINAPI SecondThread(PVOID pvParam) {unique_lock<mutex> lock(mtx);cv.wait(lock, [] { return ready; });  // 等待条件变量,条件满足时继续执行ready = false;  // 进入临界区后将条件设为false,阻止其他线程进入x++;  // 修改共享资源ready = true;  // 修改完共享资源后恢复条件,允许其他线程进入lock.unlock();  // 解锁互斥锁cv.notify_one();  // 通知其他等待线程return 0;
}int main() {// 创建线程,绑定入口函数HANDLE handle1 = CreateThread(NULL, 0, FirstThread, NULL, 0, 0);HANDLE handle2 = CreateThread(NULL, 0, SecondThread, NULL, 0, 0);// 主线程等待子线程结束WaitForSingleObject(handle1, INFINITE);WaitForSingleObject(handle2, INFINITE);// 关闭线程句柄CloseHandle(handle1);CloseHandle(handle2);cout << "x = " << x << endl;return 0;
}

事件

:::info

  1. 描述: 事件是一种同步机制,可以在一个线程中设置事件状态,另一个线程中等待事件的触发。事件有自动重置和手动重置两种类型。
  2. 优点: 可以在线程间发出信号通知,非常适合用于一个线程通知多个线程的场景。
  3. 缺点: 需要额外的资源管理。
    :::
#include <iostream>
#include <windows.h>using namespace std;long x = 0;
HANDLE eventHandle;DWORD WINAPI FirstThread(PVOID pvParam) {WaitForSingleObject(eventHandle, INFINITE);  // 等待事件变为有信号状态x++;  // 修改共享资源SetEvent(eventHandle);  // 设置事件为有信号状态,允许其他线程继续执行return 0;
}DWORD WINAPI SecondThread(PVOID pvParam) {WaitForSingleObject(eventHandle, INFINITE);  // 等待事件变为有信号状态x++;  // 修改共享资源SetEvent(eventHandle);  // 设置事件为有信号状态,允许其他线程继续执行return 0;
}int main() {// 创建自动重置事件对象,初始状态为有信号状态eventHandle = CreateEvent(NULL, FALSE, TRUE, NULL);// 创建线程,绑定入口函数HANDLE handle1 = CreateThread(NULL, 0, FirstThread, NULL, 0, 0);HANDLE handle2 = CreateThread(NULL, 0, SecondThread, NULL, 0, 0);// 主线程等待子线程结束WaitForSingleObject(handle1, INFINITE);WaitForSingleObject(handle2, INFINITE);// 关闭线程句柄CloseHandle(handle1);CloseHandle(handle2);// 关闭事件句柄CloseHandle(eventHandle);cout << "x = " << x << endl;return 0;
}

:::info
事件在某个线程调用WaitForSingleObject并成功进入临界区后,自动重置为无信号状态,阻止其他线程进入临界区,确保线程间的互斥访问
:::

CreateEvent
/** 功能:创建一个事件对象。(有人说创建或打开一个命名的或无名的事件对象,当名字为参4时,会返回已打开的事件对象,但是我下面的案例测试是无法根据参4(无论是否为NULL都不行)获取已打开的对象)。* 返回值:返回一个句柄HANDLE。* 参1:属性,一般传NULL即可。* 参2:是否设置手动改变事件状态。false自动,true手动。* 参3:状态的初始值,分为无状态和有状态,false代表无状态,true代表有状态。* 参4:事件的名字,可以为NULL。
*/
CreateEventW(_In_opt_ LPSECURITY_ATTRIBUTES lpEventAttributes,_In_ BOOL bManualReset,_In_ BOOL bInitialState,_In_opt_ LPCWSTR lpName
);
SetEvent
/* * 功能:设置状态为有状态。* 返回值:1成功,0失败,该返回值实际意义不大。* 参1:一个内核对象的句柄,不过主要是Event。
*/ 
SetEvent(_In_ HANDLE hEvent
);
ResetEvent
/** 功能:设置状态为无状态。* 返回值:1成功,0失败,该返回值实际意义不大。* 参1:一个内核对象的句柄,不过主要是Event。
*/
ResetEvent(_In_ HANDLE hEvent
);
WaitForSingleObject
/** 功能:阻塞等待状态改变返回。* 返回值:返回DWORD的值,一般使用宏去判断,若立即返回,返回值为WAIT_OBJECT_0;超时返回WAIT_TIMEOUT;失败返回WAIT_FAILED。* 参1:一个内核对象的句柄,可以是Event,Mutex,Semaphore(信号量),Process,Thread。* 参2:等待时长,单位ms。* * 注意:参2的取值:* 1)传0:表示不阻塞,立即返回,返回值为WAIT_OBJECT_0。* 2)传>0:阻塞时长,超时时返回WAIT_TIMEOUT。* 3)传INFINITE:表示一直阻塞,直到等待句柄的状态发生改变。
*/ 
WaitForSingleObject(_In_ HANDLE hHandle,_In_ DWORD dwMilliseconds
);
案例

:::info
需求:主线程通过唤醒线程2后退出while循环,线程2等到event2后唤醒线程1,线程1等待event1,线程1是手动改变状态,调用完WaitForSingleObject是无法自动改变状态为无状态,所以最后退出时必须手动调用ResetEvent将状态改变为无状态。最后主线程由于两个线程都退出后,主线程就会退出循环,并且回收句柄。注意主线程阻塞等待的是两个线程,而两个线程等待的是事件。
:::

#include <iostream>
#include <string>
#include <windows.h>
#include <tchar.h>
using namespace std;HANDLE h_event1 = NULL;
HANDLE h_event2 = NULL;
DWORD  FunProc1(LPVOID lpParameter);
DWORD  FunProc2(LPVOID lpParameter);//h_event1初始状态为无信号时,WaitForSingleObject(h_event1, 300)
DWORD  FunProc1(LPVOID lpParameter)
{cout << "线程1开始运行。\n" << endl;while (1){int ret = WaitForSingleObject(h_event1, 7000);if (WAIT_OBJECT_0 == ret) {cout << "线程1等到event1\n" << endl;break;}else if (WAIT_TIMEOUT == ret) {cout << "线程1等待event1超时\n" << endl;}else if (WAIT_FAILED == ret) {cout << "线程1调用WaitForSingleObject失败\n" << endl;}else {cout << "线程1调用WaitForSingleObject返回未知结果\n" << endl;break;}}cout << "线程1等到了event1,线程1结束。\n" << endl;ResetEvent(h_event1);//因为创建事件时信号改变设置为手动改变,所以必须自动调用改变为无信号return 0;
}DWORD FunProc2(LPVOID lpParameter)
{cout << "线程2开始运行。\n" << endl;while (1) {int ret = WaitForSingleObject(h_event2, 3000);//因为创建事件设置为自动,收到信号不阻塞后,该函数返回自动将状态改为无信号状态if (WAIT_OBJECT_0 == ret) {cout << "线程2等到event2\n" << endl;break;}else if (WAIT_TIMEOUT == ret) {cout << "线程2等待event2超时\n" << endl;}else if (WAIT_FAILED == ret) {cout << "线程2调用WaitForSingleObject失败\n" << endl;}else {cout << "线程2调用WaitForSingleObject返回未知结果\n" << endl;break;}}cout << "线程2等到了event2,线程2结束,并唤醒线程1。\n" << endl;Sleep(350);SetEvent(h_event1);return 0;
}int main(int argc, char** argv)
{h_event1 = CreateEvent(NULL, true, false, _T("event_one"));//参2代表设置手动改变状态,参3代表初始状态为无状态h_event2 = CreateEvent(NULL, false, false, _T("event_two"));//参2代表设置自动改变状态HANDLE hThread1;hThread1 = CreateThread(NULL, 0, FunProc1, NULL, 0, NULL);HANDLE hThread2;hThread2 = CreateThread(NULL, 0, FunProc2, NULL, 0, NULL);Sleep(5000);SetEvent(h_event2);//线程1或者线程2都没退出继续等待,注意每个线程阻塞过程中收到信号改变立马不阻塞,并且结束的线程下次调用WaitForSingleObject直接返回while (WaitForSingleObject(hThread1, 150) != WAIT_OBJECT_0 || WaitForSingleObject(hThread2, 150) != WAIT_OBJECT_0){cout << "线程还没有结束,主程序等了150ms。\n" << endl;}cout << "主线程等待两个子线程结束完毕" << endl;CloseHandle(hThread1);CloseHandle(hThread2);CloseHandle(h_event1);CloseHandle(h_event2);system("pause");return 0;
}
//改成INFINITE就是一直阻塞
WaitForMultiObject
/** 功能:同样是阻塞等待状态改变返回。* 返回值:返回DWORD的值,一般使用宏去判断,若立即返回,返回值为WAIT_OBJECT_0;超时返回WAIT_TIMEOUT;失败返回WAIT_FAILED。* 参1:句柄的数量,最大值为MAXIMUM_WAIT_OBJECTS(64),可以是Event,Mutex,Semaphore(信号量),Process,Thread。* 参2:句柄数组的指针。* 参3:等待的类型,如果为TRUE 则等待所有信号量有效再往下执行,FALSE 当有其中一个信号量有效时就向下执行。* 参4:等待时长,单位ms。 * 注意:参4的取值:* 1)传0:表示不阻塞,立即返回,返回值为WAIT_OBJECT_0。* 2)传>0:阻塞时长,超时时返回WAIT_TIMEOUT。* 3)传INFINITE:表示一直阻塞,直到等待句柄的状态发生改变。
*/
WaitForMultipleObjects(_In_ DWORD nCount,_In_reads_(nCount) CONST HANDLE* lpHandles,_In_ BOOL bWaitAll,_In_ DWORD dwMilliseconds
);
案例
m_threadShow = std::thread(std::mem_fn(&MainWindow::ShowData), this);MainWindow::~MainWindow()
{SetEvent(m_KillEvent);if(m_threadShow.joinable())m_threadShow.join();delete ui;
}void MainWindow::ShowData()
{while(1){HANDLE sigs[2] = {m_KillEvent, m_showEvent};const DWORD ret = WaitForMultipleObjects(2, sigs, false, dwMilliseconds);switch (ret) {case WAIT_OBJECT_0: {//数组的第一个事件:m_KillEvent发生会来到这里break;}case WAIT_OBJECT_0 + 1: {//数组的第二个事件:m_showEvent发生会来到这里break;}case WAIT_TIMEOUT: {// 超时来到这里}default: {// 其它未知返回值的处理}}
}

读写锁

:::info

  • 描述: 读写锁允许多个线程同时读取共享资源,但在写入时,只有一个线程可以独占访问。读锁和写锁是分开的。
  • 优点: 提高了读操作的并发性,适合读多写少的场景。
  • 缺点: 如果写操作过多,可能会导致读操作被阻塞
    :::
#include <iostream>
#include <windows.h>using namespace std;long x = 0;
SRWLOCK rwLock;DWORD WINAPI FirstThread(PVOID pvParam) {AcquireSRWLockExclusive(&rwLock);  // 获取写锁,进入临界区x++;  // 修改共享资源ReleaseSRWLockExclusive(&rwLock);  // 释放写锁,离开临界区return 0;
}DWORD WINAPI SecondThread(PVOID pvParam) {AcquireSRWLockExclusive(&rwLock);  // 获取写锁,进入临界区x++;  // 修改共享资源ReleaseSRWLockExclusive(&rwLock);  // 释放写锁,离开临界区return 0;
}int main() {InitializeSRWLock(&rwLock);  // 初始化读写锁// 创建线程,绑定入口函数HANDLE handle1 = CreateThread(NULL, 0, FirstThread, NULL, 0, 0);HANDLE handle2 = CreateThread(NULL, 0, SecondThread, NULL, 0, 0);// 主线程等待子线程结束WaitForSingleObject(handle1, INFINITE);WaitForSingleObject(handle2, INFINITE);// 关闭线程句柄CloseHandle(handle1);CloseHandle(handle2);cout << "x = " << x << endl;return 0;
}

自旋锁

:::info

  • 描述: 自旋锁是一种忙等待的锁,线程在尝试获取锁时会不断循环检查锁的状态,直到成功获取锁。适合在锁争用时间很短的场景。
  • 优点: 无需线程切换,适用于短期锁定的场景。
  • 缺点: 可能浪费CPU时间,容易导致忙等问题
    :::
#include <iostream>
#include <atomic>
#include <windows.h>using namespace std;long x = 0;
std::atomic_flag lock = ATOMIC_FLAG_INIT;  // 初始化自旋锁void AcquireSpinLock() {// 自旋直到获取锁while (lock.test_and_set(std::memory_order_acquire)) {// 可以插入短暂的暂停来降低CPU占用率Sleep(0);  // 暂停一段时间,让其他线程有机会运行}
}void ReleaseSpinLock() {lock.clear(std::memory_order_release);  // 释放锁
}DWORD WINAPI FirstThread(PVOID pvParam) {AcquireSpinLock();  // 获取自旋锁x++;  // 修改共享资源ReleaseSpinLock();  // 释放自旋锁return 0;
}DWORD WINAPI SecondThread(PVOID pvParam) {AcquireSpinLock();  // 获取自旋锁x++;  // 修改共享资源ReleaseSpinLock();  // 释放自旋锁return 0;
}int main() {// 创建线程,绑定入口函数HANDLE handle1 = CreateThread(NULL, 0, FirstThread, NULL, 0, NULL);HANDLE handle2 = CreateThread(NULL, 0, SecondThread, NULL, 0, NULL);// 主线程等待子线程结束WaitForSingleObject(handle1, INFINITE);WaitForSingleObject(handle2, INFINITE);// 关闭线程句柄CloseHandle(handle1);CloseHandle(handle2);cout << "x = " << x << endl;return 0;
}

:::info

Sleep(0) 的作用:
  1. 降低 CPU 占用: 自旋锁的忙等循环会占用大量的 CPU 资源,而 Sleep(0) 使线程暂时放弃 CPU 资源,减少无用的忙等,允许其他线程在同一时间片内执行,从而提高整体系统的响应性。
  2. 线程调度: Sleep(0) 给操作系统提供了一个机会来调度其他线程。这样,即使自旋锁的持有者线程由于某些原因没有释放锁,等待的线程也不会完全浪费 CPU 资源在忙等上。
    :::
自旋锁和互斥锁的区别

:::info

  1. 自旋锁:

优点: 在锁竞争不激烈和临界区很短的情况下,开销小,性能高。
缺点: 如果锁竞争激烈或持有时间长,会导致高 CPU 占用和性能下降。

  1. 互斥锁:

优点: 在锁竞争高和临界区较长的情况下表现良好,能够有效地处理线程阻塞和上下文切换。
缺点: 可能引入额外的上下文切换开销,在锁竞争低的情况下性能较差
:::

共享内存

#include <iostream>
#include <windows.h>using namespace std;const int BUFFER_SIZE = sizeof(long);  // 共享内存的大小
long* sharedMemory;  // 指向共享内存区域的指针
HANDLE hMapFile;     // 文件映射对象的句柄
HANDLE hMutex;       // 互斥锁的句柄DWORD WINAPI FirstThread(PVOID pvParam) {WaitForSingleObject(hMutex, INFINITE);  // 获取互斥锁(*sharedMemory)++;  // 修改共享资源ReleaseMutex(hMutex);  // 释放互斥锁return 0;
}DWORD WINAPI SecondThread(PVOID pvParam) {WaitForSingleObject(hMutex, INFINITE);  // 获取互斥锁(*sharedMemory)++;  // 修改共享资源ReleaseMutex(hMutex);  // 释放互斥锁return 0;
}int main() {// 创建文件映射对象hMapFile = CreateFileMapping(INVALID_HANDLE_VALUE,    // 使用系统分页文件NULL,                    // 默认安全属性PAGE_READWRITE,          // 可读写0,                       // 最大对象大小(高 32 位)BUFFER_SIZE,             // 最大对象大小(低 32 位)TEXT("SharedMemoryName") // 名称);if (hMapFile == NULL) {cerr << "Could not create file mapping object. Error code: " << GetLastError() << endl;return 1;}// 映射视图到进程地址空间sharedMemory = (long*) MapViewOfFile(hMapFile,                 // 文件映射对象的句柄FILE_MAP_ALL_ACCESS,      // 可读写权限0,                        // 文件映射偏移(高 32 位)0,                        // 文件映射偏移(低 32 位)BUFFER_SIZE               // 映射的大小);if (sharedMemory == NULL) {cerr << "Could not map view of file. Error code: " << GetLastError() << endl;CloseHandle(hMapFile);return 1;}// 创建互斥锁hMutex = CreateMutex(NULL,            // 默认安全属性FALSE,           // 不拥有初始状态TEXT("Global\\MyMutex") // 名称);if (hMutex == NULL) {cerr << "Could not create mutex. Error code: " << GetLastError() << endl;UnmapViewOfFile(sharedMemory);CloseHandle(hMapFile);return 1;}// 初始化共享内存区域*sharedMemory = 0;// 创建线程HANDLE handle1 = CreateThread(NULL, 0, FirstThread, NULL, 0, NULL);HANDLE handle2 = CreateThread(NULL, 0, SecondThread, NULL, 0, NULL);if (handle1 == NULL || handle2 == NULL) {cerr << "Could not create threads. Error code: " << GetLastError() << endl;CloseHandle(hMutex);UnmapViewOfFile(sharedMemory);CloseHandle(hMapFile);return 1;}// 主线程等待子线程结束WaitForSingleObject(handle1, INFINITE);WaitForSingleObject(handle2, INFINITE);// 关闭线程句柄CloseHandle(handle1);CloseHandle(handle2);// 输出结果cout << "x = " << *sharedMemory << endl;// 清理资源CloseHandle(hMutex);UnmapViewOfFile(sharedMemory);CloseHandle(hMapFile);return 0;
}

屏障

:::info

  • 描述: 屏障允许多个线程在达到一个同步点之前阻塞,直到所有线程都到达这个点时,才能继续执行。适用于并行计算中的阶段性同步。
  • 优点: 可以强制多个线程在同一时刻达到同步点,适用于阶段性任务。
  • 缺点: 可能会导致线程在等待期间浪费资源。
    :::
#include <iostream>
#include <windows.h>using namespace std;int x = 0;
HANDLE hBarrierEvent; // 屏障事件句柄DWORD WINAPI FirstThread(PVOID pvParam) { x++;SetEvent(hBarrierEvent); // 线程1完成工作,通知主线程return 0;
}DWORD WINAPI SecondThread(PVOID pvParam) {x++;SetEvent(hBarrierEvent); // 线程2完成工作,通知主线程return 0;
}int main(){// 创建一个事件用于线程同步hBarrierEvent = CreateEvent(NULL, TRUE, FALSE, NULL);// 主线程,线程1,线程2谁先运行完不知道HANDLE hThread1 = CreateThread(NULL, 0, FirstThread, NULL, 0, NULL);HANDLE hThread2 = CreateThread(NULL, 0, SecondThread, NULL, 0, NULL);// 等待两个线程完成WaitForSingleObject(hThread1, INFINITE);WaitForSingleObject(hThread2, INFINITE);// 等待屏障事件被设置WaitForSingleObject(hBarrierEvent, INFINITE);cout << "x=" << x << endl;// 释放资源CloseHandle(hThread1);CloseHandle(hThread2);CloseHandle(hBarrierEvent);return 0;
}

内核对象同步

  1. 当进程正在运行的时候,进程内核对象处于未通知状态,当进程终止运行的时候,它就变 为已通知状态。进程内核对象中是个布尔值,当对象创建时,该值被初始化为FALSE(未通知状态)。当进程终止运行时,操作系统自动将对应的对象布尔值改为TRUE,表示该对象已经得到通知
  2. 线程可以使自己进入等待状态,直到一个对象变为已通知状态。注意,用于控制每个对象 的已通知/未通知状态的规则要根据对象的类型而定。前面已经提到进程和线程对象的规则及作业的规则。

image.png

这篇关于Windows—线程基本知识和线程同步的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1125500

相关文章

基于MySQL Binlog的Elasticsearch数据同步实践

一、为什么要做 随着马蜂窝的逐渐发展,我们的业务数据越来越多,单纯使用 MySQL 已经不能满足我们的数据查询需求,例如对于商品、订单等数据的多维度检索。 使用 Elasticsearch 存储业务数据可以很好的解决我们业务中的搜索需求。而数据进行异构存储后,随之而来的就是数据同步的问题。 二、现有方法及问题 对于数据同步,我们目前的解决方案是建立数据中间表。把需要检索的业务数据,统一放到一张M

服务器集群同步时间手记

1.时间服务器配置(必须root用户) (1)检查ntp是否安装 [root@node1 桌面]# rpm -qa|grep ntpntp-4.2.6p5-10.el6.centos.x86_64fontpackages-filesystem-1.41-1.1.el6.noarchntpdate-4.2.6p5-10.el6.centos.x86_64 (2)修改ntp配置文件 [r

关于数据埋点,你需要了解这些基本知识

产品汪每天都在和数据打交道,你知道数据来自哪里吗? 移动app端内的用户行为数据大多来自埋点,了解一些埋点知识,能和数据分析师、技术侃大山,参与到前期的数据采集,更重要是让最终的埋点数据能为我所用,否则可怜巴巴等上几个月是常有的事。   埋点类型 根据埋点方式,可以区分为: 手动埋点半自动埋点全自动埋点 秉承“任何事物都有两面性”的道理:自动程度高的,能解决通用统计,便于统一化管理,但个性化定

在 Windows 上部署 gitblit

在 Windows 上部署 gitblit 在 Windows 上部署 gitblit 缘起gitblit 是什么安装JDK部署 gitblit 下载 gitblit 并解压配置登录注册为 windows 服务 修改 installService.cmd 文件运行 installService.cmd运行 gitblitw.exe查看 services.msc 缘起

Windows如何添加右键新建菜单

Windows如何添加右键新建菜单 文章目录 Windows如何添加右键新建菜单实验环境缘起以新建`.md`文件为例第一步第二步第三步 总结 实验环境 Windows7 缘起 因为我习惯用 Markdown 格式写文本,每次新建一个.txt后都要手动修改为.md,真的麻烦。如何在右键新建菜单中添加.md选项呢? 网上有很多方法,这些方法我都尝试了,要么太麻烦,要么不凑效

Windows下Nginx的安装及开机启动

1、将nginx-1.16.1.zip解压拷贝至D:\web\nginx目录下。 2、启动Nginx,两种方法: (1)直接双击nginx.exe,双击后一个黑色的弹窗一闪而过。 (2)打开cmd命令窗口,切换到nginx目录下,输入命令 nginx.exe 或者 start nginx ,回车即可。 3、检查nginx是否启动成功。 直接在浏览器地址栏输入网址 http://lo

线程的四种操作

所属专栏:Java学习        1. 线程的开启 start和run的区别: run:描述了线程要执行的任务,也可以称为线程的入口 start:调用系统函数,真正的在系统内核中创建线程(创建PCB,加入到链表中),此处的start会根据不同的系统,分别调用不同的api,创建好之后的线程,再单独去执行run(所以说,start的本质是调用系统api,系统的api

java线程深度解析(六)——线程池技术

http://blog.csdn.net/Daybreak1209/article/details/51382604 一种最为简单的线程创建和回收的方法: [html]  view plain copy new Thread(new Runnable(){                @Override               public voi

java线程深度解析(五)——并发模型(生产者-消费者)

http://blog.csdn.net/Daybreak1209/article/details/51378055 三、生产者-消费者模式     在经典的多线程模式中,生产者-消费者为多线程间协作提供了良好的解决方案。基本原理是两类线程,即若干个生产者和若干个消费者,生产者负责提交用户请求任务(到内存缓冲区),消费者线程负责处理任务(从内存缓冲区中取任务进行处理),两类线程之

java线程深度解析(四)——并发模型(Master-Worker)

http://blog.csdn.net/daybreak1209/article/details/51372929 二、Master-worker ——分而治之      Master-worker常用的并行模式之一,核心思想是由两个进程协作工作,master负责接收和分配任务,worker负责处理任务,并把处理结果返回给Master进程,由Master进行汇总,返回给客