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

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

语法:

BOOL CloseHandle(
  [in] HANDLE hObject
);

函数说明:

hOnject:对象的有效句柄。

返回值:如果函数成功,则返回非零值。

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

#include <iostream>
#include <windows.h>
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时间片的问题,我们实际输出是这样的:

我们使用互斥体来解决:

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

我们来看看运行效果:

可以发现我们使用互斥体解决了该线程安全问题。

四. 事件

前面介绍的都是线程互斥技术,在我们实际开发的过程中,有很多地方是有好几个线程相互依赖,这时候就需要线程同步技术了,这里首先我们来介绍事件:

这里给出官方文档地址:事件对象 (同步)

事件我个人理解为就是一个通知,当两个线程相互依赖的时候,其中一个线程完成了工作,就将事件设置为有信号(可以理解为发出了通知)另一个线程等待到事件消息后,开始工作。

事件的使用也较为简单,主要包括以下操作:

1. 创建事件

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

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

语法:

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

参数说明:

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

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

五. 信号量

我们之前的线程同步和线程互斥技术,都是创建了对应的对象后,被相关线程获取后即可使用,那么有没有一种技术,能够指定被获取多少次呢?这样我们就可以控制相关的线程执行多少次了。

答案是有的,我们称之为信号量,这里给出官方文档地址:信号灯对象

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

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

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 <iostream>
#include <windows.h>
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,所以只能输出两行。

我们看看执行效果:

本篇文章就分享到这里,如果大家发现其中有错误或者是个人理解不到位的地方,还请大家指出来,我会非常虚心地学习,希望我们共同进步!!!

相关文章
|
1月前
|
Java 数据库连接 数据库
不同业务使用同一个线程池发生死锁的技术探讨
【10月更文挑战第6天】在并发编程中,线程池是一种常用的优化手段,用于管理和复用线程资源,减少线程的创建和销毁开销。然而,当多个不同业务场景共用同一个线程池时,可能会引发一系列并发问题,其中死锁就是最为严重的一种。本文将深入探讨不同业务使用同一个线程池发生死锁的原因、影响及解决方案,旨在帮助开发者避免此类陷阱,提升系统的稳定性和可靠性。
46 5
|
1月前
|
安全 Java 开发者
在多线程编程中,确保数据一致性与防止竞态条件至关重要。Java提供了多种线程同步机制
【10月更文挑战第3天】在多线程编程中,确保数据一致性与防止竞态条件至关重要。Java提供了多种线程同步机制,如`synchronized`关键字、`Lock`接口及其实现类(如`ReentrantLock`),还有原子变量(如`AtomicInteger`)。这些工具可以帮助开发者避免数据不一致、死锁和活锁等问题。通过合理选择和使用这些机制,可以有效管理并发,确保程序稳定运行。例如,`synchronized`可确保同一时间只有一个线程访问共享资源;`Lock`提供更灵活的锁定方式;原子变量则利用硬件指令实现无锁操作。
20 2
|
1月前
|
网络协议 安全 Java
难懂,误点!将多线程技术应用于Python的异步事件循环
难懂,误点!将多线程技术应用于Python的异步事件循环
56 0
|
1月前
|
安全 Linux
Linux线程(十一)线程互斥锁-条件变量详解
Linux线程(十一)线程互斥锁-条件变量详解
|
2月前
|
监控 Java
线程池中线程异常后:销毁还是复用?技术深度剖析
在并发编程中,线程池作为一种高效利用系统资源的工具,被广泛用于处理大量并发任务。然而,当线程池中的线程在执行任务时遇到异常,如何妥善处理这些异常线程成为了一个值得深入探讨的话题。本文将围绕“线程池中线程异常后:销毁还是复用?”这一主题,分享一些实践经验和理论思考。
128 3
|
3月前
|
监控 安全 Java
Java多线程调试技巧:如何定位和解决线程安全问题
Java多线程调试技巧:如何定位和解决线程安全问题
127 2
|
2月前
|
安全 Java
LinkedBlockingQueue 是线程安全的,为什么会有两个线程都take()到同一个对象了?
LinkedBlockingQueue 是线程安全的,为什么会有两个线程都take()到同一个对象了?
44 0
|
3月前
|
Java C语言 C++
并发编程进阶:线程同步与互斥
并发编程进阶:线程同步与互斥
38 0
|
3月前
|
算法 Java 调度
【多线程面试题二十】、 如何实现互斥锁(mutex)?
这篇文章讨论了在Java中实现互斥锁(mutex)的两种方式:使用`synchronized`关键字进行块结构同步,以及使用`java.util.concurrent.locks.Lock`接口进行非块结构同步,后者提供了更灵活的同步机制和扩展性。
|
3月前
|
存储 安全 Java
【多线程面试题十七】、如果不使用synchronized和Lock,如何保证线程安全?
这篇文章探讨了在不使用`synchronized`和`Lock`的情况下保证线程安全的方法,包括使用`volatile`关键字、原子变量、线程本地存储(`ThreadLocal`)以及设计不可变对象。