秒杀多线程系列(摘录)

本文摘自 《专栏秒杀多线程面试题系列
版权归:【MoreWindows】所有

CreateThread与_beginthreadex本质区别

尽量使用_beginthreadex()来代替使用CreateThread()

像strerror()、strtok()、tmpnam()、gmtime()、asctime()等函数也会遇到这种由多个线程访问修改导致的数据覆盖问题。

为了解决由多个线程访问修改导致的数据覆盖这个问题,Windows操作系统提供了这样的一种解决方案
每个线程都将拥有自己专用的一块内存区域来供标准C运行库中所有有需要的函数使用。而且这块内存区域的创建就是由C/C++运行库函数_beginthreadex()来负责的

//最简单的创建多线程实例
#include <stdio.h>
#include <windows.h>
//子线程函数
DWORD WINAPI ThreadFun(LPVOID pM)
{
	printf("子线程的线程ID号为:%d\n子线程输出Hello World\n", GetCurrentThreadId());
	return 0;
}
//主函数,所谓主函数其实就是主线程执行的函数。
int main()
{
	printf("     最简单的创建多线程实例\n");
 
	HANDLE handle = CreateThread(NULL, 0, ThreadFun, NULL, 0, NULL);
	WaitForSingleObject(handle, INFINITE);
	return 0;
}

在这里插入图片描述

//子线程报数
#include <stdio.h>
#include <process.h>
#include <windows.h>
int g_nCount;
//子线程函数
unsigned int __stdcall ThreadFun(PVOID pM)
{
	g_nCount++;
	printf("线程ID号为%4d的子线程报数%d\n", GetCurrentThreadId(), g_nCount);
	return 0;
}
//主函数,所谓主函数其实就是主线程执行的函数。
int main()
{
	printf("     子线程报数 \n");
	
	const int THREAD_NUM = 10;
	HANDLE handle[THREAD_NUM];
 
	g_nCount = 0;
	for (int i = 0; i < THREAD_NUM; i++)
		handle[i] = (HANDLE)_beginthreadex(NULL, 0, ThreadFun, NULL, 0, NULL);
	WaitForMultipleObjects(THREAD_NUM, handle, TRUE, INFINITE);
	return 0;
}

在这里插入图片描述

答案是不对的,虽然这种做法在逻辑上是正确的,但在多线程环境下这样做是会产生严重的问题

原子操作 Interlocked系列函数

常用的Interlocked系列函数

  1. 增减操作
 LONG __cdec lInterlockedIncrement(LONG volatile* Addend);
 LONG __cdecl InterlockedDecrement(LONG volatile* Addend);
 返回变量执行增减操作之后的值。

 LONG__cdec InterlockedExchangeAdd(LONG volatile* Addend, LONGValue);
返回运算后的值,注意!加个负数就是减。 
  1. 赋值操作
LONG __cdecl InterlockedExchange(LONG volatile* Target, LONGValue);
Value就是新值,函数会返回原先的值。

在本例中只要使用InterlockedIncrement()函数就可以了。
将线程函数代码改成:

DWORD WINAPI ThreadFun(void *pPM)
{
	Sleep(100);//some work should to do
	//g_nLoginCount++;
	InterlockedIncrement((LPLONG)&g_nLoginCount);
	Sleep(50);
	return 0;
}

在这里插入图片描述

一个经典的多线程同步问题

程序描述:
主线程启动10个子线程并将表示子线程序号的变量地址作为参数传递给子线程。子线程接收参数 -> sleep(50) -> 全局变量++ -> sleep(0) -> 输出参数和全局变量。
要求:
1.子线程输出的线程序号不能重复。
2.全局变量的输出必须递增。
在这里插入图片描述

分析下这个问题的考察点,主要考察点有二个:

1.主线程创建子线程并传入一个指向变量地址的指针作参数,由于线程启动须要花费一定的时间,所以在子线程根据这个指针访问并保存数据前,主线程应等待子线程保存完毕后才能改动该参数并启动下一个线程。这涉及到主线程与子线程之间的同步

2.子线程之间会互斥的改动和输出全局变量。要求全局变量的输出必须递增。这涉及到各子线程间的互斥

经典线程同步 关键段CS

关键段CRITICAL_SECTIONの四个函数

  • 初始化函数

    • void InitializeCriticalSection(LPCRITICAL_SECTIONlpCriticalSection);
    • 定义关键段变量后必须先初始化
  • 销毁函数

    • void DeleteCriticalSection(LPCRITICAL_SECTIONlpCriticalSection);
  • 进入关键区域

    • void EnterCriticalSection(LPCRITICAL_SECTIONlpCriticalSection);
    • 系统保证各线程互斥的进入关键区域
  • 离开关关键区域

    • void LeaveCriticalSection(LPCRITICAL_SECTIONlpCriticalSection);
在经典多线程问题中设置二个关键区域
  1. 主线程在递增子线程序号时
  1. 各子线程互斥的访问输出全局资源时

关键段会记录拥有该关键段的线程句柄即关键段是有“线程所有权”概念的。

#include <stdio.h>
#include <process.h>
#include <windows.h>
long g_nNum;
unsigned int __stdcall Fun(void *pPM);
const int THREAD_NUM = 10;
//关键段变量声明
CRITICAL_SECTION  g_csThreadParameter, g_csThreadCode;
int main()
{
	printf("     经典线程同步 关键段\n");
 
	//关键段初始化
	InitializeCriticalSection(&g_csThreadParameter);
	InitializeCriticalSection(&g_csThreadCode);
	
	HANDLE  handle[THREAD_NUM];	
	g_nNum = 0;	
	int i = 0;
	while (i < THREAD_NUM) 
	{
		EnterCriticalSection(&g_csThreadParameter);//进入子线程序号关键区域
		handle[i] = (HANDLE)_beginthreadex(NULL, 0, Fun, &i, 0, NULL);
		++i;
	}
	WaitForMultipleObjects(THREAD_NUM, handle, TRUE, INFINITE);
 
	DeleteCriticalSection(&g_csThreadCode);
	DeleteCriticalSection(&g_csThreadParameter);
	return 0;
}
unsigned int __stdcall Fun(void *pPM)
{
	int nThreadNum = *(int *)pPM; 
	LeaveCriticalSection(&g_csThreadParameter);//离开子线程序号关键区域
 
	Sleep(50);//some work should to do
 
	EnterCriticalSection(&g_csThreadCode);//进入各子线程互斥区域
	g_nNum++;
	Sleep(0);//some work should to do
	printf("线程编号为%d  全局资源值为%d\n", nThreadNum, g_nNum);
	LeaveCriticalSection(&g_csThreadCode);//离开各子线程互斥区域
	return 0;
}

在这里插入图片描述
可以看出来,各子线程已经可以互斥的访问与输出全局资源了,但主线程与子线程之间的同步还是有点问题。

原因:主线程能多次进入这个关键区域!

关键段可以用于线程间的互斥,但不可以用于同步

配合了旋转锁的关键段初始化函数

  • 初始化关键段并设置旋转次数

    • BOOL InitializeCriticalSectionAndSpinCount( LPCRITICAL_SECTIONlpCriticalSection, DWORDdwSpinCount);
    • 旋转次数一般设置为4000
  • 修改关键段的旋转次数

    • DWORD SetCriticalSectionSpinCount( LPCRITICAL_SECTIONlpCriticalSection, DWORDdwSpinCount);

推荐在使用关键段的时候同时使用旋转锁,这样有助于提高性能。

注意:如果主机只有一个处理器,那么设置旋转锁是无效的。无法进入关键区域的线程总会被系统将其切换到等待状态。

总结下关键段:

  1. 关键段共初始化化、销毁、进入和离开关键区域四个函数。

  2. 关键段可以解决线程的互斥问题,但因为具有“线程所有权”,所以无法解决同步问题。

  3. 推荐关键段与旋转锁配合使用。

经典线程同步 事件Event

