【线程安全问题】线程互斥与线程同步技术

在达内Windows/Win32编程专栏中,我们已经介绍过线程同步与线程互斥技术,包括了原子锁,互斥体,事件和信号量。但是与海哥讲的线程同步与线程互斥技术不太一样,这篇文章来带领大家学习线程同步与线程互斥技术,包含了【Windows线程开发】Windows线程同步技术文章中的技术和海哥讲的技术,来系统了解一下线程同步与线程互斥技术。

本篇文章包含了线程同步技术(事件,信号量)和线程互斥技术(原子锁,临界区,互斥体)。之前写过相关文章,但是在最近逆向的时候发现还是不熟悉,而且之前的文章中讲到的技术与海哥讲的技术不是很贴合,今天写一篇文章来系统学习一下线程同步与线程互斥技术。

文章目录

  • 线程互斥
    • 一. 原子锁
    • 二. 临界区
      • 1. 创建关键节对象
      • 2.初始化关键节对象
      • 3. 进入关键节
      • 4. 离开关键节
      • 5. 释放关键节资源
    • 三.互斥体
      • 1. 创建互斥体
      • 2. 多线程使用互斥体
    • 四. 事件
      • 1. 创建事件
      • 2.多线程中使用事件
    • 五. 信号量
      • 1. 创建信号量
      • 2. 等待信号量对象
      • 3.增加信号量数量计数
      • 4.关闭信号量

线程互斥

一. 原子锁

原子锁我们在这里介绍到了,但是严格来说,它不属于线程互斥对象。
我们先来看看存在线程安全问题的一个控制台程序:

#include 
#include 

DWORD WINAPI ThreadProc(LPVOID);

int g_value = 0;

int main()
{
	HANDLE Thread[2] = { 0 };
	Thread[0]=CreateThread(NULL, 0, ThreadProc, 0, NULL, NULL);
	Thread[1]=CreateThread(NULL, 0, ThreadProc, 0, NULL, NULL);
	WaitForMultipleObjects(2, Thread, TRUE, INFINITE);
	printf("%d", g_value);
	return 0;
}
DWORD WINAPI ThreadProc(LPVOID lpParameter) {
	for (int i = 0; i < 10000000; i++) {
		g_value++;
	}
	return 0;
}

程序运行:

【线程安全问题】线程互斥与线程同步技术_第1张图片

我们创建了两个线程分别对全局变量g_value进行+1的操作,分别进行10000000次,但是最后出现的结果并不是20000000,具体原因看【Windows线程开发】Windows线程同步技术。
这时候就需要我们使用原子锁,对全局变量g_value进行加锁了。

我们来看看原子锁:
原子锁主要解决的问题就是:当多个线程使用同一个变量时,对变量进行“加锁”技术,防止多个线程使用同一个变量。原子锁主要解决的就是对变量进行写的操作时,方式多个线程同时对同一个变量操作。

对于每一个不同类型的变量,都有自己的“加锁”函数,对于不同的运算操作,也有不同的“加锁”函数,具体可查看文档。
这里展示对变量++操作时使用原子锁:
这里为了方便大家查看,只给出了线程处理函数:

DWORD WINAPI ThreadProc(LPVOID lpParameter) {
	for (int i = 0; i < 10000000; i++) {
		InterlockedIncrement(&g_value);
	}
	return 0;
}

我们来看看运行效果:

【线程安全问题】线程互斥与线程同步技术_第2张图片

二. 临界区

临界区也被称作关键节,关键节对象提供与互斥对象提供的同步类似,但关键节只能由单个进程的线程使用。 关键节对象不能跨进程共享。
在使用临界区的时候,我们需要一下几个步骤:

    1. 创建全局临界区对象
    1. 初始化临界区对象
  • 之后,我们在使用临界区的时候,可以使用函数进入临界区,离开临界区。

1. 创建关键节对象

这里给出官方文档地址:Critical Section 对象

CRITICAL_SECTION cs;

2.初始化关键节对象

这里给出官方文档地址:initializeCriticalSection 函数 (synchapi.h)
语法:

void InitializeCriticalSection(
  [out] LPCRITICAL_SECTION lpCriticalSection
);

参数说明:
LPCRITICAL_SECTION:指向关键节对象的指针。

3. 进入关键节

