8.6 条件变量(Condition Variables)——可利用临界区或SRWLock锁来实现

8.6.1 条件变量的使用

(1)条件变量机制就是为了简化 “生产者-消费者”问题而设计的一种线程同步机制。其目的让线程以原子方式释放锁并将自己阻塞,直到某一个条件成立为止。如读者线程当没有数据可读取时,则应释放锁并等待,直到写者线程产生了新的数据。同理,当写者把数据结构写满时,那么写者应该释放SRWLock并等待,直到读者把数据结构清空。

(2)等待函数:SleepConditionVariableCS/SleepConditionVariableSRW。其功能类似于事件对象:WaitForSingleObject(hEvent,INFINITE);

参数

描述

pConditionVariable

己初始化的条件变量,调用线程正在等待该条件变量

pCriticalSection/pSRWLock

指向临界区或SRWLock的指针,用来同步对共享资源的访问

dwMilliSeconds

等待条件变量被触发(不是锁)的时间

0:只是用来测试,并立即返回。

INFINITE:无限等待条件变量(不是锁),直到它被触发

其它值:如果超时时,则不再等待,直接返回。返回值为FALSE

Flags

使用SRWLock才有这个参数,表示一旦条件变量被触发,将于何种方式得到锁,如共享或排它。对于写者线程传入0,即获得一个排它锁。对于读者线程传入CONDITION_VARIABLE_LOCKMODE_SHARDED,表示共享锁。

(3)唤醒线程:WakeConditionVariable或WakeAllConditionVariable(阻塞在Sleep*函数中的线程),其参数只有一个,就是条件变量。其功能类似于SetEvent将事件设为有信号状态。

  ①WakeConditionVariable唤醒一个在Sleep*函数中的线程以便让其获得锁。如消费者消费完数据,可以唤醒一个生产者来生产数据。因为生产者获得的是排他锁,所以一次只需唤醒一个

  ②当调用WakeAllConditionVariable时,会唤醒Sleep*函数中的多个线程。如当生产者产生好数据后,可以唤醒所有的消费者,因为这些消费者可共享锁,所以可以同时唤醒。

  ③注意Wake*函数唤醒的是那些被Sleep*函数阻塞的线程,而当调用RleaseSRWLock*后,唤醒的是那些阻塞在AcquireSRWLock*函数中的线程,这两者是不同的。

(4)条件变量使用的一般模式

//消费者线程函数

  DWORD WINAPI Consumer(PVOID pvParam)
{
AcquireSRWLockShared(&g_srwLock); //请求共享锁(读锁)
……//消费之前的一些操作。然后等待条件变量,如果不满足,会释放锁,并开始等待。
SleepConditionVariableSRW(&g_cvConsume,&g_srwLock,INFINITE, //等待消费者条件变量
CONDITION_VARIABLE_LOCKMODE_SHARED); //等待条件变量,会被生产者唤醒
//当条件满足时会重新获得锁(共享)
…… //消费 ReleaseSRWLockShared(&g_srwLock); //释放锁
WakeConditionVariable(&g_cvProduce);//唤醒一个生产者线程,触发生产者条件变量。因为生产者使用排他锁,所以每次只需唤醒一个线程。
}

//生产者线程函数

DWORD WINAPI Producer(PVOID pvParam)
{
AcquireSRWLockExclusive(&g_srwLock); //请求排他锁(读锁) …… //生产数据之前的一些操作,然后等待条件变量,如果不满足,则释放锁,并开始等待。 //等待条件变量,会被消费者线程唤醒
SleepConditionVariableSRW(&g_cvProduce,&g_srwLock,INFINITE,); //等待重新获得排他锁 …… //生产 ReleaseSRWLockExclusive(&g_srwLock); //释放排他锁
WakeAllConditionVariable(&g_cvProduce);//触发消费者条件变量,唤醒所有Sleep*函数中的消费者,因为消费者会通过共享锁共享数据,所以可同时唤醒。
}

【Queue程序】使用SRWLock和条件变量演示生产者和消费者问题

效果图

★★程序的几个说明★★

  ①初始化时,会创建4个客户线程(写入者)和两个服务线程(读者),每个客户线程会在队列末尾添加一个请求元素,并更新客户列表信息。因为写入线程多,读者线程少,所以队列很快就会被塞满,这里会在客户列表中会有操作失败的提示。

  ②客户列表框每项显示的内容为客户线程(是个索引号,4个线程的索引值)添加的请求号(注意请求号会随着每次线程添加的请求而递增)。

  ③服务线程负责对请求处理,线程0处理奇数号请求,线程1处理偶数号请求。在元素被添加到队列之前,两个线程会处理请求状态,当条件变量满足时会被唤醒起来处理客户的请求号,并标记为己读。同时当队列己满时,客户线程也会进行睡眠,当条件变量满足时,被服务线程唤醒起来工作,断续向队列中写入数据。

  ④当按下“停止”按钮时,会结束所有的客户线程和服务线程。

