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

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

达内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
线程池中线程异常后:销毁还是复用?技术深度剖析
在并发编程中,线程池作为一种高效利用系统资源的工具,被广泛用于处理大量并发任务。然而,当线程池中的线程在执行任务时遇到异常,如何妥善处理这些异常线程成为了一个值得深入探讨的话题。本文将围绕“线程池中线程异常后:销毁还是复用?”这一主题,分享一些实践经验和理论思考。
53 3
|
2月前
|
监控 安全 Java
Java多线程调试技巧:如何定位和解决线程安全问题
Java多线程调试技巧:如何定位和解决线程安全问题
82 2
|
25天前
|
安全 Java
LinkedBlockingQueue 是线程安全的,为什么会有两个线程都take()到同一个对象了?
LinkedBlockingQueue 是线程安全的,为什么会有两个线程都take()到同一个对象了?
31 0
|
2月前
|
安全 算法 Java
【Java集合类面试二】、 Java中的容器,线程安全和线程不安全的分别有哪些?
这篇文章讨论了Java集合类的线程安全性,列举了线程不安全的集合类(如HashSet、ArrayList、HashMap)和线程安全的集合类(如Vector、Hashtable),同时介绍了Java 5之后提供的java.util.concurrent包中的高效并发集合类,如ConcurrentHashMap和CopyOnWriteArrayList。
【Java集合类面试二】、 Java中的容器,线程安全和线程不安全的分别有哪些?
|
2月前
|
Java C语言 C++
并发编程进阶:线程同步与互斥
并发编程进阶:线程同步与互斥
30 0
|
2月前
|
算法 Java 调度
【多线程面试题二十】、 如何实现互斥锁(mutex)?
这篇文章讨论了在Java中实现互斥锁(mutex)的两种方式:使用`synchronized`关键字进行块结构同步,以及使用`java.util.concurrent.locks.Lock`接口进行非块结构同步,后者提供了更灵活的同步机制和扩展性。
|
2月前
|
存储 安全 Java
【多线程面试题十七】、如果不使用synchronized和Lock,如何保证线程安全?
这篇文章探讨了在不使用`synchronized`和`Lock`的情况下保证线程安全的方法,包括使用`volatile`关键字、原子变量、线程本地存储(`ThreadLocal`)以及设计不可变对象。
|
2月前
|
安全 Java
【多线程面试题 六】、 如何实现线程同步?
实现线程同步的方法包括同步方法、同步代码块、使用ReentrantLock、volatile关键字以及原子变量类,以确保线程安全和数据一致性。
|
2月前
|
存储 监控 Java
Java多线程优化:提高线程池性能的技巧与实践
Java多线程优化:提高线程池性能的技巧与实践
64 1
|
8天前
|
数据采集 负载均衡 安全
LeetCode刷题 多线程编程九则 | 1188. 设计有限阻塞队列 1242. 多线程网页爬虫 1279. 红绿灯路口
本文提供了多个多线程编程问题的解决方案,包括设计有限阻塞队列、多线程网页爬虫、红绿灯路口等,每个问题都给出了至少一种实现方法,涵盖了互斥锁、条件变量、信号量等线程同步机制的使用。
LeetCode刷题 多线程编程九则 | 1188. 设计有限阻塞队列 1242. 多线程网页爬虫 1279. 红绿灯路口