【学习笔记】Windows 下线程同步之互斥锁

前言

  1. 本文所涉及的同步主要描述在 Windows 环境下的机制,和 Linux 中的同步机制有一定的联系,但注意并不完全相同。类似于,Windows 和 Linux 按照自己的方式实现了操作系统中同步机制的概念
  2. 本文记录的是 Windows 下的互斥锁同步机制,但在 Windows 的同步机制中,其中很多的概念和逻辑同样适用于事件(Event),信号量,计时器等其他同步机制

环境

  1. OS:win
  2. IDE:Visual Studio 2015

简介

  • 简介:互斥锁是一种同步对象,当没有任何线程拥有互斥锁时,互斥锁处于有信号(signaled)状态,当互斥锁被某个线程拥有,则它处于无信号状态(nonsignaled)。

    顾名思义,互斥锁就是一种为了达到访问共享资源而互斥目的的锁。比如生活中,公共厕所就是一种共享资源,公厕一次只能有一个人使用,使用者在使用的时候就会关门上锁,使用完之后需要开门释放锁。对于每个使用者来说,这个锁一次只能被一个人占有

  • 特点

    • 任何一个互斥锁,一次只能被一个线程拥有
    • 可以跨进程使用,即进程间同步
  • 适用场景:同步一些共享资源,比如共享内存(shared memory)

相关函数

CreateMutex

  1. 作用:创建或打开命名或未命名的互斥锁对象。如果某互斥锁已经被创建,当再次使用 CreateMutex 操做该互斥锁,实际的操作等效于 OpenMutex,但通过 GetLastError 会返回 ERROR_ALREADY_EXISTS 标识
  2. 语法
HANDLE CreateMutexA(
  [in, optional] LPSECURITY_ATTRIBUTES lpMutexAttributes,
  [in]           BOOL                  bInitialOwner,
  [in, optional] LPCSTR                lpName
);
  1. 参数
    1. lpMutexAttributes,为 NULL 时,句柄不能被子进程继承
    2. bInitialOwner,为 true 时,创建该互斥锁的线程获取该互斥锁
    3. lpName,互斥锁的名字,为 NULL 时,为未命名互斥锁,关于未命名互斥锁如何传递见下方“未命名互斥锁的同步”

Wait 函数

  1. Wait 函数是一系列提供类似功能的等待函数(如 WaitForMultipleObjects),该函数的作用是请求某个互斥锁的使用权,若没有获取到,则阻塞
  2. 等待函数的返回值表明等待函数因为某些原因返回,而不是正常的互斥锁信号转换
  3. 多个线程等待互斥锁时,只有一个线程会被随机选择获取互斥锁

ReleaseMutex

  1. 作用:释放控制权,释放后,互斥锁变为有信号状态
  2. 语法
BOOL ReleaseMutex(
  [in] HANDLE hMutex
);
  1. 参数:hMutex 是要释放的互斥锁句柄

CloseHandle

  1. 作用:关闭句柄,本文中即关闭互斥锁
    【注】除了使用 CloseHandle 手动关闭句柄外,当某个进程终止后,也会自动关闭句柄
    【注】CloseHandle 关闭当前线程中使用的句柄,但如果还有其他线程拥有该句柄,那么句柄对象并未真正关闭,只有当最后一个该对象被关闭,句柄才会真正关闭
    在这里插入图片描述

如图,只有当所有句柄均被关闭,互斥锁对象才会自动关闭

其他

互斥锁的名字

特别注意的是,互斥锁的名字和其他同步对象(如,事件,信号量)的名字位于相同的命名空间,因此如果互斥锁有名字为“ExampleName”,事件也有名字为“ExampleName”则发生冲突,通过 GetLastError 函数返回 ERROR_INVALID_HANDLE 错误。

更多关于内核对象命名空间的知识可以阅读参考中的链接。

未命名互斥锁的同步

  1. 命名的互斥锁我们可以很容易理解如何让两个或多个线程使用相同的互斥锁。而未命名的互斥锁要如何让系统中多个线程与同一个互斥锁产生联系呢,答案是通过线程间(或进程间)复制句柄或者父子句柄继承实现,这里我们主要讲一下复制句柄

  2. 复制句柄:通过该方法可以在两个进程之间传递句柄,但相比于命名句柄和父子继承的方式,这种方式是最麻烦的,它需要在创建句柄的进程和使用句柄的进程间进行通信(如,命名管道,命名共享内存),当然这一步 Windows 通过高层函数隐藏了底层实现的细节,也就是说,你只需要调用一个 DuplicateHandle 函数即可完成两个进程的通信。

    另一个需要注意的地方,复制的句柄本质上和源句柄是等同的,你可以理解为指针间的赋值,赋值过后的两个指针实际是指向的相同的区域,任何改变都会影响两个指针指向的区域,句柄也是如此。

  3. 语法