常用的函数

  • CreateEvent

    • 创建事件
    • HANDLECreateEvent( LPSECURITY_ATTRIBUTESlpEventAttributes, BOOLbManualReset, BOOLbInitialState, LPCTSTRlpName );
    • 函数说明:
      • 第一个参数表示安全控制,一般直接传入NULL。
      • 第二个参数确定事件是手动置位还是自动置位,传入TRUE表示手动置位,传入FALSE表示自动置位。如果为自动置位,则对该事件调用WaitForSingleObject()后会自动调用ResetEvent()使事件变成未触发状态。打个小小比方,手动置位事件相当于教室门,教室门一旦打开(被触发),所以有人都可以进入直到老师去关上教室门(事件变成未触发)。自动置位事件就相当于医院里拍X光的房间门,门打开后只能进入一个人,这个人进去后会将门关上,其它人不能进入除非门重新被打开(事件重新被触发)。
      • 第三个参数表示事件的初始状态,传入TRUE表示已触发。
      • 第四个参数表示事件的名称,传入NULL表示匿名事件。
  • OpenEvent

    • 根据名称获得一个事件句柄

    • HANDLE OpenEvent( DWORDdwDesiredAccess, BOOLbInheritHandle, LPCTSTRlpName //名称);

    • 函数说明:

      • 第一个参数表示访问权限,对事件一般传入EVENT_ALL_ACCESS
      • 第二个参数表示事件句柄继承性,一般传入TRUE即可。
      • 第三个参数表示名称,不同进程中的各线程可以通过名称来确保它们访问同一个事件。
  • SetEvent

    • 触发事件

    • BOOL SetEvent(HANDLEhEvent);

    • 函数说明:

      • 每次触发后,必有一个或多个处于等待状态下的线程变成可调度状态。
  • ResetEvent

    • 将事件设为末触发
    • BOOL ResetEvent(HANDLEhEvent);

在经典多线程问题中设置1个event和1个关键段

event处理主线程与子线程的同步,用关键段来处理各子线程间的互斥

#include <stdio.h>
#include <process.h>
#include <windows.h>
long g_nNum;
unsigned int __stdcall Fun(void *pPM);
const int THREAD_NUM = 10;
//事件与关键段
HANDLE  g_hThreadEvent;
CRITICAL_SECTION g_csThreadCode;
int main()
{
	printf("     经典线程同步 事件Event\n");
	
	//初始化事件和关键段 自动置位,初始无触发的匿名事件
	g_hThreadEvent = CreateEvent(NULL, FALSE, FALSE, NULL); 
	InitializeCriticalSection(&g_csThreadCode);
 
	HANDLE  handle[THREAD_NUM];	
	g_nNum = 0;
	int i = 0;
	while (i < THREAD_NUM) 
	{
		handle[i] = (HANDLE)_beginthreadex(NULL, 0, Fun, &i, 0, NULL);
		WaitForSingleObject(g_hThreadEvent, INFINITE); //等待事件被触发
		i++;
	}
	WaitForMultipleObjects(THREAD_NUM, handle, TRUE, INFINITE);
 
	//销毁事件和关键段
	CloseHandle(g_hThreadEvent);
	DeleteCriticalSection(&g_csThreadCode);
	return 0;
}
unsigned int __stdcall Fun(void *pPM)
{
	int nThreadNum = *(int *)pPM; 
	SetEvent(g_hThreadEvent); //触发事件
	
	Sleep(50);//some work should to do
	
	EnterCriticalSection(&g_csThreadCode);
	g_nNum++;
	Sleep(0);//some work should to do
	printf("线程编号为%d  全局资源值为%d\n", nThreadNum, g_nNum); 
	LeaveCriticalSection(&g_csThreadCode);
	return 0;
}

在这里插入图片描述

深挖event

  • PulseEvent
    • 将事件触发后立即将事件设置为未触发,相当于触发一个事件脉冲

    • BOOL PulseEvent(HANDLEhEvent);

      • 这是一个不常用的事件函数,此函数相当于SetEvent()后立即调用ResetEvent();
      • 此时情况可以分为两种:
      1. 对于手动置位事件,所有正处于等待状态下线程都变成可调度状态。对于自动置位事件,所有正处于等待状态下线程只有一个变成可调度状态。此后事件是末触发的。该函数不稳定,因为无法预知在调用PulseEvent ()时哪些线程正处于等待状态。
      2. 对于自动置位事件,所有正处于等待状态下线程只有一个变成可调度状态。此后事件是末触发的。该函数不稳定,因为无法预知在调用PulseEvent ()时哪些线程正处于等待状态。

此后事件是末触发的。该函数不稳定,因为无法预知在调用PulseEvent ()时哪些线程正处于等待状态。

示例
主线程启动7个子线程,其中有5个线程 Sleep(10)后对一事件调用等待函数(称为快线程),另有2个线程 Sleep(100)后也对该事件调用等待函数(称为慢线程)。主线程启动所有子线程后再 Sleep(50)保证有5个 快线程都正处于等待状态中。此时若主线程触发一个事件脉冲,那么对于手动置位 event,这5个线程都将顺利执行下去。对于 自动置位event,这5个线程中会有中一个顺利执行下去。而不论手动置位事件还是 自动置位event,那2个 慢线程由于 Sleep(100)所以会错过事件脉冲,因此 慢线程都会进入等待状态而无法顺利执行下去。
//使用PluseEvent()函数
#include <stdio.h>
#include <conio.h>
#include <process.h>
#include <windows.h>
HANDLE  g_hThreadEvent;
//快线程
unsigned int __stdcall FastThreadFun(void *pPM)
{
	Sleep(10); //用这个来保证各线程调用等待函数的次序有一定的随机性
	printf("%s 启动\n", (PSTR)pPM);
	WaitForSingleObject(g_hThreadEvent, INFINITE);
	printf("%s 等到事件被触发 顺利结束\n", (PSTR)pPM);
	return 0;
}
//慢线程
unsigned int __stdcall SlowThreadFun(void *pPM)
{
	Sleep(100);
	printf("%s 启动\n", (PSTR)pPM);
	WaitForSingleObject(g_hThreadEvent, INFINITE);
	printf("%s 等到事件被触发 顺利结束\n", (PSTR)pPM);
	return 0;
}
int main()
{
	printf("  使用PluseEvent()函数\n");
 
	BOOL bManualReset = FALSE;
	//创建事件 第二个参数手动置位TRUE,自动置位FALSE
	g_hThreadEvent = CreateEvent(NULL, bManualReset, FALSE, NULL);
	if (bManualReset == TRUE)
		printf("当前使用手动置位事件\n");
	else
		printf("当前使用自动置位事件\n");
 
	char szFastThreadName[5][30] = {"快线程1000", "快线程1001", "快线程1002", "快线程1003", "快线程1004"};
	char szSlowThreadName[2][30] = {"慢线程196", "慢线程197"};
 
	int i;
	for (i = 0; i < 5; i++)
		_beginthreadex(NULL, 0, FastThreadFun, szFastThreadName[i], 0, NULL);
	for (i = 0; i < 2; i++)
		_beginthreadex(NULL, 0, SlowThreadFun, szSlowThreadName[i], 0, NULL);
	
	Sleep(50); //保证快线程已经全部启动
	printf("现在主线程触发一个事件脉冲 - PulseEvent()\n");
	PulseEvent(g_hThreadEvent);//调用PulseEvent()就相当于同时调用下面二句
	//SetEvent(g_hThreadEvent);
	//ResetEvent(g_hThreadEvent);
	
	Sleep(3000); 
	printf("时间到,主线程结束运行\n");
	CloseHandle(g_hThreadEvent);
	return 0;
}

自动置位事件,运行结果如下:
在这里插入图片描述

手动置位事件,运行结果如下:
在这里插入图片描述

总结下事件Event

  1. 事件是内核对象,事件分为手动置位事件自动置位事件。事件Event内部它包含一个使用计数(所有内核对象都有),一个布尔值表示是手动置位事件还是自动置位事件,另一个布尔值用来表示事件有无触发。

  2. 事件可以由SetEvent()来触发,由ResetEvent()来设成未触发。还可以由PulseEvent()来发出一个事件脉冲。

  3. 事件可以解决线程间同步问题,因此也能解决互斥问题。

经典线程同步 互斥量Mutex

互斥量与关键段的行为非常相似,并且互斥量可以用于不同进程中的线程互斥访问资源。

  • CreateMutex

    • 创建互斥量(注意与事件Event的创建函数对比)

    • HANDLE CreateMutex( LPSECURITY_ATTRIBUTESlpMutexAttributes, BOOLbInitialOwner, LPCTSTRlpName);

    • 函数说明:

      • 第一个参数表示安全控制,一般直接传入NULL。
      • 第二个参数用来确定互斥量的初始拥有者。如果传入TRUE表示互斥量对象内部会记录创建它的线程的线程ID号并将递归计数设置为1,由于该线程ID非零,所以互斥量处于未触发状态。如果传入FALSE,那么互斥量对象内部的线程ID号将设置为NULL,递归计数设置为0,这意味互斥量不为任何线程占用,处于触发状态
      • 第三个参数用来设置互斥量的名称,在多个进程中的线程就是通过名称来确保它们访问的是同一个互斥量。
    • 函数访问值:成功返回一个表示互斥量的句柄,失败返回NULL。

  • 打开互斥量

    • HANDLE OpenMutex( DWORDdwDesiredAccess, BOOLbInheritHandle, LPCTSTRlpName //名称);

    • 函数说明:

      • 第一个参数表示访问权限,对互斥量一般传入MUTEX_ALL_ACCESS
      • 第二个参数表示互斥量句柄继承性,一般传入TRUE即可。
      • 第三个参数表示名称。某一个进程中的线程创建互斥量后,其它进程中的线程就可以通过这个函数来找到这个互斥量。
    • 函数访问值:

      • 成功返回一个表示互斥量的句柄,失败返回NULL。
  • 触发互斥量

    • BOOL ReleaseMutex (HANDLEhMutex)

    • 函数说明:

      • 访问互斥资源前应该要调用等待函数,结束访问时就要调用**ReleaseMutex()**来表示自己已经结束访问,其它线程可以开始访问了。
//经典线程同步问题 互斥量Mutex
#include <stdio.h>
#include <process.h>
#include <windows.h>
 
long g_nNum;
unsigned int __stdcall Fun(void *pPM);
const int THREAD_NUM = 10;
//互斥量与关键段
HANDLE  g_hThreadParameter;
CRITICAL_SECTION g_csThreadCode;
 
int main()
{
	printf("     经典线程同步 互斥量Mutex\n");
	
	//初始化互斥量与关键段 第二个参数为TRUE表示互斥量为创建线程所有
	g_hThreadParameter = CreateMutex(NULL, FALSE, NULL);
	InitializeCriticalSection(&g_csThreadCode);
 
	HANDLE  handle[THREAD_NUM];	
	g_nNum = 0;	
	int i = 0;
	while (i < THREAD_NUM) 
	{
		handle[i] = (HANDLE)_beginthreadex(NULL, 0, Fun, &i, 0, NULL);
		WaitForSingleObject(g_hThreadParameter, INFINITE); //等待互斥量被触发
		i++;
	}
	WaitForMultipleObjects(THREAD_NUM, handle, TRUE, INFINITE);
	
	//销毁互斥量和关键段
	CloseHandle(g_hThreadParameter);
	DeleteCriticalSection(&g_csThreadCode);
	for (i = 0; i < THREAD_NUM; i++)
		CloseHandle(handle[i]);
	return 0;
}
unsigned int __stdcall Fun(void *pPM)
{
	int nThreadNum = *(int *)pPM;
	ReleaseMutex(g_hThreadParameter);//触发互斥量
	
	Sleep(50);//some work should to do
 
	EnterCriticalSection(&g_csThreadCode);
	g_nNum++;
	Sleep(0);//some work should to do
	printf("线程编号为%d  全局资源值为%d\n", nThreadNum, g_nNum);
	LeaveCriticalSection(&g_csThreadCode);
	return 0;
}

在这里插入图片描述

与关键段类似,互斥量也是不能解决线程间的同步问题。

关键段会记录线程ID即有“线程拥有权”的,而互斥量也记录线程ID,互斥量也是有“线程拥有权”概念的。

由于互斥量常用于多进程之间的线程互斥,所以它比关键段还多一个很有用的特性——“遗弃”情况的处理。比如有一个占用互斥量的线程在调用ReleaseMutex()触发互斥量前就意外终止了(相当于该互斥量被“遗弃”了),那么所有等待这个互斥量的线程是否会由于该互斥量无法被触发而陷入一个无穷的等待过程中了?

这显然不合理。因为占用某个互斥量的线程既然终止了那足以证明它不再使用被该互斥量保护的资源,所以这些资源完全并且应当被其它线程来使用。
因此在这种“遗弃”情况下,系统自动把该互斥量内部的线程ID设置为0,并将它的递归计数器复置为0,表示这个互斥量被触发了。然后系统将“公平地”选定一个等待线程来完成调度(被选中的线程的WaitForSingleObject()会返回WAIT_ABANDONED_0)。

示例验证

  1. 创建互斥量并等待用户输入后就触发互斥量
#include <stdio.h>
#include <conio.h>
#include <windows.h>
const char MUTEX_NAME[] = "Mutex_MoreWindows";
int main()
{
	HANDLE hMutex = CreateMutex(NULL, TRUE, MUTEX_NAME); //创建互斥量
	printf("互斥量已经创建,现在按任意键触发互斥量\n");
	getch();
	//exit(0);
	ReleaseMutex(hMutex);
	printf("互斥量已经触发\n");
	CloseHandle(hMutex);
	return 0;
}

  1. 先打开互斥量,成功后就等待并根据等待结果作相应的输出
#include <stdio.h>
#include <windows.h>
const char MUTEX_NAME[] = "Mutex_MoreWindows";
int main()
{
	HANDLE hMutex = OpenMutex(MUTEX_ALL_ACCESS, TRUE, MUTEX_NAME); //打开互斥量
	if (hMutex == NULL)
	{
		printf("打开互斥量失败\n");
		return 0;
	}
	printf("等待中....\n");
	DWORD dwResult = WaitForSingleObject(hMutex, 20 * 1000); //等待互斥量被触发
	switch (dwResult)
	{
	case WAIT_ABANDONED:
		printf("拥有互斥量的进程意外终止\n");
		break;
 
	case WAIT_OBJECT_0:
		printf("已经收到信号\n");
		break;
 
	case WAIT_TIMEOUT:
		printf("信号未在规定的时间内送到\n");
		break;
	}
	CloseHandle(hMutex);
	return 0;
}

运用这二个程序时要先启动程序1再启动程序2

结果一.二个进程顺利执行完毕:
在这里插入图片描述

结果二.将程序一中//exit(0);前面的注释符号去掉,这样程序1在触发互斥量之前就会因为执行exit(0);语句而且退出,程序2会收到WAIT_ABANDONED消息并输出“拥有互斥量的进程意外终止”:

在这里插入图片描述

这个对“遗弃”问题的处理,在多进程中的线程同步也可以放心的使用mutex

总结下互斥量Mutex

  1. 互斥量是内核对象,它与关键段都有“线程所有权”所以不能用于线程的同步。

  2. 互斥量能够用于多个进程之间线程互斥问题,并且能完美的解决某进程意外终止所造成的“遗弃”问题。

经典线程同步 信号量Semaphore

  • CreateSemaphore

    • 创建信号量

    • HANDLE CreateSemaphore( LPSECURITY_ATTRIBUTES lpSemaphoreAttributes, LONG lInitialCount, LONG lMaximumCount, LPCTSTR lpName );

    • 函数说明:

      • 第一个参数表示安全控制,一般直接传入NULL。
      • 第二个参数表示初始资源数量。
      • 第三个参数表示最大并发数量。
      • 第四个参数表示信号量的名称,传入NULL表示匿名信号量。
  • OpenSemaphore

    • 打开信号量

    • HANDLE OpenSemaphore( DWORD dwDesiredAccess, BOOL bInheritHandle, LPCTSTR lpName);

    • 函数说明:

      • 第一个参数表示访问权限,对一般传入SEMAPHORE_ALL_ACCESS
      • 第二个参数表示信号量句柄继承性,一般传入TRUE即可。
      • 第三个参数表示名称,不同进程中的各线程可以通过名称来确保它们访问同一个信号量。
  • ReleaseSemaphore

    • 递增信号量的当前资源计数
    • BOOL ReleaseSemaphore( HANDLE hSemaphore, LONG lReleaseCount, LPLONG lpPreviousCount );
    • 函数说明:
      • 第一个参数是信号量的句柄。
      • 第二个参数表示增加个数,必须大于0且不超过最大资源数量。
      • 第三个参数可以用来传出先前的资源计数,设为NULL表示不需要传出。

注意
当前资源数量大于0,表示信号量处于触发,等于0表示资源已经耗尽故信号量处于末触发
在对信号量调用等待函数时,等待函数会检查信号量的当前资源计数,如果大于0(即信号量处于触发状态),减1后返回让调用线程继续执行。一个线程可以多次调用等待函数来减小信号量。

在经典多线程问题中设置一个信号量和一个关键段。用信号量处理主线程与子线程的同步,用关键段来处理各子线程间的互斥

#include <stdio.h>
#include <process.h>
#include <windows.h>
long g_nNum;
unsigned int __stdcall Fun(void *pPM);
const int THREAD_NUM = 10;
//信号量与关键段
HANDLE            g_hThreadParameter;
CRITICAL_SECTION  g_csThreadCode;
int main()
{
	printf("     经典线程同步 信号量Semaphore\n");
	printf(" -- by MoreWindows( http://blog.csdn.net/MoreWindows ) --\n\n");
 
	//初始化信号量和关键段
	g_hThreadParameter = CreateSemaphore(NULL, 0, 1, NULL);//当前0个资源,最大允许1个同时访问
	InitializeCriticalSection(&g_csThreadCode);
 
	HANDLE  handle[THREAD_NUM];	
	g_nNum = 0;
	int i = 0;
	while (i < THREAD_NUM) 
	{
		handle[i] = (HANDLE)_beginthreadex(NULL, 0, Fun, &i, 0, NULL);
		WaitForSingleObject(g_hThreadParameter, INFINITE);//等待信号量>0
		++i;
	}
	WaitForMultipleObjects(THREAD_NUM, handle, TRUE, INFINITE);
	
	//销毁信号量和关键段
	DeleteCriticalSection(&g_csThreadCode);
	CloseHandle(g_hThreadParameter);
	for (i = 0; i < THREAD_NUM; i++)
		CloseHandle(handle[i]);
	return 0;
}
unsigned int __stdcall Fun(void *pPM)
{
	int nThreadNum = *(int *)pPM;
	ReleaseSemaphore(g_hThreadParameter, 1, NULL);//信号量++
 
	Sleep(50);//some work should to do
 
	EnterCriticalSection(&g_csThreadCode);
	++g_nNum;
	Sleep(0);//some work should to do
	printf("线程编号为%d  全局资源值为%d\n", nThreadNum, g_nNum);
	LeaveCriticalSection(&g_csThreadCode);
	return 0;
}

在这里插入图片描述

经典线程同步总结 关键段 事件 互斥量 信号量

1.线程(进程)同步的主要任务

在引入多线程后,由于线程执行的异步性,会给系统造成混乱,特别是在急用临界资源时,如多个线程急用同一台打印机,会使打印结果交织在一起,难于区分。当多个线程急用共享变量,表格,链表时,可能会导致数据处理出错,因此线程同步的主要任务是使并发执行的各线程之间能够有效的共享资源和相互合作,从而使程序的执行具有可再现性。

2. 线程(进程)之间的制约关系?

当线程并发执行时,由于资源共享和线程协作,使用线程之间会存在以下两种制约关系。

  • 间接相互制约。一个系统中的多个线程必然要共享某种系统资源,如共享CPU,共享I/O设备,所谓间接相互制约即源于这种资源共享,打印机就是最好的例子,线程A在使用打印机时,其它线程都要等待。
  • 直接相互制约。这种制约主要是因为线程之间的合作,如有线程A将计算结果提供给线程B作进一步处理,那么线程B在线程A将数据送达之前都将处于阻塞状态。

间接相互制约可以称为互斥,直接相互制约可以称为同步,对于互斥可以这样理解,线程A和线程B互斥访问某个资源则它们之间就会产个顺序问题——要么线程A等待线程B操作完毕,要么线程B等待线程操作完毕,这其实就是线程的同步了。因此同步包括互斥,互斥其实是一种特殊的同步

3. 临界资源和临界区

在一段时间内只允许一个线程访问的资源就称为临界资源或独占资源,计算机中大多数物理设备,进程中的共享变量等待都是临界资源,它们要求被互斥的访问。每个进程中访问临界资源的代码称为临界区

关键段CS与互斥量Mutex

创建或初始化 销毁 进入互斥区域 离开互斥区域
关键段 CSInitialize-CriticalSection Delete-CriticalSection Enter-CriticalSection Leave-CriticalSection
互斥量 MutexCreateMutex CloseHandle 等待系列函数如WaitForSingleObject ReleaseMutex

关键段与互斥量都有“线程所有权”概念,可以将“线程所有权”理解成旅馆的房卡,在旅馆前台登记名字拥有房卡后是可以多次进出房间的,其它人则无法进入直到你交出房卡。每个线程必须先通过EnterCriticalSectionWaitForSingleObject来尝试获得“线程所有权”才能调用LeaveCriticalSectionReleaseMutex。否则会调用失败,这就相当于伪造房卡去办理退房手续——由于登记本上没有你的名字所以会被拒绝。

互斥量能很好的处理“遗弃”情况,因此在多进程之间可以放心的使用。

事件Event

创建 销毁 使事件触发 使事件未触发
事件Event CreateEvent CloseHandle SetEvent ResetEvent

注意事件的手动置位自动置位

信号量Semaphore

创建 销毁 递减计数 递增计数
信号量Semaphore CreateSemaphore CloseHandle 等待系列函数如WaitForSingleObject ReleaseSemaphore

信号量在计数大于0时表示触发状态,调用WaitForSingleObject不会阻塞,等于0表示未触发状态,调用WaitForSingleObject会阻塞直到有其它线程递增了计数。

注意:互斥量,事件,信号量都是内核对象,可以跨进程使用(通过OpenMutex,OpenEvent,OpenSemaphore)。

不过只有互斥量能解决“遗弃”情况。

生产者消费者问题

问题描述
有一个生产者在生产产品,这些产品将提供给若干个消费者去消费,为了使生产者和消费者能并发执行,在两者之间设置一个具有多个缓冲区的缓冲池,生产者将它生产的产品放入一个缓冲区中,消费者可以从缓冲区中取走产品进行消费,显然生产者和消费者之间必须保持 同步,即不允许消费者到一个空的缓冲区中取产品,也不允许生产者向一个已经放入产品的缓冲区中再次投放产品。

首先来简化问题,先假设生产者和消费者都只有一个,且缓冲区也只有一个。这样情况就简便多了。

  • 第一.从缓冲区取出产品和向缓冲区投放产品必须是互斥进行的。可以用关键段和互斥量来完成。
  • 第二.生产者要等待缓冲区为空,这样才可以投放产品,消费者要等待缓冲区不为空,这样才可以取出产品进行消费。并且由于有二个等待过程,所以要用二个事件或信号量来控制。
//1生产者 1消费者 1缓冲区
//使用二个事件,一个表示缓冲区空,一个表示缓冲区满。
//再使用一个关键段来控制缓冲区的访问
#include <stdio.h>
#include <process.h>
#include <windows.h>
//设置控制台输出颜色
BOOL SetConsoleColor(WORD wAttributes)
{
	HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE);
	if (hConsole == INVALID_HANDLE_VALUE)
		return FALSE;	
	return SetConsoleTextAttribute(hConsole, wAttributes);
}
const int END_PRODUCE_NUMBER = 10;   //生产产品个数
int g_Buffer;                        //缓冲区
//事件与关键段
CRITICAL_SECTION g_cs;
HANDLE g_hEventBufferEmpty, g_hEventBufferFull;
//生产者线程函数
unsigned int __stdcall ProducerThreadFun(PVOID pM)
{
	for (int i = 1; i <= END_PRODUCE_NUMBER; i++)
	{
		//等待缓冲区为空
		WaitForSingleObject(g_hEventBufferEmpty, INFINITE);
 
		//互斥的访问缓冲区
		EnterCriticalSection(&g_cs);
		g_Buffer = i;
		printf("生产者将数据%d放入缓冲区\n", i);
		LeaveCriticalSection(&g_cs);
		
		//通知缓冲区有新数据了
		SetEvent(g_hEventBufferFull);
	}
	return 0;
}
//消费者线程函数
unsigned int __stdcall ConsumerThreadFun(PVOID pM)
{
	volatile bool flag = true;
	while (flag)
	{
		//等待缓冲区中有数据
		WaitForSingleObject(g_hEventBufferFull, INFINITE);
		
		//互斥的访问缓冲区
		EnterCriticalSection(&g_cs);
		SetConsoleColor(FOREGROUND_GREEN);
		printf("  消费者从缓冲区中取数据%d\n", g_Buffer);
		SetConsoleColor(FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE);
		if (g_Buffer == END_PRODUCE_NUMBER)
			flag = false;
		LeaveCriticalSection(&g_cs);
		
		//通知缓冲区已为空
		SetEvent(g_hEventBufferEmpty);
 
		Sleep(10); //some other work should to do
	}
	return 0;
}
int main()
{
	printf("  生产者消费者问题   1生产者 1消费者 1缓冲区\n");
 
	InitializeCriticalSection(&g_cs);
	//创建二个自动复位事件,一个表示缓冲区是否为空,另一个表示缓冲区是否已经处理
	g_hEventBufferEmpty = CreateEvent(NULL, FALSE, TRUE, NULL);
	g_hEventBufferFull = CreateEvent(NULL, FALSE, FALSE, NULL);
	
	const int THREADNUM = 2;
	HANDLE hThread[THREADNUM];
	
	hThread[0] = (HANDLE)_beginthreadex(NULL, 0, ProducerThreadFun, NULL, 0, NULL);
	hThread[1] = (HANDLE)_beginthreadex(NULL, 0, ConsumerThreadFun, NULL, 0, NULL);
	WaitForMultipleObjects(THREADNUM, hThread, TRUE, INFINITE);
	CloseHandle(hThread[0]);
	CloseHandle(hThread[1]);
	
	//销毁事件和关键段
	CloseHandle(g_hEventBufferEmpty);
	CloseHandle(g_hEventBufferFull);
	DeleteCriticalSection(&g_cs);
	return 0;
}

在这里插入图片描述

再对这个简单生产者消费者问题加大难度。将消费者改成2个,缓冲池改成拥有4个缓冲区的大缓冲池

//1生产者 2消费者 4缓冲区
#include <stdio.h>
#include <process.h>
#include <windows.h>
//设置控制台输出颜色
BOOL SetConsoleColor(WORD wAttributes)
{
	HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE);
	if (hConsole == INVALID_HANDLE_VALUE)
		return FALSE;
	
	return SetConsoleTextAttribute(hConsole, wAttributes);
}
const int END_PRODUCE_NUMBER = 8;  //生产产品个数
const int BUFFER_SIZE = 4;          //缓冲区个数
int g_Buffer[BUFFER_SIZE];          //缓冲池
int g_i, g_j;
//信号量与关键段
CRITICAL_SECTION g_cs;
HANDLE g_hSemaphoreBufferEmpty, g_hSemaphoreBufferFull;
//生产者线程函数
unsigned int __stdcall ProducerThreadFun(PVOID pM)
{
	for (int i = 1; i <= END_PRODUCE_NUMBER; i++)
	{
		//等待有空的缓冲区出现
		WaitForSingleObject(g_hSemaphoreBufferEmpty, INFINITE);
 
		//互斥的访问缓冲区
		EnterCriticalSection(&g_cs);
		g_Buffer[g_i] = i;
		printf("生产者在缓冲池第%d个缓冲区中投放数据%d\n", g_i, g_Buffer[g_i]);
		g_i = (g_i + 1) % BUFFER_SIZE;
		LeaveCriticalSection(&g_cs);
 
		//通知消费者有新数据了
		ReleaseSemaphore(g_hSemaphoreBufferFull, 1, NULL);
	}
	printf("生产者完成任务,线程结束运行\n");
	return 0;
}
//消费者线程函数
unsigned int __stdcall ConsumerThreadFun(PVOID pM)
{
	while (true)
	{
		//等待非空的缓冲区出现
		WaitForSingleObject(g_hSemaphoreBufferFull, INFINITE);
		
		//互斥的访问缓冲区
		EnterCriticalSection(&g_cs);
		SetConsoleColor(FOREGROUND_GREEN);
		printf("  编号为%d的消费者从缓冲池中第%d个缓冲区取出数据%d\n", GetCurrentThreadId(), g_j, g_Buffer[g_j]);
		SetConsoleColor(FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE);
		if (g_Buffer[g_j] == END_PRODUCE_NUMBER)//结束标志
		{
			LeaveCriticalSection(&g_cs);
			//通知其它消费者有新数据了(结束标志)
			ReleaseSemaphore(g_hSemaphoreBufferFull, 1, NULL);
			break;
		}
		g_j = (g_j + 1) % BUFFER_SIZE;
		LeaveCriticalSection(&g_cs);
 
		Sleep(50); //some other work to do
 
		ReleaseSemaphore(g_hSemaphoreBufferEmpty, 1, NULL);
	}
	SetConsoleColor(FOREGROUND_GREEN);
	printf("  编号为%d的消费者收到通知,线程结束运行\n", GetCurrentThreadId());
	SetConsoleColor(FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE);
	return 0;
}
int main()
{
	printf("  生产者消费者问题   1生产者 2消费者 4缓冲区\n");
 
	InitializeCriticalSection(&g_cs);
	//初始化信号量,一个记录有产品的缓冲区个数,另一个记录空缓冲区个数.
	g_hSemaphoreBufferEmpty = CreateSemaphore(NULL, 4, 4, NULL);
	g_hSemaphoreBufferFull  = CreateSemaphore(NULL, 0, 4, NULL);
	g_i = 0;
	g_j = 0;
	memset(g_Buffer, 0, sizeof(g_Buffer));
 
	const int THREADNUM = 3;
	HANDLE hThread[THREADNUM];
	//生产者线程
	hThread[0] = (HANDLE)_beginthreadex(NULL, 0, ProducerThreadFun, NULL, 0, NULL);
	//消费者线程
	hThread[1] = (HANDLE)_beginthreadex(NULL, 0, ConsumerThreadFun, NULL, 0, NULL);
	hThread[2] = (HANDLE)_beginthreadex(NULL, 0, ConsumerThreadFun, NULL, 0, NULL);
	WaitForMultipleObjects(THREADNUM, hThread, TRUE, INFINITE);
	for (int i = 0; i < THREADNUM; i++)
		CloseHandle(hThread[i]);
 
	//销毁信号量和关键段
	CloseHandle(g_hSemaphoreBufferEmpty);
	CloseHandle(g_hSemaphoreBufferFull);
	DeleteCriticalSection(&g_cs);
	return 0;
}