//队列的实现——队列头文件

#pragma once
#include <windows.h> //////////////////////////////////////////////////////////////////////////
//此队列并不具有线程安全性,由客户线程和服务线程自行访问的同步问题
class CQueue{
public:
//队列中元素的类型
//当某个客户线程请求每一次会将其线程ID和请求号记录在队列中
//本例假设中,请求号是这样给的:每个线程每次发送请求时,请求号是
//递增的。如线程1,第1次会发送1号请求,第2次发送2号请求,依此类推
struct ELEMENT{
int m_nThreadNum; //客户线程号
int m_nRequestNum;//请求号
//其他元素放这以后
};
typedef ELEMENT* PELEMENT; private:
struct INNER_ELEMENT{
int m_nStamp; //0表示该元素是空元素。
//记录该元素加入队列时的顺序(即属于第几个加入队列中的元素了)
ELEMENT m_element;
};
typedef INNER_ELEMENT* PINNER_ELEMENT; private:
PINNER_ELEMENT m_pElements; //队列元素数组,数组长度设计为固定,这就是需要保护的数据
int m_nMaxElements; //数组长度。CQueue构造时数组初始长度
int m_nCurrentStamp; //跟踪当前插入的元素,添加元素时递增,且只增不减 private:
int GetFreeSlot(); //返回队列中第1个m_nStamp为0的元素
int GetNextSlot(int nThreadNum); //返回需要服务线程nThreadNum处理的最早的请求
//即属于服务线程要处理的请求中m_nStamp值最小的那项
public:
CQueue(int nMaxElements);
~CQueue(); BOOL IsFull(); //队列是否己满
BOOL IsEmpty(int nThreadNum); //队列是否存在指定线程nTreadNum要处理的请求
void AddElement(ELEMENT e);
BOOL GetNewElement(int nThreadNum, ELEMENT &e); //获取队列中需要线程nThreadNum处理的
//最早插入的元素
};

//队列的实现——队列实现的文件Queue.cpp

#include "Queue.h"

// 构造函数
CQueue::CQueue(int nMaxElements)
{
//分配固定大小的(nMaxElements个元素)队列,并初始化各元素
m_pElements = (PINNER_ELEMENT)HeapAlloc(GetProcessHeap(), ,
sizeof(INNER_ELEMENT)* nMaxElements);
ZeroMemory(m_pElements, sizeof(INNER_ELEMENT)*nMaxElements); //各元素初始化为0 //初始化当前元素个数的计数值
m_nCurrentStamp = ; //保存队列元素的最大个数
m_nMaxElements = nMaxElements;
} //析构函数
CQueue::~CQueue()
{
HeapFree(GetProcessHeap(), , m_pElements);
} //判断队列是否己满
BOOL CQueue::IsFull()
{
return (- == GetFreeSlot());
} //队列是否存在指定线程nTreadNum要处理的请求
BOOL CQueue::IsEmpty(int nThreadNum)
{
return (GetNextSlot(nThreadNum) == -);
} //获得队列中第1个空的元素(即m_nStamp=0,说明该元素为空或该元素己被读取过了
//注意,己经读取过的也直接废弃,可以重新被新的请求覆盖使用)
int CQueue::GetFreeSlot()
{
//查找第1个m_nStamp的元素
for (int idx = ; idx < m_nMaxElements; idx++)
{
if (m_pElements[idx].m_nStamp == )
return idx;
} //如果找不到元素(即队列己满),返回-1
return (-);
} //返回需要服务线程nThreadNum处理的最早的请求
//即属于服务线程要处理的请求中m_nStamp最小的那项
int CQueue::GetNextSlot(int nThreadNum)
{
//默认没有需要服务线程nThreadNum处理的元素
int firstSlot = -; //m_nCurrentStamp为最后添加元素的stamp,为最大的stamp
//因要在队列中查找属于线程nThreadNum处理的,且其m_nStamp值最小
//因此根据查找最小值的方法,
int minStamp = m_nCurrentStamp + ; //线程nThreadNum为奇数时处理客户请求号为奇数的元素,为偶数时,处理偶数号请求
for (int idx = ; idx < m_nMaxElements; idx++){
if ((m_pElements[idx].m_nStamp != ) && //确保元素非空
((m_pElements[idx].m_element.m_nRequestNum % ) == nThreadNum) && //需要该线程处理的请求
(m_pElements[idx].m_nStamp < minStamp)){ //查找m_nStamp的最小值
minStamp = m_pElements[idx].m_nStamp;
firstSlot = idx;
}
} return (firstSlot);
} //获取队列中需要线程nThreadNum处理的最早插入的元素
BOOL CQueue::GetNewElement(int nThreadNum, ELEMENT &e)
{
int nNewSlot = GetNextSlot(nThreadNum);
if (- == nNewSlot)
return FALSE; //拷贝元素的内容
e = m_pElements[nNewSlot].m_element; //将该元素标访为己读
m_pElements[nNewSlot].m_nStamp = ; return TRUE;
} //增加请求(元素)
void CQueue::AddElement(ELEMENT e)
{
//如果队列己满时,不能再增加,直接返回
int nFreeSlot = GetFreeSlot();
if (nFreeSlot == -)
return; //拷贝内容
m_pElements[nFreeSlot].m_element = e; //记录一下该元素加入队列的顺序(即,属于第几个加入进来的元素了)
m_pElements[nFreeSlot].m_nStamp = ++m_nCurrentStamp;
}

