先介绍几个概念:
原子访问:是指线程在访问资源时能够确保所有其他线程都不在同一时间内访问相同的资源
循环锁:是指在线程1中如果要对变量进行操作,要先查看这个变量(或资源)有没有被其它线程用到,如果是,则一直循环(循环次数自定),直到其它线程放弃对该变量(或资源)的控制,如果否,直接可以对该变量(或资源)进行操作
临界区:在所有同步对象中,临界区是最容易使用的,但它只能用于同步单个进程中的线程。取得对某个数据区的访临界区一次只允许一个线程访问权。还有,在用到的同步对象中,只有临界区不是内核对象,它不由操作系统的低级部件管理,而且不能使用句柄来操纵,由于不是内核对象,使得它作为一种轻量级的同步机制,同步速度比较快。
原子访问都是InterLocked开头的互锁函数家族:如InterlockedExchangeAdd/InterlockedExchange,具体参看MSDN
循环锁实例:
BOOL g_fResourceInUse = FALSE; void Func1() { //Wait to access the resource. while(InterlockedExchange(&g_fResourceInUse, TRUE) == TRUE)//这里也可以加个标记循环次数的变量,循环次数一到,就退出循环锁 Sleep(0); //Access the resource. //other content //We no longer need to access the resource. InterlockedExchange(&g_fResourceInUse, FALSE); }while循环是循环运行的,它将g_fResourceInUse 中的值改为TRUE,并查看它的前一个值,以了解它是否是TRUE。如果这个值原先是FALSE,那么该资源并没有在使用,而是调用线程将它设置为在用状态并退出该循环。如果前一个值是TRUE,那么资源正在被另一个线程使用,while循环将继续循环运行。
如果另一个线程要执行类似的代码,它将在while循环中运行,直到g_fResourceInUse 重新改为FALSE。调用函数结尾处的InterlockedExchange,将g_fResourceInUse重新设置为FALSE。
使用循环锁必须格外小心,因为循环锁会浪费C P U时间。C P U必须不断地比较两个值,直到一个值由于另一个线程而“奇妙地”改变为止。另外,该代码假定使用循环锁的所有线程都以相同的优先级等级运行。也可以把执行循环锁的线程的优先级提高功能禁用(通过调用SetProcessPriorityBoost或SetThreadPriorityBoost函数来实现)
循环锁假定,受保护的资源总是被访问较短的时间。这使它能够更加有效地循环运行,然后转为内核方式并进入等待状态。许多编程人员循环运行一定的次数(比如4 0 0次),如果对资源的访问仍然被拒绝,那么该线程就转为内核方式(大约1000个C P U周期,相当的浪费时间),在内核方式下,它要等待(不消耗C P U时间),直到该资源变为可供使用为止,
临界区使用步骤:
1. 在进程中创建一个临界区,即在进程中分配一个CRITICAL_SECTION数据结构,该临界区结构的分配必须是全局的,这样该进程的不同线程就能访问它。
2. 在使用临界区同步线程之前,必须调用InitializeCriticalSection来初始化临界区。在释放资源之前,只需要初始化一次。
3. EnterCriticalSection:阻塞函数。The function returns when the calling thread is granted ownership。换言之,调用线程不能获取指定临界区的所有权时,该线程将睡眠,且在被唤醒之前,系统不会给它分配CPU。或者使用TryEnterCriticalSection方法尝试进入临界区,如果进入成功,则调用者线程获得临界区的使用权,否则返回失败。TryEnterCriticalSection和EnterCriticalSection之间的最大区别在于TryEnterCriticalSection从来不挂起线程,运用TryEnterCriticalSection这个函数,线程能够迅速查看它是否可以访问某个共享资源,如果不能访问,那么它可以继续执行某些其他操作,而不必进行等待。
4. 执行临界区内的任务
5. BOOL LeaveCriticalSection:非阻塞函数。将当前线程对指定临界区的引用计数减壹;在使用计数变为零时,另一等待此临界区的一个线程将被唤醒。
6. 当不需要再使用该临界区时,使用DeleteCriticalSection来释放临界区需要的资源。此函数执行后,再也不能使用EnterCriticalSection和LeaveCriticalSection,除非再次使用InitializeCriticalSection初始化了该临界区。
关键代码段与循环锁的关系
当线程试图进入另一个线程拥有的关键代码段时,调用线程就立即被置于等待状态。这意味着该线程必须从用户方式转入内核方式(大约1000个CPU周期)。这种转换是要付出很大代价的。在多处理器计算机上,当前拥有资源的线程可以在不同的处理器上运行,并且能够很快放弃对资源的控制。实际上拥有资源的线程可以在另一个线程完成转入内核方式之前释放资源。如果出现这种情况,就会浪费许多C P U时间
为了提高关键代码段的运行性能, M i c r o s o f t将循环锁纳入了这些代码段。因此,当EnterCriticalSection函数被调用时,它就使用循环锁进行循环,以便设法多次取得该资源。只有当为了取得该资源的每次试图都失败时,该线程才转入内核方式,以便进入等待状态。
要将循环锁用于关键代码段,应该调用下面的函数,以便对关键代码段进行初始化:
BOOL InitializeCriticalSectionAndSpinCount(
PCRITICAL_SECTION pcs, DWORD dwSpinCount);
如果在单处理器计算机上运行时调用该函数, dwSpinCount参数将被忽略,它的计数始终被置为0。这是对的,因为在单处理器计算机上设置循环次数是毫无用处的,如果另一个线程正在循环运行,那么拥有资源的线程就不能放弃它。
通过调用下面的函数,就能改变关键代码段的循环次数:
DWORD SetCriticalSectionSpinCount(
PCRITICAL_SECTION pcs, DWORD dwSpinCount);
同样,如果主计算机只有一个处理器,那么dwSpinCount的值将被忽略。我认为,始终都应该将循环锁用于关键代码段,因为这样做有百利而无一害。难就难在确定为dwSpinCount参数传递什么值。为了实现最佳的性能,只需要调整这些数字,直到对性能结果满意为止。作为一个指导原则,保护对进程的堆栈进行访问的关键代码段使用的循环次数是4000次。
提示和技巧:
1.每一个共享资源使用一个CRITICAL_SECTION变量。
如果应用程序中拥有若干个互不相干的数据结构,应该为每一个数据结构创建一个CRITICAL_SECTION.
2.必须始终按照完全相同的顺序请求对资源的访问
同时访问多个资源时,必须始终按照完全相同的顺序请求资源的访问,比如当线程1有
EnterCriticalSection(&cs1);
EnterCriticalSection(&cs2);
//.
LeaveCriticalSection(&cs1);
LeaveCriticalSection(&cs1);
如果线程2也要使用到cs1和cs2,那么他的关键区域进入点的测试顺序要与其他使用cs1和cs2的完全一致,否则就会出现死锁现象。
3.不要长时间运行关键代码段