在这里插入图片描述

总结

  1. 首先要考虑生产者与消费者对缓冲区操作时的互斥。
  2. 不管生产者与消费者有多少个,缓冲池有多少个缓冲区。都只有二个同步过程——分别是生产者要等待有空缓冲区才能投放产品,消费者要等待有非空缓冲区才能去取产品。

读者写者问题

问题描述
有一个写者很多读者,多个读者可以同时读文件,但写者在写文件时不允许有读者在读文件,同样有读者在读文件时写者也不去能写文件。
在这里插入图片描述

上面是读者写者问题示意图,类似于生产者消费者问题的分析过程,首先来找找哪些是属于“等待”情况。

  1. 写者要等到没有读者时才能去写文件。

  2. 所有读者要等待写者完成写文件后才能去读文件。

找完“等待”情况后,再看看有没有要互斥访问的资源。由于只有一个写者而读者们是可以共享的读文件,所以按题目要求并没有需要互斥访问的资源。

//读者与写者问题
#include <stdio.h>
#include <process.h>
#include <windows.h>
//设置控制台输出颜色
BOOL SetConsoleColor(WORD wAttributes)
{
	HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE);
	if (hConsole == INVALID_HANDLE_VALUE)
		return FALSE;
	
	return SetConsoleTextAttribute(hConsole, wAttributes);
}
const int READER_NUM = 5;  //读者个数
//关键段和事件
CRITICAL_SECTION g_cs, g_cs_writer_count;
HANDLE g_hEventWriter, g_hEventNoReader;
int g_nReaderCount;
//读者线程输出函数(变参函数的实现)
void ReaderPrintf(char *pszFormat, ...)
{
	va_list   pArgList;
	
	va_start(pArgList, pszFormat);
	EnterCriticalSection(&g_cs);
	vfprintf(stdout, pszFormat, pArgList);
	LeaveCriticalSection(&g_cs);
	va_end(pArgList);
}
//读者线程函数
unsigned int __stdcall ReaderThreadFun(PVOID pM)
{
	ReaderPrintf("     编号为%d的读者进入等待中...\n", GetCurrentThreadId());
	//等待写者完成
	WaitForSingleObject(g_hEventWriter, INFINITE);
 
	//读者个数增加
	EnterCriticalSection(&g_cs_writer_count);
	g_nReaderCount++;
	if (g_nReaderCount == 1)
		ResetEvent(g_hEventNoReader);
	LeaveCriticalSection(&g_cs_writer_count);
 
	//读取文件
	ReaderPrintf("编号为%d的读者开始读取文件...\n", GetCurrentThreadId());
 
	Sleep(rand() % 100);
 
	//结束阅读,读者个数减小,空位增加
	ReaderPrintf(" 编号为%d的读者结束读取文件\n", GetCurrentThreadId());
 
	//读者个数减少
	EnterCriticalSection(&g_cs_writer_count);
	g_nReaderCount--;
	if (g_nReaderCount == 0)
		SetEvent(g_hEventNoReader);
	LeaveCriticalSection(&g_cs_writer_count);
 
	return 0;
}
//写者线程输出函数
void WriterPrintf(char *pszStr)
{
	EnterCriticalSection(&g_cs);
	SetConsoleColor(FOREGROUND_GREEN);
	printf("     %s\n", pszStr);
	SetConsoleColor(FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE);
	LeaveCriticalSection(&g_cs);
}
//写者线程函数
unsigned int __stdcall WriterThreadFun(PVOID pM)
{
	WriterPrintf("写者线程进入等待中...");
	//等待读文件的读者为零
	WaitForSingleObject(g_hEventNoReader, INFINITE);
	//标记写者正在写文件
	ResetEvent(g_hEventWriter);
		
	//写文件
	WriterPrintf("  写者开始写文件.....");
	Sleep(rand() % 100);
	WriterPrintf("  写者结束写文件");
 
	//标记写者结束写文件
	SetEvent(g_hEventWriter);
	return 0;
}
int main()
{
	printf("  读者写者问题\n");
	
	//初始化事件和信号量
	InitializeCriticalSection(&g_cs);
	InitializeCriticalSection(&g_cs_writer_count);
 
	//手动置位,初始已触发
	g_hEventWriter = CreateEvent(NULL, TRUE, TRUE, NULL);
	g_hEventNoReader  = CreateEvent(NULL, FALSE, TRUE, NULL);
	g_nReaderCount = 0;
 
	int i;
	HANDLE hThread[READER_NUM + 1];
	//先启动二个读者线程
	for (i = 1; i <= 2; i++)
		hThread[i] = (HANDLE)_beginthreadex(NULL, 0, ReaderThreadFun, NULL, 0, NULL);
	//启动写者线程
	hThread[0] = (HANDLE)_beginthreadex(NULL, 0, WriterThreadFun, NULL, 0, NULL);
	Sleep(50);
	//最后启动其它读者结程
	for ( ; i <= READER_NUM; i++)
		hThread[i] = (HANDLE)_beginthreadex(NULL, 0, ReaderThreadFun, NULL, 0, NULL);
	WaitForMultipleObjects(READER_NUM + 1, hThread, TRUE, INFINITE);
	for (i = 0; i < READER_NUM + 1; i++)
		CloseHandle(hThread[i]);
 
	//销毁事件和信号量
	CloseHandle(g_hEventWriter);
	CloseHandle(g_hEventNoReader);
	DeleteCriticalSection(&g_cs);
	DeleteCriticalSection(&g_cs_writer_count);
	return 0;
}

