副标题#e#
线程同步是多线程措施设计的焦点内容,它的目标是正确处理惩罚多线程并发时的各类问题,譬喻线程的期待、多个线程会见同一数据时的互斥,防死锁等。Win32提供多种内核工具和手段用于线程同步,如互斥量、信号量、事件、临界区等。所差异的是,互斥量、信号量、事件都是Windows的内核工具,当措施对这些工具举办节制时会自动转换到焦点态,而临界区自己不是内核工具,它是事情在用户态的。我们知道从用户态转换到焦点态是需要以时间为价钱的,所以假如能在用户态就简朴办理的问题,就可以不必劳烦焦点态了。
这里我要说的是两种用于C++的多线程同步类,通过对这两种类的利用就可以利便的实现对变量或代码段的加锁节制,从而防备多线程对变量不正确的操纵。
所谓加锁,就是说当我们要会见某要害变量之前,都需要首先得到答允才气继承,假如未得到答允则只有期待。一个要害变量拥有一把锁,一个线程必需先获得这把锁(其实称为钥匙大概更形象)才可以会见这个变量,而当某个变量持有这把锁的时候,其他线程就不能反复的获得它,只有等持有锁的线程把锁偿还今后其他线程才有大概获得它。之所以这样做,就是为了防备一个线程读取某工具途中另一线程对它举办了修改,或两线程同时对一变量举办修改,譬喻:
// 全局:
struct MyStruct ... { int a, b; } ;
MyStruct s;
// 线程1:
int a = s.a;
int b = s.b;
// 线程2:
s.a ++ ;
s.b -- ;
假如实际的执行顺序就是上述书写的顺序那到没有什么,但假如线程2的执行打断了线程1,变为如下顺序:
int a = s.a; //线程1
s.a++; //线程2
s.b++; //线程2
int b = s.b; //线程1
那么这时线程1读出来的a和b就会有问题了,因为a是在修改前读的,而b是在修改后读的,这样读出来的是不完整的数据,会对措施带来不行预料的效果。天知道两个程的调治顺序是什么样的。为了防备这种环境的呈现,需要对变量s加锁,也就是当线程1获得锁今后就可以安心的会见s,这时假如线程2要修改s,只有等线程1会见完成今后将锁释放才可以,从而担保了上述两线程交错会见变量的环境不会呈现。
#p#副标题#e#
利用Win32提供的临界区可以利便的实现这种锁:
// 全局:
CRITICAL_SECTION cs;
InitializeCriticalSection( & cs);
// 线程1:
EnterCriticalSection( & cs);
int a = s.a;
int b = s.b;
LeaveCriticalSection( & cs);
// 线程2:
EnterCriticalSection( & cs);
s.a ++ ;
s.b -- ;
LeaveCriticalSection( & cs);
// 最后:
DeleteCriticalSection( & cs);
代码中的临界区变量(cs)就可以看作是变量s的锁,当函数EnterCriticalSection返回时,当前线程就得到了这把锁,之后就是对变量的会见了。会见完成后,挪用LeaveCriticalSection暗示释放这把锁,答允其他线程继承利用它。
假如每当需要对一个变量举办加锁时都需要做这些操纵,显得有些贫苦,并且变量cs与s只有逻辑上的锁干系,在语法上没有什么接洽,这对付锁的打点带来了不小的贫苦。措施员老是最懒的,可以想出各类偷懒的步伐来办理问题,譬喻让被锁的变量与加锁的变量形成物理上的接洽,使得锁变量成为被锁变量不行支解的一部门,这听起来是个好主意。
首先想到的是把锁关闭在一个类里,让类的结构函数和析构函数来打点对锁的初始化和锁毁行动,我们称这个锁为“实例锁”:
class InstanceLockBase
... {
CRITICAL_SECTION cs;
protected :
InstanceLockBase() ... { InitialCriticalSection( & cs); }
~ InstanceLockBase() ... { DeleteCriticalSection( & cs); }
} ;
假如熟悉C++,看到这里必然知道后头我要干什么了,对了,就是担任,因为我把结构函数和析构函数都声明为掩护的(protected),这样独一的浸染就是在子类里利用它。让我们的被掩护数据从这个类担任,那么它们不就不行支解了吗:
struct MyStruct: public InstanceLockBase
... { … } ;
什么?布局体还能从类担任?虽然,C++中布局体和类除了成员的默认会见节制差异外没有什么纷歧样,class能做的struct也能做。另外,也许你还会问,假如被锁的是个简朴范例,不能担任怎么办,那么要么用一个类对这个简朴范例举办封装(记得Java里有int和Integer吗),要么只能手工打点它们的接洽了。假如被锁类已经有了基类呢?不要紧,C++是答允多担任的,多一个基类也没什么。
#p#分页标题#e#
此刻我们的数据内里已经包括一把锁了,之后就是要添加加锁息争锁的行动,把它们作为InstanceLockBase类的成员函数再符合不外了:
class InstanceLockBase
... {
CRITICAL_SECTION cs;
void Lock() ... { EnterCriticalSection( & cs); }
void Unlock() ... { LeaveCriticalSection( & cs); }
…
} ;
看到这里大概会发明,我把Lock和Unlock函数都声明为私有了,那么如何会见这两个函数呢?是的,我们老是需要有一个处所来挪用这两个函数以实现加锁息争锁的,并且它们总应该成对呈现,但C++语法自己没能限制我们必需成对的挪用两个函数,假如加完锁忘相识,那效果是严重的。这里有一个破例,就是C++对付结构函数和析构函数的挪用是自动成对的,对了,那就把对Lock和Unlock的挪用专门写在一个类的结构函数和析构函数中:
class InstanceLock
... {
InstanceLockBase * _pObj;
public :
InstanceLock(InstanceLockBase * pObj)
... {
_pObj = pObj; // 这里会生存一份指向s的指针,用于解锁
if (NULL != _pObj)
_pObj -> Lock(); // 这里加锁
}
~ InstanceLock()
... {
if (NULL != _pObj)
_pObj -> Unlock(); // 这里解锁
}
} ;
最后别忘了在类InstanceLockBase中把InstanceLock声明为友元,使得它能正确会见Lock和Unlock这两个私有函数:
class InstanceLockBase
... {
friend class InstanceLock;
…
} ;
好了,有了上面的基本,此刻对变量s的加解锁打点酿成了对InstanceLock的实例的生命周期的打点了。如果我们有一个函数ModifyS中要对s举办修改,那么只要在函数一开始就声明一个InstaceLock的实例,这样整个函数就自动对s加锁,一旦进入这个函数,其他线程就都不能得到s的锁了:
void ModifyS()
... {
InstanceLock lock ( & s); // 这里已经实现加锁了
// some operations on s
} // 一旦分开lock工具的浸染域,自动解锁
假如是要对某函数中一部门代码加锁,只要用一对大括号把它们括起来再声明一个lock就可以了:
…
... {
InstanceLock lock ( & s);
// do something …
}
…
好了,就是这么简朴。下面来看一个测试。
首先筹备一个输出函数,对我们领略措施有辅佐。它会在输出我们想输出的内容同时打出行号和时间:
void Say( char * text)
... {
static int count = 0 ;
SYSTEMTIME st;
::GetLocalTime( & st);
printf( " %03d [%02d:%02d:%02d.%03d]%s " , ++ count, st.wHour, st.wMinute, st.wSecond, st.wMilliseconds, text);
}
虽然,原则上当多线程都挪用这个函数时应该对其静态局部变量count举办加锁,这里就省略了。
我们声明一个很是简朴的被锁的范例,并生成一个实例:
class MyClass: public InstanceLockBase
... {} ;
MyClass mc;
子线程的任务就是对这个工具加锁,然后输出一些信息:
DWORD CALLBACK ThreadProc(LPVOID param)
... {
InstanceLock il( & mc);
Say( " in sub thread, lock " );
Sleep( 2000 );
Say( " in sub thread, unlock " );
return 0 ;
}
这里会输出两条信息,一是在方才得到锁的时间,二是在释放锁的时候,中间通过Sleep来延迟2秒。
主线程认真开启子线程,然后也对mc加锁:
CreateThread( 0 , 0 , ThreadProc, 0 , 0 , 0 );
... {
InstanceLock il( & mc);
Say( " in main thread, lock " );
Sleep( 3000 );
Say( " in main thread, lock " );
}
运行此措施,获得的输出如下:
001 [13:43:23.781]in main thread, lock
002 [13:43:26.781]in main thread, lock
003 [13:43:26.781]in sub thread, lock
004 [13:43:28.781]in sub thread, unlock
从其输出的行号和时间可以清楚的看到两个线程间的互斥:当主线程刚好首先得到锁时,它会延迟3秒,然后释放锁,之后子线程才得以继承举办。这个例子也证明我们的类事情的很好。
总结一下,要利用InstanceLock系列类,要做的就是:
1、让被锁类从InstanceLockBase担任
2、所有要会见被锁工具的代码前面声明InstanceLock的实例,并传入被锁工具的指针。
附:完整源代码:
#pragma once
#include < windows.h >
class InstanceLock;
class InstanceLockBase
... {
friend class InstanceLock;
CRITICAL_SECTION cs;
void Lock()
... {
::EnterCriticalSection( & cs);
}
void Unlock()
... {
::LeaveCriticalSection( & cs);
}
protected :
InstanceLockBase()
... {
::InitializeCriticalSection( & cs);
}
~ InstanceLockBase()
... {
::DeleteCriticalSection( & cs);
}
} ;
class InstanceLock
... {
InstanceLockBase * _pObj;
public :
InstanceLock(InstanceLockBase * pObj)
... {
_pObj = pObj;
if (NULL != _pObj)
_pObj -> Lock();
}
~ InstanceLock()
... {
if (NULL != _pObj)
_pObj -> Unlock();
}
} ;