//主程序main.cpp

#include "../../CommonFiles/CmnHdr.h"
#include <tchar.h>
#include <strsafe.h>
#include "resource.h"
#include "Queue.h" //////////////////////////////////////////////////////////////////////////
CQueue g_q(); //共享队列,共有10个元素
volatile LONG g_bShutDown; //是否结束所有读、写者线程
HWND g_hWnd; //供客户/服务线程使用
SRWLOCK g_srwLock; //读\写锁,用于保护共享队列
CONDITION_VARIABLE g_cvReadyToConsume; //写者发出信号,表示可以读取
CONDITION_VARIABLE g_cvReadyToProduce; //读者发出信号,表示可以写入 //所有读、写者线程的句柄(本例共6个线程:4个写者、2个写者)
HANDLE g_hThreads[MAXIMUM_WAIT_OBJECTS]; //为了可扩展性,最多可以创建64个线程 //读、写者线程的总数量
int g_nNumThreads = ; //每创建一个线程,该值加1 //////////////////////////////////////////////////////////////////////////
void AddText(HWND hWndLB, PCTSTR pszFormat, ...)
{
va_list argList;
va_start(argList, pszFormat); TCHAR sz[ * ];
_vstprintf_s(sz, _countof(sz), pszFormat, argList);
ListBox_SetCurSel(hWndLB, ListBox_AddString(hWndLB, sz));
va_end(argList);
} //////////////////////////////////////////////////////////////////////////
//读取队列中需要服务线程[nThreadNum]处理的最早的元素(每次读完后,最早的这个元素会被废弃),并显示在列表框中
//该函数的第2个参数在本例中没用,被我注释掉了
BOOL ConsumeElement(int nThreadNum, /*int nRequestNum,*/ HWND hWndLB)
{
// 以共享模式获得SRWLock,如果锁已经被写者线程以独占模式占用,函数会阻塞
// 如果锁已经被另一个读者线程以共享模式获得,运行对请求进行处理
AcquireSRWLockShared(&g_srwLock); // 如果队列不存在该读者线程(nThreadNum)处理的元素,则阻塞,等待一个写者产生新的元素
// 直到写者线程触发g_cvReadyToConsume条件变量为止
while (g_q.IsEmpty(nThreadNum) && !g_bShutDown){
//没有可读的元素
AddText(hWndLB, TEXT("服务线程[%d]没有可处理的数据!"), nThreadNum); //等待,直到写者线程将其唤醒(注意,写者线程只要写入数据,就会唤醒
//这些等待读取数据的线程,让他们自行判断这些数据是不是属于自己的那部分。
SleepConditionVariableSRW(&g_cvReadyToConsume, &g_srwLock, INFINITE,
CONDITION_VARIABLE_LOCKMODE_SHARED);
} //如果用户按了“结束”按钮
if (g_bShutDown){
//显示当前服务线程退出的信息
AddText(hWndLB, TEXT("服务线程[%d]退出!"), nThreadNum); //唤醒另一个可能被阻塞的写者线程(因为共享锁,读者线程不会阻塞)
ReleaseSRWLockShared(&g_srwLock); //通知那些正在等待数据的其他读线程结束(即有部分读线程正在
//阻塞在上面的Sleep*函数中等待数据的到达
WakeConditionVariable(&g_cvReadyToConsume);//用来唤醒其他读线程!
return FALSE;
} CQueue::ELEMENT e; //获取队列中需要线程[nThreadNum]处理的最早插入的元素
g_q.GetNewElement(nThreadNum, e); //释放共享锁
ReleaseSRWLockShared(&g_srwLock); AddText(hWndLB, TEXT("服务线程[%d]处理了客户线程[%d]的元素%d"),
nThreadNum,e.m_nThreadNum,e.m_nRequestNum); //读完一个元素后队列中会产生一个空项,唤醒写者线程
WakeConditionVariable(&g_cvReadyToProduce);
return TRUE;
}
//读者线程函数(服务线程)
DWORD WINAPI ReadThread(PVOID pParam)
{
int nThreadNum = PtrToUlong(pParam);
HWND hWndLB = GetDlgItem(g_hWnd, IDC_SERVERS); //课本中nRequestNum这个变量不需要用到,该句改为如下的while语句
//for (int nRequestNum = 1; !g_bShutDown;nRequestNum++){
while (!g_bShutDown){
if (!ConsumeElement(nThreadNum, /*nRequestNum,*/ hWndLB))
return ;
Sleep(); //读取下一个元素时先休眠2.5秒
} //结束线程(用户按下了“结束”按钮)
AddText(hWndLB, TEXT("服务线程[%d]退出!"), nThreadNum);
return ;
} //////////////////////////////////////////////////////////////////////////
//写者线程函数(客户线程)
DWORD WINAPI WriteThread(PVOID pParam)
{
int nThreadNum = PtrToUlong(pParam); //线程的索引号
HWND hWndLB = GetDlgItem(g_hWnd, IDC_CLIENTS); for (int nRequestNum = ; !g_bShutDown;nRequestNum++){ //模拟写入数据,每次将自己的线程号及请求号保存到队列中去
CQueue::ELEMENT e = { nThreadNum, nRequestNum }; //以独占的方式获得SRWLock锁,如果调用线程获得锁后,其他任何申请锁的线程会被阻塞在
//AcquireSRWLock*函数中,直到锁被释放。当调用线程申请排他锁时,若锁已被其他线程获得
//则他们都释放锁以后才可申请到。
AcquireSRWLockExclusive(&g_srwLock); //获得排它锁后,判断队列是否己满,如果己满,则挂起自己,直到条件变量满足
// 注意在写入的过程中,可以会收到“结束”线程的命令
if (g_q.IsFull() & !g_bShutDown){
//队列己满
AddText(hWndLB, TEXT("线程[%d]检测到队列己满:不能增加元素%d"),
nThreadNum,nRequestNum); //睡眠,等待读取者线程读取一个元素,以便腾出空间来容纳新元素
//这里调用线程会交给锁,并挂起自己,直到读者线程读出元素,最后一个参数
//为0,表示当被唤醒时,以排他方式再次获得共享锁
SleepConditionVariableSRW(&g_cvReadyToProduce, &g_srwLock, INFINITE, );
} //如果用户按了结束按钮,则结束该线程
if (g_bShutDown){
//显示客户线程结束提示
AddText(hWndLB, TEXT("客户线程[%d]退出"), nThreadNum); //释放锁,唤醒那些阻塞在Acuqire*函数中的线程
ReleaseSRWLockExclusive(&g_srwLock); //唤醒那些被阻塞在Sleep*函数中的线程(这些线程已经获得锁了,当释放排他锁后被系统唤醒
//而不是被Wake*函数唤醒),以便让其自然结束。
WakeAllConditionVariable(&g_cvReadyToConsume); return ; //结束自己
} else{
//将新元素加入到队列中
g_q.AddElement(e); //显示新加入的元素
AddText(hWndLB, TEXT("客户线程[%d]添加元素%d"), nThreadNum, nRequestNum); //释放锁
ReleaseSRWLockExclusive(&g_srwLock); //唤醒所有被阻塞的读者线程,让其自然结束
WakeAllConditionVariable(&g_cvReadyToConsume); //在新增下一个元素前,休眠1.5秒
Sleep();
}
} //当前线程退出
AddText(hWndLB, TEXT("客户线程[%d]退出"), nThreadNum); return ;
}
//////////////////////////////////////////////////////////////////////////
//
void StopProcessing()
{
if (!g_bShutDown){ //请求所有的线程结束
InterlockedExchange(&g_bShutDown, TRUE); //通知所有正在等待条件变量的线程结束
WakeAllConditionVariable(&g_cvReadyToConsume);
WakeAllConditionVariable(&g_cvReadyToProduce); //等待所有的线程
WaitForMultipleObjects(g_nNumThreads, g_hThreads, TRUE, INFINITE); //清除线程内核对象
while (g_nNumThreads--)
CloseHandle(g_hThreads[g_nNumThreads]); //关闭列表框
AddText(GetDlgItem(g_hWnd, IDC_SERVERS), TEXT("-----------------------------------"));
AddText(GetDlgItem(g_hWnd, IDC_CLIENTS), TEXT("-----------------------------------"));
}
} DWORD WINAPI StoppingThread(PVOID pParam)
{
StopProcessing();
return ;
} //////////////////////////////////////////////////////////////////////////
BOOL Dlg_OnInitDialog(HWND hwnd, HWND hwndFocus, LPARAM lParam)
{
chSETDLGICONS(hwnd, IDI_QUEUE); g_hWnd = hwnd; //用于让读、写者线程显示状态 //初始化SRWLock锁
InitializeSRWLock(&g_srwLock); //初始化条件变量
InitializeConditionVariable(&g_cvReadyToConsume);
InitializeConditionVariable(&g_cvReadyToProduce); //将被设为TRUE来结束读、写者线程
g_bShutDown = FALSE; //创建4个写者线程(客户线程)
DWORD dwThreadID;
for (int x = ; x < ;x++){
g_hThreads[g_nNumThreads++] =
chBEGINTHREADEX(NULL, ,
WriteThread,
(PVOID)(INT_PTR)x,//将x作为线程索引号
, //立即运行
&dwThreadID);
} //创建2个读者线程(服务线程)
for (int x = ; x < ; x++){
g_hThreads[g_nNumThreads++] =
chBEGINTHREADEX(NULL, ,
ReadThread,
(PVOID)(INT_PTR)x,//将x作为线程索引号
, //立即运行
&dwThreadID);
} return TRUE;
}
//////////////////////////////////////////////////////////////////////////
void Dlg_OnCommand(HWND hWnd, int id, HWND hWndCtrl, UINT codeNotify)
{
switch (id)
{
case IDCANCEL:
EndDialog(hWnd, id);
break; case IDC_BTN_STOP:
//StopProcessing不能在UI线程中调用,因为这个函数
//里会的WaitForMultipleObjects会挂起UI线程,使得当子线程SendMessage
//来清空列表框时会发生死锁,这里另开一个线程
DWORD dwThreadID;
CloseHandle(chBEGINTHREADEX(NULL, , StoppingThread, NULL, , &dwThreadID)); //禁用“结束”按钮,防止被多次按下
Button_Enable(hWndCtrl, FALSE);
break;
}
}
//////////////////////////////////////////////////////////////////////////
INT_PTR WINAPI DlgProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch (uMsg)
{
chHANDLE_DLGMSG(hWnd, WM_INITDIALOG, Dlg_OnInitDialog);
chHANDLE_DLGMSG(hWnd, WM_COMMAND, Dlg_OnCommand);
} return FALSE;
}
//////////////////////////////////////////////////////////////////////////
int WINAPI _tWinMain(HINSTANCE hInstExe, HINSTANCE, PTSTR, int)
{
DialogBox(hInstExe, MAKEINTRESOURCE(IDD_QUEUE), NULL, DlgProc);
StopProcessing();
return ;
}