在这里插入图片描述

PV操作

PV操作由P操作原语和V操作原语组成(原语也叫原子操作Atomic Operation,是不可中断的过程),对信号量(注意不要和Windows中的信号量机制相混淆)进行操作,具体定义如下:

P(S)
①将信号量S的值减1,即S=S-1
②如果S>=0,则该进程继续执行;否则该进程置为等待状态。

V(S)
①将信号量S的值加1,即S=S+1
②该进程继续执行;如果该信号的等待队列中有等待进程就唤醒一等待进程。

PV操作实现多线程的同步与互斥是非常简单的,只要考虑逻辑处理上合理严密而不用考虑具体技术细节,因此与写伪代码较为相似。比如有多个进程P1、P2、 ……PN。它们要互斥的访问一个资源。用PV操作来实现就非常方便直观。

下面是PV操作代码:设置信号量为S,初值为1。各进程的操作流程如下:

进程P1                进程P2           ……         进程Pn
P(S);              P(S);                     P(S);
访问资源;            访问资源;                   访问资源;
V(S);              V(S);                     V(S);

可以看出PV操作会忽略具体的编程细节,让程序员的主要精力放在线程同步互斥的逻辑处理上。因此,通过练习PV操作能快速有效提高程序员对多线程的逻辑思维能力,达到强化“内功”的目的。