这里给出官方文档地址:enterCriticalSection 函数 (synchapi.h)

函数功能:等待指定关键部分对象的所有权。 此函数将在授予调用线程所有权时返回。

语法:

void EnterCriticalSection(
  [in, out] LPCRITICAL_SECTION lpCriticalSection
);

参数说明:
LPCRITICAL_SECTION指向关键节对象的指针。

4. 离开关键节

这里给出官方文档地址:LeaveCriticalSection 函数 (synchapi.h)

函数功能:释放指定关键节对象的所有权。

语法:

void LeaveCriticalSection(
  [in, out] LPCRITICAL_SECTION lpCriticalSection
);

参数说明:
LPCRITICAL_SECTION:指向关键节对象的指针

5. 释放关键节资源

这里给出官方文档地址:deleteCriticalSection 函数 (synchapi.h)

函数功能:释放未拥有的关键节对象使用的所有资源。

语法:

void DeleteCriticalSection(
  [in, out] LPCRITICAL_SECTION lpCriticalSection
);

参数说明:
LPCRITICAL_SECTION:指向关键节对象的指针。 该对象以前必须使用 InitializeCriticalSection 函数进行初始化。

我们来使用临界区解决最开始提出的线程安全问题:

#include 
#include 

DWORD WINAPI ThreadProc(LPVOID);

CRITICAL_SECTION cs;
DWORD g_value = 0;

int main()
{
	InitializeCriticalSection(&cs);
	HANDLE Thread[2] = { 0 };
	Thread[0]=CreateThread(NULL, 0, ThreadProc, 0, NULL, NULL);
	Thread[1]=CreateThread(NULL, 0, ThreadProc, 0, NULL, NULL);
	WaitForMultipleObjects(2, Thread, TRUE, INFINITE);
	printf("%d", g_value);
	DeleteCriticalSection(&cs);
	return 0;
}
DWORD WINAPI ThreadProc(LPVOID lpParameter) {
	
	for (int i = 0; i < 10000000; i++) {
		EnterCriticalSection(&cs);
		g_value++;
		LeaveCriticalSection(&cs);
	}
	return 0;
}

我们来看看运行效果:
【线程安全问题】线程互斥与线程同步技术_第3张图片
可以看到我们成功使用临界区实现了线程互斥。

三.互斥体

大家可以到官方文档中查看互斥对象:互斥体对象

互斥对象状态设置为当任何线程不拥有时发出信号,一次只能有一个线程可以拥有互斥体对象。与临界区不同的是,互斥体可以跨进程使用。

互斥体的使用相对来说较为简单,我们只需要创建互斥体对象即可使用。

1. 创建互斥体

使用CreateMutex()函数,这里给出官方文档地址:createMutexA 函数 (synchapi.h)
语法:

HANDLE CreateMutexA(
  [in, optional] LPSECURITY_ATTRIBUTES lpMutexAttributes,
  [in]           BOOL                  bInitialOwner,
  [in, optional] LPCSTR                lpName
);

参数说明:
LPSECURITY_ATTRIBUTES:安全属性,我们一般不关注。
bInitialOwner:如果此值设置为TRUE,并且调用方创建了互斥体,则调用线程获取互斥体对象的使用权。也就是说,我们在某个线程中创建了互斥体,并且此字段设置为TRUE,那么这个线程立即拥有该互斥体。
lpName:为互斥体对象命名。
返回值:互斥体句柄。

2. 多线程使用互斥体

临界区对象创建并且初始化之后,可以使用进入临界区或者离开临界区的方式来实现线程互斥,那么我们如何使用互斥体实现线程间的互斥呢?

  • 我们可以在创建互斥体的时候,将bInitialOwner字段设置为FALSE,然后在使用互斥体的时候,采用等候消息的方式来获取互斥体使用权:
    这里给出官方文档地址:WaitForSingleObject 函数 (synchapi.h)
    使用WaitForSingleObject函数。
    语法:
DWORD WaitForSingleObjectEx(
  [in] HANDLE hHandle,
  [in] DWORD  dwMilliseconds,
);

参数说明:
hHandle:要等待的对象的句柄。
dwMilliseconds:超时间隔(以毫秒为单位)。

  • 在一个线程使用互斥体结束后,想要释放互斥体让其他线程使用,我们可以使用ReleaseMutex()函数来释放已获得的互斥体。
    这里给出官方文档地址:releaseMutex 函数 (synchapi.h)