//resource.h

//{{NO_DEPENDENCIES}}
// Microsoft Visual C++ 生成的包含文件。
// 供 8_Queue.rc 使用
//
#define IDD_QUEUE 101
#define IDI_QUEUE 102
#define IDC_SERVERS 1001
#define IDC_CLIENTS 1002
#define IDC_BTN_STOP 1003 // Next default values for new objects
//
#ifdef APSTUDIO_INVOKED
#ifndef APSTUDIO_READONLY_SYMBOLS
#define _APS_NEXT_RESOURCE_VALUE 103
#define _APS_NEXT_COMMAND_VALUE 40001
#define _APS_NEXT_CONTROL_VALUE 1004
#define _APS_NEXT_SYMED_VALUE 101
#endif
#endif

//资源文件Queue.rc

// Microsoft Visual C++ generated resource script.
//
#include "resource.h" #define APSTUDIO_READONLY_SYMBOLS
/////////////////////////////////////////////////////////////////////////////
//
// Generated from the TEXTINCLUDE 2 resource.
//
#include "winres.h" /////////////////////////////////////////////////////////////////////////////
#undef APSTUDIO_READONLY_SYMBOLS /////////////////////////////////////////////////////////////////////////////
// 中文(简体,中国) resources #if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_CHS)
LANGUAGE LANG_CHINESE, SUBLANG_CHINESE_SIMPLIFIED #ifdef APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// TEXTINCLUDE
// TEXTINCLUDE
BEGIN
"resource.h\0"
END TEXTINCLUDE
BEGIN
"#include ""winres.h""\r\n"
"\0"
END TEXTINCLUDE
BEGIN
"\r\n"
"\0"
END #endif // APSTUDIO_INVOKED /////////////////////////////////////////////////////////////////////////////
//
// Dialog
// IDD_QUEUE DIALOGEX , , ,
STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU
CAPTION "队列"
FONT , "MS Shell Dlg", , , 0x1
BEGIN
DEFPUSHBUTTON "停止",IDC_BTN_STOP,,,,
GROUPBOX "客户线程",IDC_STATIC,,,,
GROUPBOX "服务线程",IDC_STATIC,,,,
LISTBOX IDC_SERVERS,,,,,NOT LBS_NOTIFY | WS_VSCROLL | WS_TABSTOP
LISTBOX IDC_CLIENTS, , , , , NOT LBS_NOTIFY | WS_VSCROLL | WS_TABSTOP
END /////////////////////////////////////////////////////////////////////////////
//
// DESIGNINFO
// #ifdef APSTUDIO_INVOKED
GUIDELINES DESIGNINFO
BEGIN
IDD_QUEUE, DIALOG
BEGIN
LEFTMARGIN,
RIGHTMARGIN,
TOPMARGIN,
BOTTOMMARGIN,
END
END
#endif // APSTUDIO_INVOKED /////////////////////////////////////////////////////////////////////////////
//
// Icon
// // Icon with lowest ID value placed first to ensure application icon
// remains consistent on all systems.
IDI_QUEUE ICON "Queue.ico"
#endif // 中文(简体,中国) resources
///////////////////////////////////////////////////////////////////////////// #ifndef APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// Generated from the TEXTINCLUDE 3 resource.
// /////////////////////////////////////////////////////////////////////////////
#endif // not APSTUDIO_INVOKED

