1. 在以下两种情况下,线程之间要相互通信.
■ 需要让多个线程同时访问一个共享资源,同时不能破坏资源的完整性.
■ 一个线程需要通知其他线程某项任务已经完成.
2. 原子访问:一个线程在访问某个资源的同时能保证没有其他线程会在同一时刻访问同一资源.
Windows提供了InterLocked系列函数来保证对一个值的递增操作时原子操作.
函数:
LONG InterlockedExchangAdd(
PLONG volatile plAddend,//需操作的值
LONG lInCrement); //plAddend的增量大小
LONGLONG InterlockedExchangeAdd64(
PLONGLONG volatile pllAddend,//同上
LONGLONG llInCrement); //同上
Volatile:表明变量可能会被意想不到改变,这样编译器就不会去假设这个变量的值了.精确地说,优化器在用到这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器的值.
LONG InterlockedIncrement(
LPLONG volatile lpAddend);//递增的变量
Interlocked的工作方式取决于代码运行的CPU平台.如果是x86系列CPU,那么Interlocked函数会在总线上维持一个硬件信号,这个信号会阻止其他CPU访问同一个内存地址.
说明: C运行库提供了一个_aligned_malloc函数,我们可以使用这个函数来分配一块对齐过的内存. Void * _aligned_malloc(size_t size,size_t alignment)
参数:size:表示要分配的字节数
Alignment:表示要对齐到的字节边界(必须是2的整数幂次方).
如果想要做减法:只需传入一个负数就可以了.
LONG InterlockedExchange(
PLONG volatile plTarget,
LONG lValue)
PVOID InterlockedExchangePointer(
PVOID* volatile ppvTarget,
PVOID pvValue);
说明:这两个函数主要的功能是,把第一个参数所指向的内存地址的当前值,以原子方式替换为第二个参数指定的值.对于32位应用程序,都是用32位替换为另一32位的值.
区别:对于64位,InterlockedExchangPointer替换的是64位值.这两个函数都会返回原来的值.
LONG InterlockedExchange64(
PLONGLONG volatile plTarget,
LONGLONG lValue);
代码段:(旋转锁)
BOOL g_fResourceInUse = FALSE;
void Fun()
{
while ( InterlockedExchange( &g_fResourceInUse,TRUE ) == TRUE )
{
Sleep( 0 );//or use SwitchToThread
}
//access the resource
...
InterlockedExchange( &g_fResourceInUse,FALSE );
}
注意:对于旋转锁,我们一般调用SetProcessPriorityBoot或SetThreadPriorityBoot来禁用线程优先级的提升;此外,我们必须确保变量和锁所保护的数据位于不同的高速缓存行中.(如果锁变量和数据共享同一高速缓存行,那么使用资源的CPU就会与任何试图访问资源的CPU发生争夺,从而影响性能.)
函数:
PLONG InterlockedCompareExchange(
PLONG plDestination,
LONG lExchange,
LONG lpComparand);
LONG InterlockedCompareExchangePointer(
PVOID ppvDestination,
PVOID pvExchange,
PVOID pvComparand);
同样,如果在64位应用程序上,前者还是处理32位,而后者则是64位.
函数功能:比较plDestination和lComparand的值.如果相等,那么将修改*plDestination的值,否则保持该值不变.函数将返回*plDestination原来的值.
其他函数:
LONG InterlockedIncrement(PLONG plAddend);//递增
LONG InterlockedDecrement(PLONG plAddend);//递减
从XP开始,除了能对整数或者布尔值进行这些原子操作外,我们还能使用一系列其他的函数来对一种被称为Interlocked单向链表的栈进行操作.
函数 |
描述 |
InitializeSListHead |
创建一个空栈 |
InterlockedPushEntrySList |
在栈顶添加一个元素 |
InterlockedPopEntrySList |
移除位于栈顶的元素并将他返回 |
InterlockedFlushSList |
清空栈 |
QueryDepthSList |
返回栈中元素的数量 |
3.为了使得CPU(主要针对多处理器)访问各自独立的内存地址,而且不互相干涉.我们应该根据高速缓存行的大小将应用程序的数据组织在一起.另外对于可读写和只读类型的数据也要单独存放.
4.当线程想要访问一个共享资源或者想要得到一些"特殊"事件通知时,线程必须调用操作系统的一个函数,并将线程正在等待的东西作为参数传入.如果操作系统检测到资源已经可以开始使用了,或者特殊事件已经发生了,那么这个函数会立即返回.这样线程将仍然保持可调度状态.(线程可能不会立即运行,它是可调度的,系统会根据规则来分配CPU时间)
如果无法获取对资源的访问权,或者特殊事件尚未发生,那么系统会将线程却换到等待状态,使线程变得不可调度,从而避免了让线程浪费CPU时间.当线程在等待的时候,系统会充当他的代理.记住线程想要访问的资源.当该资源可供使用时,他会自动将线程唤醒.
实际情况:大多数线程在大部分情况下都处理等待状态.当系统检测到所有已经在等待状态中度过了好几分钟的时候,系统的电源管理器将会介入.
我们既不应该使用旋转锁,也不应该进行轮询,而应该调用函数把线程却换到等待状态,直到线程想要访问的资源可供使用为止.
5.关键段:是一小段代码,他在执行之前需要独占对一些共享资源的访问权.
通过使用EnterCriticalSection( PCRITICAL_SECTION g_cs);使得资源上锁
使用LeaveCriticalSection(CRITICAL_SECTION cs);来解锁.
这种关键段的最大好处是:他们非常容易使用,而且他们在内部也使用了Interlocked函数,因此执行熟读非常快.缺点是:他无法用来在多个线程之间进行线程同步.
使用CRITICAL_SECTION具备的两个条件:
● 所有想要访问资源的线程必须知道用来保护资源的CRITICAL_SECTION结构的地址
● 在任何线程试图访问被保护的资源之前必须初始化CRITICAL_SECTION.
初始化该结构的函数如下:
VOID InitializeCriticalSection(PCRITICAL_SECTION pcs);//对pcs指定的值初始化
由于这个函数只是设置一些变量的值,不可能失败.因此函数返回VOID.
当知晓不需要改结构的变量时(不需要共享资源的时候),我们可以调用以下函数来清理:
VOID DeleteCriticalSection(PCRITICAL_SECTION pcs);
关于EnterCriticalSection和LeaveCriticalSection函数详解如下:
■ EnterCriticalSection函数会检查结构中的成员变量.这些变量表示是否有线程正在访问资源,以及那些线程正在访问资源.它会执行下列操作:
● 如果没有线程正在访问资源,那么EnterCriticalSection会更新成员变量,以表示调用线程已获准对资源的访问,并立即返回,这样线程就可以继续执行(访问资源)
● 如果成员变量表示调用线程已经获准访问资源,那么EnterCriticalSection会更新变量,以表示调用调用线程被获准访问的次数,并立即返回,这样线程就可以继续进行了.只有当线程在调用LeaveCriticalSection之前连续调用两次才会发生.
● 如果成员变量表示有一个(调用线程之外的其他)线程已获准访问资源,那么EnterCriticalSection会使用一个事件内核对象来把调用线程却换到等待状态.
LeaveCriticalSection系统会自动更新CRITICAL_SECTION的成员变量并将等待中的线程却换到可调度状态.
如果EnterCriticalSection把一个线程切换到等待状态,那么在很长一段时间内系统可能不会去调度这个线程,事实上,一个编写的非常糟糕的应用程序中,系统可能再也不会给这个线程调度CPU时间了.如果发生这种情况,我们称作线程在挨饿.
实际情况是,等待关键段的线程时绝对不会挨饿的.对于EnterCriticalSection的调用都会引发异常.我们可以再注册表中的以下子项中设置:
HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manger
但是不要把这个值设的太低,否则会影响系统中等待关键段的时间.
如果不想使用EnterCriticalSection则可使用替代函数:
BOOL TryEnterCriticalSection( PCRITICAL_SECTION pcs)
该函数从来都不会让调用线程进入等待状态.它会通过返回值来表示调用线程是否获准访问资源.(即如果函数发现资源正在被其他线程访问,那么他会返回FALSE).如果返回TRUE表示CRITICAL_SECTION成员已经被更新过.(即当前线程获得访问资源权限).
使用LeaveCriticalSection函数来告知系统完成对共享资源的访问:
VOID LeaveCriticalSection(PCRITICAL_SECTION pcs);
该函数会检查内部成员变量并将计数器减1,该计数器用来表示调用线程获取访问共享资源的次数.如果计数器大于0,LeaveCriticalSection会直接返回.不执行其他操作.如果为0,LeaveCriticalSection会更新成员变量,以表示没有任何线程正在访问被保护的资源.查找其他是否有处于等待状态的线程,使其变为可调度状态.
6.当程序试图进入关键段,如果它此时正被另一个线程访问,那么它会立即把当前需要访问关键段的线程从用户模式却换到内核模式.但是,应该说是通常会出现当刚等待的线程处于暂停状态,当前访问关键段的进程已经访问完毕.这种情况下,Microsoft把旋转锁合并到了关键段中.即当使用EnterCriticalSection试图调用的时候,它会用一个旋转锁不停的循环,尝试在一段时间内获得对资源的访问权,只有当尝试失败后,线程才进行切换(用户->内核).
使用旋转锁的主要函数:
BOOL InitializeCriticalSectionAndSpinCount(
PCRITICAL_SECTION pcs,//与InitializeCriticalSection一致
DWORD dwSpinCount);//旋转锁循环的次数(取值为0~0x00FFFFFF之间.
注意:如果在单处理器调用该函数,则函数会忽略dwSpinCount.
DWORD SetCriticalSectionSpinCount(
PCRITICAL_SECTION pcs,
DWORD dwSpinCount);//用来保护进程堆的关键段所使用的旋转次数大约是4000
InitializeCriticalSection函数可能会失败.此时抛出STATUS_NO_MEMORY异常.系统为了提高性能,会使得线程在第一次使用到事件内核对象时真正创建它.只有在使用了DeleteCriticalSection时对象才会被删除.
如果使用InitializeCriticalSectionAndSpinCount来创建关键段,并将dwSpinCount参数的最高位设为1.(此时创建一个与关键段先关联的事件内核对象.创建不成功返回FLASE.但是这样做会造成资源浪费,我们需要这样做的理由如下:
● 我们无法接受EnterCriticalSection失败;
● 我们知道争夺资源一定会发生
● 我们预计进程会在内存不足时运行.
7.SRWLock的目的和关键代码段相同:对资源进行保护,不让其他线程访问它.但是,与关键代码段不同的是,SRWLock允许我们区分那些想要读取资源值的线程(读取者线程)和想要更新资源的值的线程(写入者线程).当所有读取者线程在同一时刻访问共享资源应该是可行的;但是如果写入者线程想要对资源进行更新时就需要同步操作.
其主要的函数如下:
VOID InitializeSRWLock(PSRWLOCK SRWLock);//分配一个SRWLOCK的结构.
其结构是未公开的,我们不能以任何方式来操作其成员.
typedef struct _RTL_SRWLOCK
{
PVOID Ptr;
}RTL_SRWLOCK,*PRTL_SRWLOCK;
一旦创建完成SRWLOCK结构之后,写入者线程就可以调用AcquireSRWLockExclusive,来尝试获得对被保护的资源的独占访问权.
VOID AcquireSWRLockExclusive(PSRWLOCK SRWLock);
完成对资源的更新之后,应该调用ReleaseSRWLockExclusive,这样就可以解除对资源的锁定:
VOID ReleaseSRWLockExclusive(PSRWLOCK SRWLock);
注意:不存在删除或者销毁SRWLock的函数,系统将自动对其结构进行清理.
和关键段相比,SRWLock缺乏下面两个特性:
◆ 不存在TryEnter(Shared/Exclusive)SRWLock之类的函数:如果锁已经被占用,那么调用AcquireSRWLock(Shared/Exclusive)会柱塞调用线程.
◆ 不能递归调用SRWLOCK.也就是说,一个线程不能为了多次写入资源而多次锁定资源,然后调用多次ReleaseSRWLock*来释放对资源的锁定.
8.让线程以原子方式把锁释放并将自己阻塞,直到某个条件成立为止,我们可以使用一下两个函数:
BOOL SleepConditionVariableCS(
PCONDITION_VARIABLE pConditionVariable,//已初始化的条件变量(也即等待该 //条件的变量
PCRITICAL_SECTION pCriticalSection,
DWORD dwMillseconds);//希望花多少时间来等待条件变量被触发.
BOOL SleepConditionVariableSRW(
PCONDITION_VARIABLE pConditionVariable,
PSRWLOCK pSRWLock,
ULONG Flags);//如果条件变量被触发,我们想线程以哪种方式来得到锁,
//对写入者线程来说,应该传入0,表示希望独占对资源的访问,
//对读取者线程来说,应传入CONDITION_VARIABLE_LOCKMODE_SHARED,表 //示希望共享对资源的访问.
当指定的时间用完的时候,如果条件变量尚未被触发,函数会返回FALSE,否则函数会返回TRUE.当函数返回FLASE的时候,线程显然并没有获得锁或关键段.
阻塞之后,当另一个线程检测到相应的条件已经满足的时候,他会调用WakeConditionVariable或者WakeAllConditionVariable,这样阻塞在Sleep*函数中的线程就会被唤醒.
VOID WakeConditionVariable(
PCONDITION_VARIABLE COnditionVariable);
VOID WakeAllConditionWariable(
PCONDITION_VARIABLE ConditionVariable);
10. 在使用锁的时候,需要一些窍门和技巧(注:这些窍门和技巧同样适合内核同步对象).
◆ 以原子方式操作一组对象时使用一个锁.多个对象聚在一起构成单独的"逻辑"资源.无论我们需要对这个逻辑资源进行读操作还是写操作,都应该只是用一个锁.应用程序中的没有逻辑资源都应该有自己的锁,用来对逻辑资源的部分和整体的访问进行同步.我们不应该为所有的逻辑资源创建一个单独的锁,这是因为多个线程访问不同逻辑资源,会降低可伸缩性:任意时刻只允许一个线程执行.
◆ 同时访问多个逻辑资源,如果每个资源都有自己的锁,那么我们必须使用所有的锁才能以原子方式完成这些操作.在使用访问临界资源的时候,两次访问顺序应该保持一致.(这样避免产生死锁.
◆ 不要长时间占用锁.