函数功能:释放指定互斥对象的所有权。

语法:

BOOL ReleaseMutex(
  [in] HANDLE hMutex
);

参数说明:hMutex:要释放的互斥体对象句柄。

  • 在互斥体使用结束后,我们可以使用CloseHandle函数来关闭互斥体。
    这里给出官方文档地址:closeHandle 函数 (handleapi.h)

函数功能:关闭打开的对象句柄。

语法:

BOOL CloseHandle(
  [in] HANDLE hObject
);

函数说明:
hOnject:对象的有效句柄。
返回值:如果函数成功,则返回非零值。

我们来看一个新的线程安全问题:

#include 
#include 

DWORD WINAPI ThreadProc1(LPVOID);
DWORD WINAPI ThreadProc2(LPVOID);

DWORD g_value = 0;
HANDLE hMutex;

int main()
{
	hMutex = CreateMutex(NULL, FALSE, NULL);
	HANDLE Thread[2] = { 0 };
	Thread[0]=CreateThread(NULL, 0, ThreadProc1, 0, NULL, NULL);
	Thread[1]=CreateThread(NULL, 0, ThreadProc2, 0, NULL, NULL);
	WaitForMultipleObjects(2, Thread, TRUE, INFINITE);
	printf("%d", g_value);
	return 0;
}
DWORD WINAPI ThreadProc1(LPVOID lpParameter) {
	while (1) {
		for (int i = 0; i < 10; i++) {
			std::cout << "++ ";
			Sleep(100);
		}
		std::cout << std::endl;
	}
	return 0;
}
DWORD WINAPI ThreadProc2(LPVOID lpParameter) {
	while (1) {
		for (int i = 0; i < 10; i++) {
			std::cout << "-- ";
			Sleep(100);
		}
		std::cout << std::endl;
	}
	return 0;
}

我们的本意是输出十个“++”后,换行输出十个“-- ”,但是由于CPU时间片的问题,我们实际输出是这样的:
【线程安全问题】线程互斥与线程同步技术_第4张图片
我们使用互斥体来解决:

#include 
#include 

DWORD WINAPI ThreadProc1(LPVOID);
DWORD WINAPI ThreadProc2(LPVOID);

DWORD g_value = 0;
HANDLE hMutex= CreateMutex(NULL, FALSE, NULL);

int main()
{
	HANDLE Thread[2] = { 0 };
	Thread[0] = CreateThread(NULL, 0, ThreadProc1, 0, NULL, NULL);
	Thread[1] = CreateThread(NULL, 0, ThreadProc2, 0, NULL, NULL);
	WaitForMultipleObjects(2, Thread, TRUE, INFINITE);
	printf("%d", g_value);
	return 0;
}
DWORD WINAPI ThreadProc1(LPVOID lpParameter) {
	while (1) {
		WaitForSingleObject(hMutex,INFINITE);
		for (int i = 0; i < 10; i++) {
			std::cout << "++ ";
			Sleep(100);
		}
		std::cout << std::endl;
		ReleaseMutex(hMutex);
	}
	return 0;
}
DWORD WINAPI ThreadProc2(LPVOID lpParameter) {
	while (1) {
		WaitForSingleObject(hMutex,INFINITE);
		for (int i = 0; i < 10; i++) {
			std::cout << "-- ";
			Sleep(100);
		}
		std::cout << std::endl;
		ReleaseMutex(hMutex);
	}
	return 0;
}

我们来看看运行效果:
【线程安全问题】线程互斥与线程同步技术_第5张图片
可以发现我们使用互斥体解决了该线程安全问题。

四. 事件

前面介绍的都是线程互斥技术,在我们实际开发的过程中,有很多地方是有好几个线程相互依赖,这时候就需要线程同步技术了,这里首先我们来介绍事件:
这里给出官方文档地址:事件对象 (同步)
事件我个人理解为就是一个通知,当两个线程相互依赖的时候,其中一个线程完成了工作,就将事件设置为有信号(可以理解为发出了通知)另一个线程等待到事件消息后,开始工作。
事件的使用也较为简单,主要包括以下操作:

1. 创建事件