8.6.2 一些有用的窍门和技巧

(1)以原子方式操作一组对象时使用一个锁

  ①每个“逻辑”资源只使用一个锁来更新,如向链表中增加一个元素时,还要更新其计数值。这时元素和计数器应视为同一个单独的“逻辑”资源。同时不论对这个逻辑资源进行读或写时,都应该只使用一个锁。

  ②每个逻辑资源都应有自己锁。但不必为所有逻辑资源都创建单独的锁。这是因为任一时刻系统只允许一个线程执行。

(2)同时访问多个逻辑资源——死锁的例子

线程1

线程2

DWORD WINAP Thread1Func(…)

{

EnterCriticalSection(&g_csResource1);

EnterCriticalSection(&g_csResource2);

…… //从资源1中读取元素

…… //将元素插入到资源2中

LeaveCriticalSection(&g_csResource2);

LeaveCriticalSection(&g_csResource1);

return 0

}

DWORD WINAP Thread2Func(…)

{

EnterCriticalSection(&g_csResource2);

EnterCriticalSection(&g_csResource1);

…… //从资源1中读取元素

…… //将元素插入到资源2中

LeaveCriticalSection(&g_csResource1);

LeaveCriticalSection(&g_csResource2);

return 0

}

死锁的可能原因:设线程1取得g_csResource1锁,然后执行执行线程2并且线程2取得g_csResource2锁。此时就会出现死锁,因为线程1在等资源2的锁,而线程2在等资源1的锁。

