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

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

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

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

线程互斥

一. 原子锁

原子锁我们在这里介绍到了,但是严格来说,它不属于线程互斥对象。

我们先来看看存在线程安全问题的一个控制台程序:

#include <iostream>
#include <windows.h>
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;
}

程序运行:

我们创建了两个线程分别对全局变量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;
}

我们来看看运行效果:

二. 临界区

临界区也被称作关键节,关键节对象提供与互斥对象提供的同步类似,但关键节只能由单个进程的线程使用。 关键节对象不能跨进程共享。

在使用临界区的时候,我们需要一下几个步骤:

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

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 <iostream>
#include <windows.h>
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;
}

我们来看看运行效果:

可以看到我们成功使用临界区实现了线程互斥。

三.互斥体

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

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

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

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:要释放的互斥体对象句柄。

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




相关文章
|
1天前
|
安全 Java
java线程之线程安全
java线程之线程安全
14 1
|
7天前
|
Java
【技术瑜伽师】Java 线程:修炼生命周期的平衡之道,达到多线程编程的最高境界!
【6月更文挑战第19天】Java多线程编程犹如瑜伽修行,从创建线程开始,如`new Thread(Runnable)`,到启动线程的活跃,用`start()`赋予生命。面对竞争与冲突,借助同步机制保证资源访问的有序,如`synchronized`关键字。线程可能阻塞等待,如同瑜伽的静止与耐心。完成任务后线程终止,整个过程需密切关注状态变换,以求多线程间的和谐与平衡。持续修炼,如同瑜伽般持之以恒,实现高效稳定的多线程程序。
|
9天前
|
API
linux---线程互斥锁总结及代码实现
linux---线程互斥锁总结及代码实现
|
7天前
|
Java 开发者
【技术成长日记】Java 线程的自我修养:从新手到大师的生命周期修炼手册!
【6月更文挑战第19天】Java线程之旅,从新手到大师的进阶之路:始于创建线程的懵懂,理解就绪与运行状态的成长,克服同步难题的进阶,至洞悉生命周期的精通。通过实例,展示线程的创建、运行与同步,展现技能的不断提升与升华。
|
7天前
|
Java
【技术解码】Java线程的五味人生:新建、就绪、运行、阻塞与死亡的哲学解读!
【6月更文挑战第19天】Java线程生命周期如同人生旅程,经历新建、就绪、运行、阻塞至死亡五阶段。从`new Thread()`的诞生到`start()`的蓄势待发,再到`run()`的全力以赴,线程在代码中奔跑。阻塞时面临挑战,等待资源释放,最终通过`join()`或中断结束生命。线程的每个状态转变,都是编程世界与哲思的交汇点。
|
13天前
|
存储 安全 Java
Java多线程中线程安全问题
Java多线程中的线程安全问题主要涉及多线程环境下对共享资源的访问可能导致的数据损坏或不一致。线程安全的核心在于确保在多线程调度顺序不确定的情况下,代码的执行结果始终正确。常见原因包括线程调度随机性、共享数据修改以及原子性问题。解决线程安全问题通常需要采用同步机制,如使用synchronized关键字或Lock接口,以确保同一时间只有一个线程能够访问特定资源,从而保持数据的一致性和正确性。
|
1天前
|
存储 安全 Java
Java中的线程安全与同步技术
Java中的线程安全与同步技术
|
22天前
|
安全 Java 容器
多线程(进阶四:线程安全的集合类)
多线程(进阶四:线程安全的集合类)
17 0
|
26天前
|
安全 算法 Java
Java中的并发编程技术:解锁高效多线程应用的秘密
Java作为一种广泛应用的编程语言,其并发编程技术一直备受关注。本文将深入探讨Java中的并发编程,从基本概念到高级技巧,帮助读者更好地理解并发编程的本质,并学会如何在多线程环境中构建高效可靠的应用程序。
|
5天前
|
存储 Linux C语言
c++进阶篇——初窥多线程(二) 基于C语言实现的多线程编写
本文介绍了C++中使用C语言的pthread库实现多线程编程。`pthread_create`用于创建新线程,`pthread_self`返回当前线程ID。示例展示了如何创建线程并打印线程ID,强调了线程同步的重要性,如使用`sleep`防止主线程提前结束导致子线程未执行完。`pthread_exit`用于线程退出,`pthread_join`用来等待并回收子线程,`pthread_detach`则分离线程。文中还提到了线程取消功能,通过`pthread_cancel`实现。这些基本操作是理解和使用C/C++多线程的关键。