第一题 放水果

桌上有一空盘,允许存放一只水果。爸爸可向盘中放苹果,也可向盘中放桔子,儿子专等吃盘中的桔子,女儿专等吃盘中的苹果。
规定当盘空时一次只能放一只水果供吃者取用,请用P、V原语实现爸爸、儿子、女儿三个并发进程的同步。

这个题目涉及的东西非常之多,光人物就有三个再加水果,盘子等等,确实让人感觉好像无从下手。
但不管题目如何变,只要牢牢的抓住同步互斥来分析问题就必定能迎刃而解。

下面先考虑同步情况即所有“等待”情况:
第一.爸爸要等待盘子为空。
第二.儿子要等待盘中水果是桔子。
第三.女儿要等待盘中水果是苹果。

接下来来考虑要互斥处理的资源,看起来盘子好像是要作互斥处理的,但由于题目中的爸爸、儿子、女儿均只有一个,并且他们访问盘子的条件都不一样,所以他们根本不会同时去访问盘子,因此盘子也就不用作互斥处理了。

分析至些,这个题目已经没有难度了,
下面用PV原语给出答案:
先设置三个信号量,信号量Orange表示盘中有桔子,初值为0。信号量Apple表示盘中有苹果,初值为0。信号量EmptyDish表示盘子为空,初值为1。