解决方案:线程1和线程2都应以相同的顺序来获得资源的锁。如将线程2的第1行和第2行调换一下。注意,调用LeaveCriticalSection的顺序是无关紧要的。因为这个函数不会让线程挂起。

(3)不要长时间占用锁

线程1(可能占用锁时间较长)

线程2(占用锁的时间较短)

MYSTRUCT g_mySt;

CRITICAL_SECTION  g_cs;

DWORD WINAP Thread1Func(…)

{

 EnterCriticalSection(&g_cs);

//发送消息,因为不知SendMessage要花费

   //多少时间,所以锁被占用的时间不能预估

SendMessage(hwnd,WM_MYSTRUC,&g_mySt,0);

   LeaveCriticalSection(&g_cs);

return 0

}

MYSTRUCT g_mySt;

CRITICAL_SECTION  g_cs;

DWORD WINAP Thread1Func(…)

{

 EnterCriticalSection(&g_cs);

//锁快照的时间很短(当然这个技巧是假设窗

//口过程只需读取快照就足够了

MYSTRUCT sTemp = g_mySt;

 LeaveCriticalSection(&g_cs);

//将耗时的发送消息移到Enter/Leave之外

SendMessage(hwnd,WM_MYSTRUC,& sTemp,0);

return 0

}