使用CreatEvent()函数,这里给出官方文档地址:createEventA 函数 (synchapi.h)

函数功能:创建或打开命名或未命名的事件对象

语法:

HANDLE CreateEventA(
  [in, optional] LPSECURITY_ATTRIBUTES lpEventAttributes,
  [in]           BOOL                  bManualReset,
  [in]           BOOL                  bInitialState,
  [in, optional] LPCSTR                lpName
);

参数说明:
lpEventAttributes:安全属性
bManualReset:如果设置为TRUE,则操作系统会自动重置事件,如果设置为FALSE,则需要程序员手动设置事件。
bInitialState:如果此参数为 TRUE,则会向事件对象发出初始状态信号;否则,它将不进行签名。
lpName:为事件命名
返回值:如果函数成功,则将返回事件对象句柄,如果函数失败,则返回NULL

2.多线程中使用事件

  • 当在多线程中使用事件时,我们需要设置事件信号:
    使用SetEvent()函数,这里给出官方文档地址:SetEvent 函数 (synchapi.h)。

函数功能:将指定的事件对象设置为有信号状态。

语法:

BOOL SetEvent(
  [in] HANDLE hEvent
);

参数说明:
hEnent:指定要设置的事件对象。
返回值:如果函数成功,则返回非零值,如果函数失败,则返回NULL。

当事件对象有信号后,在其他线程中可以打开事件对象:
使用OpenEvent()函数,这里给出官方文档地址:OpenEventA 函数 (synchapi.h)。

函数功能:打开现有的命名事件对象

语法:

HANDLE OpenEventA(
  [in] DWORD  dwDesiredAccess,
  [in] BOOL   bInheritHandle,
  [in] LPCSTR lpName
);

参数说明:
dwDesireAccess:访问事件对象。
bInheritHandle:如果此值为 TRUE,则此过程创建的进程将继承句柄。 否则,进程不会继承此句柄。
lpName:要打开的事件的名称。 名称比较区分大小写。
返回值:如果函数成功,则将返回事件对象的句柄,如果函数失败,则将返回NULL。

  • 当一个线程使用事件对象结束后,可以将事件复位(无消息状态)
    使用ResetEnent函数,这里给出官方文档地址:ResetEvent 函数 (synchapi.h)。

函数功能:将事件对象设置为非对齐状态(无消息状态)

语法:

BOOL ResetEvent(
  [in] HANDLE hEvent
);

参数说明:
hEnent:要设置的事件句柄。
返回值:如果函数成功,则返回非零值,如果函数失败,则返回NULL。

  • 当事件使用结束后,可以使用CloseHandel函数来关闭事件。

我们来看看事件的使用:
这里创建了两个线程,当一个线程将全局变量g_value增加到100后,通知另一个线程工作。

#include 
#include 

DWORD WINAPI ThreadProc1(LPVOID);
DWORD WINAPI ThreadProc2(LPVOID);

HANDLE hEvent = 0;
DWORD g_value = 0;

int main()
{
	hEvent = CreateEvent(NULL, FALSE, 0, NULL);
	HANDLE Thread[2] = { 0 };
	Thread[0] = CreateThread(NULL, 0, ThreadProc1, 0, NULL, NULL);
	Thread[1] = CreateThread(NULL, 0, ThreadProc2, 0, NULL, NULL);
	WaitForMultipleObjects(2, Thread, TRUE, INFINITE);
	printf("%d", g_value);
	CloseHandle(hEvent);
	return 0;
}
DWORD WINAPI ThreadProc1(LPVOID lpParameter) {
	for (int i = 0; i < 100; i++) {
		g_value++;
	}
	SetEvent(hEvent);
	printf("g_value已达到100,将进行输出\n");
	return 0;
}
DWORD WINAPI ThreadProc2(LPVOID lpParameter) {
	WaitForSingleObject(hEvent, INFINITE);
	while (g_value<120) {
		for (int i = 0; i < 10; i++) {
			std::cout << "-- ";
			Sleep(1);
		}
		g_value++;
		std::cout << std::endl;
	}
	ResetEvent(hEvent);
	return 0;
}

我们来看看这两个线程之间的协调工作:

【线程安全问题】线程互斥与线程同步技术_第6张图片

五. 信号量