BOOL DuplicateHandle(
  [in]  HANDLE   hSourceProcessHandle,
  [in]  HANDLE   hSourceHandle,
  [in]  HANDLE   hTargetProcessHandle,
  [out] LPHANDLE lpTargetHandle,
  [in]  DWORD    dwDesiredAccess,
  [in]  BOOL     bInheritHandle,
  [in]  DWORD    dwOptions
);
  1. 参数
    1. hSourceProcessHandle:源进程的句柄,该进程必须有 PROCESS_DUP_HANDLE 的接入权限。源句柄可以通过 GetCurrentProcess 获得
    2. hSourceHandle:要被复制的句柄,比如互斥锁句柄
    3. hTargetProcessHandle:目标进程的句柄,该进程必须有 PROCESS_DUP_HANDLE 的接入权限。目标句柄可以通过 OpenProcess 获得
    4. lpTargetHandle:注意它是一个指针,指向复制过来的句柄,“LP” 是 long pointer 的缩写
    5. dwDesiredAccess:新句柄的权限设置。一般通过复制得到的句柄的权限范围 ≤ 原句柄
    6. bInheritHandle:该句柄能否被继承
    7. dwOptions:一些可选的配置项,这里不展开

关于句柄复制并非本篇的重点,详细内容移步官方文档

互斥锁的意外终止

比如,当前拥有互斥锁的线程终止,而该线程并未释放互斥锁,此时互斥锁被标记为遗弃(abandoned),它表明互斥锁未被正确释放

其他等待该互斥锁的线程可以获取它,但对应的 wait 函数会返回 WAIT_ABANDONED 来表明互斥锁对象被遗弃,由此表明此时被互斥锁操控的共享资源处于未定义状态(undefined state)

临界区对象

临界区(critical section)对象提供类似与互斥锁的功能,区别在于临界区对象不提供进程间同步,只能提供同一进程中的线程的同步
【注】这里的临界区对象是 Windows 提供的一种用户模式下的线程同步机制,不完全等同于操作系统中的临界区这个概念

示例

在本示例中,我们启动两个进程,分别为 A process 和 B process。

A process 产生一个互斥锁,名为 MutexDemo,并且在产生时就获得该锁的使用权,之后就是执行使用共享资源的代码,然后释放互斥锁,最终关闭互斥锁句柄。

B process 打开名为 MutexDemo,获得 MutexDemo 的使用权后,执行使用共享资源的代码,然后释放互斥锁,最终关闭互斥锁句柄。

A process cpp

void MutexSynchronize()
{
	cout << "A thread: enter mutex creator" << endl;

	// create a mutex and initially get it
	HANDLE hMutex = CreateMutex(NULL, true, _TEXT("MutexDemo"));

	// Simulate execute business logic
	cout << "A thread: set data into shared memory" << endl;

	// release mutex, mutex becomes signaled
	ReleaseMutex(hMutex);

	// close handle
	Sleep(5000);		// wait B thread get mutex, because we cannot predict the execution order 
				     	// of 	multiple processes
	CloseHandle(hMutex);
}

B process cpp

void MutexSynchronize()
{
	cout << "B thread: enter mutex opener" << endl;
	HANDLE hMutex = NULL;

	// wait A thread create mutex and open it
	while (hMutex == NULL)
	{
		hMutex = OpenMutex(MUTEX_ALL_ACCESS, false, _TEXT("MutexDemo"));
		cout << "B thread: wait mutex" << endl;
	}

	// wait signaled mutex
	WaitForSingleObject(hMutex, INFINITE);

	// Simulate execute business logic
	cout << "B thread: get data from shared memory" << endl;

	// release mutex, mutex becomes signaled
	ReleaseMutex(hMutex);

	// close handle
	CloseHandle(hMutex);
}

参考

  1. Synchronization
  2. 《Windows 核心编程》
  3. Process Security and Access Rights
  4. Thread Handles and Identifiers:what is pseudo handle