分析:线程1中因SendMessage被包围在Enter/Leave之间,导致锁的时间不确定,可能很长,也可能很短。而线程2将耗时的SendMessage移到Enter/Leave之外,只锁定快照,这个CPU时间是可以预估的,只需几个CPU周期便可以。

【ConditionSRWLock程序】条件变量的使用——读线程只能等到条件变量被设置时才能执行

#include <windows.h>
#include <tchar.h>
#include <time.h>
#include <locale.h> //////////////////////////////////////////////////////////////////////////
const int g_iThreadCnt = ;
int g_iGlobalVal = ; SRWLOCK g_srwLock = {};
CONDITION_VARIABLE g_cv = { }; DWORD WINAPI ReadThread(PVOID pvParam);
DWORD WINAPI WriteThread(PVOID pvParam); int _tmain()
{
_tsetlocale(LC_ALL, _T("chs"));
srand((unsigned int)time(NULL)); //初始化SRWLock锁和条件变量,不需要释放,系统会自行处理
InitializeSRWLock(&g_srwLock);
InitializeConditionVariable(&g_cv); HANDLE aThread[g_iThreadCnt];
DWORD dwThreadID = ;
SYSTEM_INFO si = { };
GetSystemInfo(&si); for (int i = ; i < g_iThreadCnt; i++)
{
if (== rand() % ){
aThread[i] = CreateThread(NULL, ,
(LPTHREAD_START_ROUTINE)WriteThread,
NULL,
CREATE_SUSPENDED,&dwThreadID);
} else{
aThread[i] = CreateThread(NULL, ,
(LPTHREAD_START_ROUTINE)ReadThread,
NULL,
CREATE_SUSPENDED, &dwThreadID);
} //设置线程的亲缘性
SetThreadAffinityMask(aThread[i], << (i%si.dwNumberOfProcessors));
ResumeThread(aThread[i]);
} //等待所有线程结束
WaitForMultipleObjects(g_iThreadCnt, aThread, TRUE, INFINITE); for (int i = ; i < g_iThreadCnt;i++){
CloseHandle(aThread[i]);
} _tsystem(_T("PAUSE")); return ;
} //读数据线程,以共享方式取得SRWLock锁
DWORD WINAPI ReadThread(PVOID pvParam)
{
__try
{
//先获取共享锁
AcquireSRWLockShared(&g_srwLock);
//条件变量不满足时交出共享锁,并挂起自己以等待条件变量
if (SleepConditionVariableSRW(&g_cv,&g_srwLock,
,CONDITION_VARIABLE_LOCKMODE_SHARED)){
_tprintf(_T("Timestamp[%u]:Thread[ID:0x%X]读取全局变量值为%d\n"),
GetTickCount(),GetCurrentThreadId(),g_iGlobalVal);
} else{
//超时的时候
_tprintf(_T("Timestamp[%u]:Thread[ID:0x%X]读取全局变量值(未改变)为%d\n"),
GetTickCount(), GetCurrentThreadId(), g_iGlobalVal);
}
}
__finally
{
ReleaseSRWLockShared(&g_srwLock); //释放锁,以便写线程有机会获得排它锁
}
return ;
} //写数据线程,以排他方式取得SRWLock锁
DWORD WINAPI WriteThread(PVOID pvParam)
{
__try
{
AcquireSRWLockExclusive(&g_srwLock); for (int i = ; i <= ;i++){
g_iGlobalVal = i;
SwitchToThread(); //为了演示,看下其他线程会不会读到“脏数据”
}
_tprintf(_T("Timestamp[%u]:Thread[ID:0x%X]写入数据值为%d\n"),
GetTickCount(), GetCurrentThreadId(), g_iGlobalVal);
}
__finally
{
ReleaseSRWLockExclusive(&g_srwLock); //释放锁,唤醒所有阻塞在Acuqire*函数中的线程
WakeAllConditionVariable(&g_cv); //唤醒所有阻塞在Sleep*中的读线程
}
return ;
}