三个人的操作流程如下所示:

1.爸爸

P(EmptyDish)

if (rand()%2==0)
{  
    放桔子
    V(Orange)
}
else
{
    放苹果
    V(Apple)
}

2.儿子

P(Orange)
取桔子
V(EmptyDish)

3.女儿

P(Apple)
取苹果
V(EmptyDish)
第二题 安全岛

在南开大学至天津大学间有一条弯曲的路,每次只允许一辆自行车通过,但中间有小的安全岛M(同时允许两辆车),可供两辆车在已进入两端小车错车,设计算法并使用P,V实现。

这个问题应该如何考虑了?
同样只要牢牢的抓住同步和互斥来分析问题就必定能迎刃而解。

考虑所有“等待”情况:
在路口N准备从N到T的人应该什么时候进入了?
如果他只判断道路K上有没有人肯定是不行的,因为如果安全岛M上已经有2个人,那么路口N和路口T再各进一人,肯定会造成死锁。

因此可以这样
——在路口N准备从N到T的人要等待与他同方向的人已经到达T,如果此人已经到达T,且道路K上没有人,他必定可以上路了。
同理在路口T准备从T到N的人也应该这样做。

再考虑互斥情况:路上每次只允许一辆自行车通过,所以道路是需要作互斥处理的。

分析之后,下面就用PV原语给出答案:
设置信号量NT表示在路口N且从N到T方向上允许出发的自行车数量,初值为1。
信号量TN表示在路口T且从T到N方向上允许出发的自行车数量,初值为1。
信号量KL表示道路,初值均为1。

这样从N到T的车和从T到N的车的行驶流程如下:

从N到T的车                     从T到N的车P(NT)       
  P(TN)P(K)                 	P(L)
  由N到M               			由T到M
  V(K)                 			V(L)
  P(L)                 			P(K)
  由M到T               			由M到T
  V(L)                			V(K)
  V(NT)                			V(TN) 

这个题目的解法有很多,比如还可以用信号量M来记录安全岛M上空位个数,初值为2。每个进入道路前的人都要先预订安全岛上的空位,订到后再互斥的进入道路。否则就要等待安全岛上有空位。信号量K和L表示道路,初值均为1。

然后从N到T的车从T到N的车的行驶流程如下:

从N到T的车          从T到N的车
P(M)                 P(M)
P(K)                 P(L)
由N到M               由T到M
V(K)                 V(L)
P(L)                 P(K)
V(M)                 V(M)
由M到T               由M到T
V(L)                 V(K)

读者写者问题继:读写锁SRWLock

读写锁在对资源进行保护的同时,还能区分想要读取资源值的线程(读取者线程)和想要更新资源的线程(写入者线程)
对于读取者线程,读写锁会允许他们并发的执行。当有写入者线程在占有资源时,读写锁会让其它写入者线程和读取者线程等待。

注意编译读写锁程序需要VS2008