我们之前的线程同步和线程互斥技术,都是创建了对应的对象后,被相关线程获取后即可使用,那么有没有一种技术,能够指定被获取多少次呢?这样我们就可以控制相关的线程执行多少次了。
答案是有的,我们称之为信号量,这里给出官方文档地址:信号灯对象

信号灯对象是一个同步对象,用于维护零和指定最大值之间的计数。 每次线程完成信号灯对象的等待时,计数都会递减,每次线程释放信号灯时递增。 当计数达到零时,不会再成功等待信号灯对象状态发出信号。 当信号量计数大于零时,会将信号量的状态设置为已发出信号;当信号量计数为零时,会将信号量的状态设置为未发出信号。

那么当我们使用信号量对象时,操作主要包括为:创建信号量,增加信号量的计数,等待信号量,离开信号量,关闭信号量。

1. 创建信号量

使用CreateSemaphore函数,这里给出官方文档地址:createSemaphoreA 函数 (winbase.h)

语法:

HANDLE CreateSemaphoreA(
  [in, optional] LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,
  [in]           LONG                  lInitialCount,
  [in]           LONG                  lMaximumCount,
  [in, optional] LPCSTR                lpName
);

参数说明:
lpSemaphoreAttributes:安全属性
lInitialCount:信号量的初始计数,此值必须小于lMaximumCount。
lMaximumCOunt:信号量的最大数量
lpName:为信号量命名
返回值:若函数成功,则返回信号量句柄,若函数失败,则返回NULL。

2. 等待信号量对象

使用WaitForSingluObject()函数,前文已经介绍过,这里不再赘述。

3.增加信号量数量计数

使用ReleaseSemaphore函数,这里给出官方文档地址:ReleaseSemaphore 函数 (synchapi.h)

函数功能:按指定量增加指定信号量的计数

语法:

BOOL ReleaseSemaphore(
  [in]            HANDLE hSemaphore,
  [in]            LONG   lReleaseCount,
  [out, optional] LPLONG lpPreviousCount
);

参数说明:
hSemaphore:指定信号量的句柄
lReleaseCount:指定要增加的数量
lpPreviousCount:这是一个OUT类型的参数,用于接收上一个计数,也就是增加之前的信号量计数
返回值:若函数成功,则返回非零值,若函数失败,则返回NULL。

4.关闭信号量

使用CloseHandle()函数,前文已经介绍过,这里不再做赘述。

我们来看看信号量的使用:

#include 
#include 

DWORD WINAPI ThreadProc1(LPVOID);
DWORD WINAPI ThreadProc2(LPVOID);

HANDLE hSemaphore = 0;
DWORD g_value = 0;

int main()
{
	hSemaphore = CreateSemaphore(NULL, 0, 3, NULL);
	HANDLE Thread[2] = { 0 };
	Thread[0] = CreateThread(NULL, 0, ThreadProc1, 0, NULL, NULL);
	Thread[1] = CreateThread(NULL, 0, ThreadProc2, 0, NULL, NULL);
	WaitForMultipleObjects(2, Thread, TRUE, INFINITE);
	printf("%d", g_value);
	CloseHandle(hSemaphore);

	return 0;
}
DWORD WINAPI ThreadProc1(LPVOID lpParameter) {
	for (int i = 0; i < 100; i++) {
		g_value++;
	}
	printf("g_value已达到100,将进行输出\n");
	ReleaseSemaphore(hSemaphore, 2, NULL);
	return 0;
}

DWORD WINAPI ThreadProc2(LPVOID lpParameter) {
	
	for (int k = 0; k < 3; k++) {
		WaitForSingleObject(hSemaphore, INFINITE);
		for (int i = 0; i < 10; i++) {
			std::cout << "-- ";
			Sleep(1);
		}
		std::cout << std::endl;
	}
	return 0;
}

我们来看看这段代码:我们本意是,当线程2执行的时候,输出三行,但是我们设置了信号量为2,所以只能输出两行。
我们看看执行效果:
【线程安全问题】线程互斥与线程同步技术_第7张图片
本篇文章就分享到这里,如果大家发现其中有错误或者是个人理解不到位的地方,还请大家指出来,我会非常虚心地学习,希望我们共同进步!!!

你可能感兴趣的:(#,滴水逆向三期win32编程,windows,系统安全,c++,安全)