第8章 用户模式下的线程同步(4)_条件变量(Condition Variable)的更多相关文章

  1. Windows核心编程:第8章 用户模式下的线程同步

    Github https://github.com/gongluck/Windows-Core-Program.git //第8章 用户模式下的线程同步.cpp: 定义应用程序的入口点. // #in ...

  2. windows核心编程---第七章 用户模式下的线程同步

    用户模式下的线程同步 系统中的线程必须访问系统资源,如堆.串口.文件.窗口以及其他资源.如果一个线程独占了对某个资源的访问,其他线程就无法完成工作.我们也必须限制线程在任何时刻都能访问任何资源.比如在 ...

  3. 第8章 用户模式下的线程同步(3)_Slim读写锁(SRWLock)

    8.5 Slim读/写锁(SRWLock)——轻量级的读写锁 (1)SRWLock锁的目的 ①允许读者线程同一时刻访问共享资源(因为不存在破坏数据的风险) ②写者线程应独占资源的访问权,任何其他线程( ...

  4. 第8章 用户模式下的线程同步(1)_Interlocked系列函数

    8.1 原子访问:Interlocked系列函数(Interlock英文为互锁的意思) (1)原子访问的原理 ①原子访问:指的是一线程在访问某个资源的同时,能够保证没有其他线程会在同一时刻访问该资源. ...

  5. 第8章 用户模式下的线程同步(2)_临界区(CRITICAL_SECTION)

    8.4 关键段(临界区)——内部也是使用Interlocked函数来实现的! 8.4.1 关键段的细节 (1)CRITICAL_SECTION的使用方法 ①CRITICAL_SECTION cs;   ...

  6. 《windows核心编程系列》七谈谈用户模式下的线程同步

    用户模式下的线程同步 系统中的线程必须访问系统资源,如堆.串口.文件.窗口以及其他资源.如果一个线程独占了对某个资源的访问,其他线程就无法完成工作.我们也必须限制线程在任何时刻都能访问任何资源.比如在 ...

  7. 【windows核心编程】 第八章 用户模式下的线程同步

    Windows核心编程 第八章 用户模式下的线程同步 1. 线程之间通信发生在以下两种情况: ①    需要让多个线程同时访问一个共享资源,同时不能破坏资源的完整性 ②    一个线程需要通知其他线程 ...

  8. 用户模式下的线程同步的分析(Windows核心编程)

    线程同步 同一进程或者同一线程可以生成许多不同的子线程来完成规定的任务,但是多个线程同时运行的情况下可能需要对某个资源进行读写访问,比如以下这个情况:创建两个线程对同一资源进行访问,最后打印出这个资源 ...

  9. 《Windows核心编程》第八章——用户模式下的线程同步

    下面起了两个线程,每个对一个全局变量加500次,不假思索进行回答,会认为最后这个全局变量的值会是1000,然而事实并不是这样: #include<iostream> #include &l ...

随机推荐

  1. go语言循环语句 for

    Go语言中的循环语句只支持for关键字,而不支持while和do-while结构. sum := 0 for i := 0; i < 10; i++ { sum += i } 无限循环的写法: ...

  2. 关于html页面head标签顺序

    基本上head就这几个标签么: <meta>.<link>.<title>.<script>.<style>.<base>. 它 ...

  3. JavaScript学习笔记2之Tab切换

    1.Tab切换简写版1 页面布局如下: <div id="tab"> <h1 id="title"> <span class=&q ...

  4. IOS中限制TextField中输入的类型以及长度

    -(BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementSt ...

  5. DropDownList 添加一个"请选择"或"全部"之类的项

    DropDownList在从数据库中得到数据源绑定后,添加一个"请选择"或"全部"之类的项 1:直接添加:<asp:ListItem Value=&quo ...

  6. <转>iOS性能优化:Instruments使用实战

    最近采用Instruments 来分析整个应用程序的性能.发现很多有意思的点,以及性能优化和一些分析性能消耗的技巧,小结如下. Instruments使用技巧 关于Instruments官方有一个很有 ...

  7. Android studio 如何查看模拟器里面的文件

    1.查看SD卡里面的内容 2.看数据库

  8. Swift - 访问通讯录-使用AddressBook.framework和AddressBookUI.framework框架实现

    1,通讯录访问介绍 通讯录(或叫地址簿,电话簿)是一个数据库,里面储存了联系人的相关信息.要实现访问通讯录有如下两种方式: (1)AddressBook.framework框架 : 没有界面,通过代码 ...

  9. Xcode cannot launch because the device is locked.

    When you plug in your iPhone, it will ask you to trust the computer. If you already trust and unlock ...

  10. BIEE 目录迁移(文件夹)方式

    文件夹迁移方式一(归档--取消归档):     归档:analytics中选择目录,定位至指定文件夹,更多中选择归档,保存为 .catalog文档:     释放归档: 进入目录管理器,离线方式登陆, ...