读写锁的主要函数就五个,分为初始化函数写入者线程申请释放函数读取者线程申请释放函数

  • InitializeSRWLock

    • 初始化读写锁
    • VOID InitializeSRWLock(PSRWLOCK SRWLock);
    • 函数说明
      • 初始化(没有删除或销毁SRWLOCK的函数,系统会自动清理)
  • AcquireSRWLockExclusive

    • 写入者线程申请写资源。
    • VOID AcquireSRWLockExclusive(PSRWLOCK SRWLock);
  • ReleaseSRWLockExclusive

    • 写入者线程写资源完毕,释放对资源的占用
    • VOID ReleaseSRWLockExclusive(PSRWLOCK SRWLock);
  • AcquireSRWLockShared

    • 读取者线程申请读资源。
    • VOID AcquireSRWLockShared(PSRWLOCK SRWLock);
  • ReleaseSRWLockShared

    • 读取者线程结束读取资源,释放对资源的占用
    • VOID ReleaseSRWLockShared(PSRWLOCK SRWLock);

注意一个线程仅能锁定资源一次,不能多次锁定资源。

//读者与写者问题继 读写锁SRWLock
#include <stdio.h>
#include <process.h>
#include <windows.h>
//设置控制台输出颜色
BOOL SetConsoleColor(WORD wAttributes)
{
    HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE);
    if (hConsole == INVALID_HANDLE_VALUE)
        return FALSE;
    return SetConsoleTextAttribute(hConsole, wAttributes);
}
const int READER_NUM = 5;  //读者个数
//关键段和事件
CRITICAL_SECTION g_cs;
SRWLOCK          g_srwLock; 
//读者线程输出函数(变参函数的实现)
void ReaderPrintf(char *pszFormat, ...)
{
    va_list   pArgList;
    va_start(pArgList, pszFormat);
    EnterCriticalSection(&g_cs);
    vfprintf(stdout, pszFormat, pArgList);
    LeaveCriticalSection(&g_cs);
    va_end(pArgList);
}
//读者线程函数
unsigned int __stdcall ReaderThreadFun(PVOID pM)
{
    ReaderPrintf("     编号为%d的读者进入等待中...\n", GetCurrentThreadId());
    //读者申请读取文件
    AcquireSRWLockShared(&g_srwLock);

    //读取文件
    ReaderPrintf("编号为%d的读者开始读取文件...\n", GetCurrentThreadId());
    Sleep(rand() % 100);
    ReaderPrintf(" 编号为%d的读者结束读取文件\n", GetCurrentThreadId());

    //读者结束读取文件
    ReleaseSRWLockShared(&g_srwLock);
    return 0;
}
//写者线程输出函数
void WriterPrintf(char *pszStr)
{
    EnterCriticalSection(&g_cs);
    SetConsoleColor(FOREGROUND_GREEN);
    printf("     %s\n", pszStr);
    SetConsoleColor(FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE);
    LeaveCriticalSection(&g_cs);
}
//写者线程函数
unsigned int __stdcall WriterThreadFun(PVOID pM)
{
    WriterPrintf("写者线程进入等待中...");
    //写者申请写文件
    AcquireSRWLockExclusive(&g_srwLock);

    //写文件
    WriterPrintf("  写者开始写文件.....");
    Sleep(rand() % 100);
    WriterPrintf("  写者结束写文件");

    //标记写者结束写文件
    ReleaseSRWLockExclusive(&g_srwLock);
    return 0;
}
int main()
{
    printf("  读者写者问题继 读写锁SRWLock\n");
    
    //初始化读写锁和关键段
    InitializeCriticalSection(&g_cs);
    InitializeSRWLock(&g_srwLock);

    HANDLE hThread[READER_NUM + 1];
    int i;
    //先启动二个读者线程
    for (i = 1; i <= 2; i++)
        hThread[i] = (HANDLE)_beginthreadex(NULL, 0, ReaderThreadFun, NULL, 0, NULL);
    //启动写者线程
    hThread[0] = (HANDLE)_beginthreadex(NULL, 0, WriterThreadFun, NULL, 0, NULL);
    Sleep(50);
    //最后启动其它读者结程
    for ( ; i <= READER_NUM; i++)
        hThread[i] = (HANDLE)_beginthreadex(NULL, 0, ReaderThreadFun, NULL, 0, NULL);
    WaitForMultipleObjects(READER_NUM + 1, hThread, TRUE, INFINITE);
    for (i = 0; i < READER_NUM + 1; i++)
        CloseHandle(hThread[i]);

    //销毁关键段
    DeleteCriticalSection(&g_cs);
    return 0;
}

在这里插入图片描述

总结读写锁SRWLock

  1. 读写锁声明后要初始化,但不用销毁,系统会自动清理读写锁。

  2. 读取者和写入者分别调用不同的申请函数和释放函数。

关键段,事件,互斥量,信号量的“遗弃”问题

什么是“遗弃”问题

互斥量常用于多进程之间的线程互斥,所以它比关键段还多一个很有用的特性——“遗弃”情况的处理。比如有一个占用互斥量的线程在调用ReleaseMutex()触发互斥量前就意外终止了(相当于该互斥量被“遗弃”了),那么所有等待这个互斥量的线程是否会由于该互斥量无法被触发而陷入一个无穷的等待过程中了?这显然不合理。因为占用某个互斥量的线程既然终止了那足以证明它不再使用被该互斥量保护的资源,所以这些资源完全并且应当被其它线程来使用。因此在这种“遗弃”情况下,系统自动把该互斥量内部的线程ID设置为0,并将它的递归计数器复置为0,表示这个互斥量被触发了。然后系统将“公平地”选定一个等待线程来完成调度(被选中的线程的WaitForSingleObject()会返回WAIT_ABANDONED_0)。

可见“遗弃”问题就是——占有某种资源的进程意外终止后,其它等待该资源的进程能否感知

关键段的“遗弃”问题

关键段在这个问题上很简单——由于关键段不能跨进程使用,所以关键段不需要处理“遗弃”问题。

事件,互斥量,信号量的“遗弃”问题

事件,互斥量,信号量都是内核对象,可以跨进程使用。一个进程在创建一个命名的事件后,其它进程可以调用OpenEvent()并传入事件的名称来获得这个事件的句柄。

因此事件,互斥量和信号量都会遇到“遗弃”问题。我们已经知道互斥量能够处理“遗弃”问题,接下来就来看看事件信号量是否能够处理“遗弃”问题。

事件信号量作同样的试验:

  1. 创建二个进程。
  2. 进程一创建一个初始为未触发的事件,然后等待按键,按下y则触发事件后结束进程,否则直接退出表示进程一已意外终止。
  3. 进程二先获得事件的句柄,然后调用WaitForSingleObject()等待这个事件10秒,在这10秒内如果事件已经触发则输出“已收到信号”,否则输出“未在规定的时间内收到信号”。如果在等待的过程中进程一意外终止,则输出“拥有事件的进程意外终止”。信号量的试验方法类似。
    在这里插入图片描述

可以看出在第一个进程在没有触发互斥量就直接退出的情况下,等待这个互斥量的第二个进程是能够感知进程一所发生的意外终止的。

完成事件的“遗弃”问题试验代码:

进程一

#include <stdio.h>
#include <conio.h>
#include <windows.h>
const TCHAR STR_EVENT_NAME[] = TEXT("Event_MoreWindows");
int main()
{
	printf("     经典线程同步 事件的遗弃处理  进程一\n");  
	
	HANDLE hEvent = CreateEvent(NULL, FALSE, FALSE, STR_EVENT_NAME);//自动置位 当前未触发
	printf("事件已经创建,现在按y触发事件,按其它键终止进程\n");
	char ch;
	scanf("%c", &ch);
	if (ch != 'y')
		exit(0); //表示进程意外终止
	SetEvent(hEvent);
	printf("事件已经触发\n");
	CloseHandle(hEvent);
	return 0;
}

进程二

#include <stdio.h>
#include <windows.h>
const TCHAR STR_EVENT_NAME[] = TEXT("Event_MoreWindows");
int main()
{
	printf("     经典线程同步 事件的遗弃处理  进程二\n");  
 
	HANDLE hEvent = OpenEvent(EVENT_ALL_ACCESS, TRUE, STR_EVENT_NAME); //打开事件
	if (hEvent == NULL)
	{
		printf("打开事件失败\n");
		return 0;
	}
	printf(" 等待中....\n");
	DWORD dwResult = WaitForSingleObject(hEvent, 10 * 1000); //等待事件被触发
	switch (dwResult)
	{
	case WAIT_ABANDONED:
		printf("拥有事件的进程意外终止\n");
		break;
 
	case WAIT_OBJECT_0:
		printf("已经收到信号\n");
		break;
 
	case WAIT_TIMEOUT:
		printf("未在规定的时间内收到信号\n");
		break;
	}
	CloseHandle(hEvent);
	return 0;
}

事件Event试验结果1-进程一触发事件后正常结束:
在这里插入图片描述

事件Event试验结果2-进程一意外终止:
在这里插入图片描述
可以看出进程二没能感知进程一意外终止,说明事件不能处理“遗弃”问题

信号量的“遗弃”问题试验代码:

进程一

#include <stdio.h>
#include <conio.h>
#include <windows.h>
const TCHAR STR_SEMAPHORE_NAME[] = TEXT("Semaphore_MoreWindows");
int main()
{
	printf("     经典线程同步 信号量的遗弃处理  进程一\n");  
	printf(" -- by MoreWindows( http://blog.csdn.net/MoreWindows ) --\n\n");  
 
	HANDLE hSemaphore = CreateSemaphore(NULL, 0, 1, STR_SEMAPHORE_NAME);//当前0个资源,最大允许1个同时访问
	printf("信号量已经创建,现在按y触发信号量,按其它键终止进程\n");
	char ch;
	scanf("%c", &ch);
	if (ch != 'y')
		exit(0); //表示进程意外终止
	ReleaseSemaphore(hSemaphore, 1, NULL);
	printf("信号量已经触发\n");
	CloseHandle(hSemaphore);
	return 0;
}

进程二

#include <stdio.h>
#include <windows.h>
const TCHAR STR_SEMAPHORE_NAME[] = TEXT("Semaphore_MoreWindows");
int main()
{
	printf("     经典线程同步 信号量的遗弃处理  进程二\n");  
	printf(" -- by MoreWindows( http://blog.csdn.net/MoreWindows ) --\n\n");  
 
	HANDLE hSemaphore = OpenSemaphore (SEMAPHORE_ALL_ACCESS, TRUE, STR_SEMAPHORE_NAME); //打开信号量
	if (hSemaphore == NULL)
	{
		printf("打开信号量失败\n");
		return 0;
	}
	printf(" 等待中....\n");
	DWORD dwResult = WaitForSingleObject(hSemaphore, 10 * 1000); //等待信号量被触发
	switch (dwResult)
	{
	case WAIT_ABANDONED:
		printf("拥有信号量的进程意外终止\n");
		break;
 
	case WAIT_OBJECT_0:
		printf("已经收到信号\n");
		break;
 
	case WAIT_TIMEOUT:
		printf("未在规定的时间内收到信号\n");
		break;
	}
	CloseHandle(hSemaphore);
	return 0;
}

信号量Semaphore试验结果1-进程一触发信号量后正常结束

在这里插入图片描述

信号量Semaphore试验结果2-进程一意外终止
在这里插入图片描述

可以看出进程二没能感知进程一意外终止,说明信号量与事件一样都不能处理“遗弃”问题

“遗弃”问题总结

互斥量能够处理“遗弃”情况,事件与信号量都无法解决这一情况。

再思考下互斥量能处理“遗弃”问题的原因,其实正是因为它有“线程所有权”概念。在系统中一旦有线程结束后,系统会判断是否有互斥量被这个线程占有,如果有,系统会将这互斥量对象内部的线程ID号将设置为NULL,递归计数设置为0,这表示该互斥量已经不为任何线程占用,处于触发状态。其它等待这个互斥量的线程就能顺利执行下去了。

多线程十大经典案例之一 双线程读写队列数据

案例描述
MFC对话框中一个按钮的响应函数实现两个功能:
显示数据同时处理数据,因此开两个线程,一个线程显示数据(开了一个定时器,响应WM_TIMER消息按照一定时间间隔向TeeChart图表添加数据并显示)同时在队列队尾添加数据,另一个线程从该队列队头去数据来处理。

案例分析

这个案例是一个线程向队列中的队列头部读取数据,一个线程向队列中的队列尾部写入数据。看起来很像读者写者问题(见《秒杀多线程第十一篇读者写者问题》和《秒杀多线程第十四篇读者写者问题继读写锁SRWLock》),但其实不然,如果将队列看成缓冲区,这个案例明显是个生产者消费者问题(见《秒杀多线程第十篇生产者消费者问题》)。

因此我们仿照生产者消费者的思路来具体分析下案例中的“等待”情况:

  1. 当队列为空时,读取数据线程必须等待写入数据向队列中写入数据。也就是说当队列为空时,读取数据线程要等待队列中有数据

  2. 当队列满时,写入数据线程必须等待读取数据线程向队列中读取数据。也就是说当队列满时,写入数据线程要等待队列中有空位

访问队列时,需要互斥吗?
这将依赖于队列的数据结构实现,如果使用STL中的vector,由于vector会动态增长。因此要做互斥保护。如果使用循环队列,那么读取数据线程拥有读取指针,写入数据线程拥有写入指针,各自将访问队列中不同位置上的数据,因此不用进行互斥保护。

分析完毕后,再来考虑使用什么样的数据结构,同样依照《秒杀多线程第十篇生产者消费者问题》中的做法。使用两个信号量,一个来记录循环队列中空位的个数,一个来记录循环队列中产品的个数(非空位个数)。

//秒杀多线程第十六篇 多线程十大经典案例之一 双线程读写队列数据
//http://blog.csdn.net/MoreWindows/article/details/8646902
#include <stdio.h>
#include <process.h>
#include <windows.h>
#include <time.h>
const int QUEUE_LEN = 5;
int g_arrDataQueue[QUEUE_LEN];
int g_i, g_j, g_nDataNum;
//关键段 用于保证互斥的在屏幕上输出
CRITICAL_SECTION g_cs;
//信号量 g_hEmpty表示队列中空位 g_hFull表示队列中非空位
HANDLE     g_hEmpty, g_hFull;
//设置控制台输出颜色
BOOL SetConsoleColor(WORD wAttributes)
{
	HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE);
	if (hConsole == INVALID_HANDLE_VALUE)
		return FALSE;	
	return SetConsoleTextAttribute(hConsole, wAttributes);
}
//读数据线程函数
unsigned int __stdcall ReaderThreadFun(PVOID pM)
{
	int nData = 0;
	while (nData < 20)
	{
		WaitForSingleObject(g_hFull, INFINITE);
		nData = g_arrDataQueue[g_i];
		g_i = (g_i + 1) % QUEUE_LEN;
		EnterCriticalSection(&g_cs);
		printf("从队列中读数据%d\n", nData);
		LeaveCriticalSection(&g_cs);
		Sleep(rand() % 300);
		ReleaseSemaphore(g_hEmpty, 1, NULL);
	}
	return 0;
}
//写数据线程函数
unsigned int __stdcall WriterThreadFun(PVOID pM)
{
	int nData = 0;
	while (nData < 20)
	{
		WaitForSingleObject(g_hEmpty, INFINITE);
		g_arrDataQueue[g_j] = ++nData;
		g_j = (g_j + 1) % QUEUE_LEN;
		EnterCriticalSection(&g_cs);
		SetConsoleColor(FOREGROUND_GREEN);
		printf("    将数据%d写入队列\n", nData);
		SetConsoleColor(FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE);
		LeaveCriticalSection(&g_cs);
		Sleep(rand() % 300);
		ReleaseSemaphore(g_hFull, 1, NULL);
	}
	return 0;
}
int main()
{
	printf(" 多线程十大经典案例 双线程读写队列数据\n");
	
	InitializeCriticalSection(&g_cs);
	g_hEmpty = CreateSemaphore(NULL, QUEUE_LEN, QUEUE_LEN, NULL);
	g_hFull = CreateSemaphore(NULL, 0, QUEUE_LEN, NULL);
	
	srand(time(NULL));
	g_i = g_j = 0;
	HANDLE hThread[2];
	hThread[0] = (HANDLE)_beginthreadex(NULL, 0, ReaderThreadFun, NULL, 0, NULL);
	hThread[1] = (HANDLE)_beginthreadex(NULL, 0, WriterThreadFun, NULL, 0, NULL);
	
	WaitForMultipleObjects(2, hThread, TRUE, INFINITE);
	
	for (int i = 0; i < 2; i++)
		CloseHandle(hThread[i]);
	CloseHandle(g_hEmpty);
	CloseHandle(g_hFull);
	DeleteCriticalSection(&g_cs);
	return 0;
}

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/a731062834/article/details/82986358