秒杀多线程第五篇 经典线程同步 关键段CS

转载:秒杀多线程第五篇 经典线程同步 关键段CS


学习笔记:

该文章只讲解了关键段怎么实现,其工作原理没有做详细介绍,在另一个博客看到对关键段的介绍,把关键段的原理说得很详细,故贴出。

出处:http://www.cnblogs.com/P_Chou/archive/2012/06/20/critical-section-in-thread-sync.html

关键段

关键段(Critical Section)是一小段代码,它在执行之前需要独占对一些共享资源的访问权。这种方式可以让多行代码以“原子方式”对资源进行操控。这里的原子方式,指的是代码知道除了当前线程之外,没有其他任何线程会同时访问该资源。当然,系统仍然可以暂停当前线程去调度其他线程。但是,在当前线程离开关键段之前,系统是不会去调度任何想要访问同一资源的其他线程的。

个人理解:关键段,顾名思义就是一段关键的代码,设置一个关键段根据上述介绍可理解为:当一个共享资源在这端代码中时,线程A执行该端代码时,只能线程A才能访问该共享资源,其他线程无法访问,直到线程A离开该关键段。

下面的代码展示了Critical Section的使用方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const  int  COUNT = 10;
int  g_nSum = 0;
CRITICAL_SECTION g_cs; //CRITICAL_SECTION struct
 
DWORD  WINAPI FirstThread( PVOID  pvParam){
     EnterCriticalSection(&g_cs); //Try enter critical section
     g_nSum = 0;
     for ( int  n = 1 ; n <= COUNT ; n++) g_nSum+=n;
     LeaveCriticalSection(&g_cs);
     return (g_nSum);
}
 
DWORD  WINAPI SecondThread( PVOID  pvParam){
     EnterCriticalSection(&g_cs); //Try enter critical section
     g_nSum = 0;
     for ( int  n = 1 ; n <= COUNT ; n++) g_nSum+=n;
     LeaveCriticalSection(&g_cs);
     return (g_nSum);
}

假如没有上面的EnterCriticalSection和LeaveCriticalSection,当两个线程函数分别在两个线程中执行的时候,g_nSum的状态是不可预计的。

在上面的代码中,首先定义了一个叫g_cs的CRITICAL_SECTION数据结构,然后把任何需要访问共享资源(这里的g_nSum)的代码放在EnterCriticalSectionLeaveCriticalSection之间。这里需要注意的是,关键段需要用在所有的相关线程中(即:上面的两个线程函数都要放在关键段中),否则共享资源还是有可能被破坏(只要对线程调度有清晰的认识就很容易理解其中的原因)。另外,在调用EnterCriticalSection之前需要调用InitializeCriticalSection初始化,当不需要访问共享资源的时候,应该调用DeleteCriticalSection:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* Sample C/C++, Windows, link to kernel32.dll */
#include <windows.h>
  
static  CRITICAL_SECTION cs; /* This is the critical section object -- once initialized,
                                it cannot be moved in memory */
                             /* If you program in OOP, declare this as a non-static member in your class */
  
/* Initialize the critical section before entering multi-threaded context. */
InitializeCriticalSection(&cs);
  
void  f()
{
     /* Enter the critical section -- other threads are locked out */
     EnterCriticalSection(&cs);
  
     /* Do some thread-safe processing! */
  
     /* Leave the critical section -- other threads can now EnterCriticalSection() */
     LeaveCriticalSection(&cs);
}
  
/* Release system object when all finished -- usually at the end of the cleanup code */
DeleteCriticalSection(&cs);

 

关键段工作原理

EnterCriticalSection会检查CRITICAL_SECTION中某些成员变量,这些成员变量表示是否有线程正在访问资源:

  • 如果没有线程正在访问资源,那么EnterCriticalSection会更新成员变量,以表示调用线程已经获准对资源的访问,并立即返回,这样线程就可以继续执行。
  • 如果成员变量表示调用线程已经获准访问资源,那么EnterCriticalSection会更新变量,以表示调用线程被获准访问的次数
  • 如果成员变量表示其他线程已经获准访问资源,那么EnterCriticalSection会使用一个事件内核对象把当前线程切换到等待状态。这样线程不会像前一篇讲的旋转锁(spinlock)那样耗费CPU。

关键段的核心价值在于它能够以原子的方式执行所有这些测试。另外TryEnterCriticalSection跟EnterCriticalSection一样拥有对共享资源的检测能力,但是不会阻塞调用线程。

 

关键段与旋转锁

关键段的另一个核心价值在于它可以使用旋转锁来对共享资源进行一定时间的“争用”,而不是立刻让线程进入等待状态、进入内核模式(线程从用户模式切换到内核模式大约需要1000个CPU周期)。因为,很多情况下共享资源不太会占用太长的时间,如果因为一个即将释放的共享资源而将线程切换到内核模式,将得不偿失。所以默认情况下在关键段阻塞线程之前,会多次尝试用旋转锁来“争用”共享资源,如果在这期间“争用”成功,那么EnterCriticalSection就会返回,代码将进入关键段执行;如果没有成功,则会将线程切换到等待状态。需要注意的是:只有在多核情况下才能够使关键段尝试这种特性。

为了在使用关键段的时候同时使用旋转锁,必须用如下函数来初始化关键段:

1
2
3
4
BOOL  WINAPI InitializeCriticalSectionAndSpinCount(
   __out  LPCRITICAL_SECTION lpCriticalSection,
   __in   DWORD  dwSpinCount
);

下面的函数用以改变关键段的旋转次数:

1
2
3
4
DWORD  WINAPI SetCriticalSectionSpinCount(
   __inout  LPCRITICAL_SECTION lpCriticalSection,
   __in     DWORD  dwSpinCount
);

关键段还可以和条件变量配合使用,这部分内容将在下一篇涉及。

更多关于关键段的内容可以参见:http://blog.csdn.net/morewindows/article/details/7442639

最后,设计一个简单的带一个缓冲队列的Log方法,要求线程安全,下面给出C++的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
void  Log( int  nLevel, const  WCHAR * message)
{
     struct  DelayedLogInfo
     {
         int  level;
         std::wstring message;
     };
     static  std::list<DelayedLogInfo> c_LogDelay; //log记录的缓冲队列
 
 
     if  (TryEnterCriticalSection(&g_CsLog)) //获得整个log的访问权限,如果失败则尝试在else里面获得对队列的访问权限
     {
         EnterCriticalSection(&g_CsLogDelay); //读队列前,获得表示”队列“的变量的访问权限
 
         while  (!c_LogDelay.empty()) //循环把队列中的东西全都写掉
         {
             DelayedLogInfo& logInfo = c_LogDelay.front();
             LogInternal(logInfo.level, logInfo.message.c_str());
 
             c_LogDelay.erase(c_LogDelay.begin());
         }
 
         LeaveCriticalSection(&g_CsLogDelay); //释放表示”队列“的变量的访问权限
 
         //代码到这里释放了队列这个共享对象,因此,在下面这真正写入log时,其他试图写log的线程将只能向缓冲队列中写数据
 
         // Log the message
         LogInternal(nLevel, message);
 
         LeaveCriticalSection(&g_CsLog);
     }
     else
     {
         EnterCriticalSection(&g_CsLogDelay); //写队列前,获得表示”队列“的变量的访问权限
 
         DelayedLogInfo logInfo = {nLevel,  message};
         c_LogDelay.push_back(logInfo); //写队列
 
         LeaveCriticalSection(&g_CsLogDelay); //释放表示”队列“的变量的访问权限
     }
}

由于关键段的“线程所有权”特性所以关键段只能用于线程的互斥而不能用于同步。

[cpp]  view plain  copy
  1. #include <stdio.h>  
  2. #include <process.h>  
  3. #include <windows.h>  
  4. long g_nNum;  
  5. unsigned int __stdcall Fun(void *pPM);  
  6. const int THREAD_NUM = 10;  
  7. //关键段变量声明  
  8. CRITICAL_SECTION  g_csThreadParameter, g_csThreadCode;  
  9. int main()  
  10. {  
  11.     printf("     经典线程同步 关键段\n");  
  12.     printf(" -- by MoreWindows( http://blog.csdn.net/MoreWindows ) --\n\n");  
  13.   
  14.     //关键段初始化  
  15.     InitializeCriticalSection(&g_csThreadParameter);  
  16.     InitializeCriticalSection(&g_csThreadCode);  
  17.       
  18.     HANDLE  handle[THREAD_NUM];   
  19.     g_nNum = 0;   
  20.     int i = 0;  
  21.     while (i < THREAD_NUM)   
  22.     {  
  23.         EnterCriticalSection(&g_csThreadParameter);//进入子线程序号关键区域  
  24.         handle[i] = (HANDLE)_beginthreadex(NULL, 0, Fun, &i, 0, NULL);  
  25.         ++i;  
  26.     }  
  27.     WaitForMultipleObjects(THREAD_NUM, handle, TRUE, INFINITE);  
  28.   
  29.     DeleteCriticalSection(&g_csThreadCode);  
  30.     DeleteCriticalSection(&g_csThreadParameter);  
  31.     return 0;  
  32. }  
  33. unsigned int __stdcall Fun(void *pPM)  
  34. {  
  35.     int nThreadNum = *(int *)pPM;   
  36.     LeaveCriticalSection(&g_csThreadParameter);//离开子线程序号关键区域  
  37.   
  38.     Sleep(50);//some work should to do  
  39.   
  40.     EnterCriticalSection(&g_csThreadCode);//进入各子线程互斥区域  
  41.     g_nNum++;  
  42.     Sleep(0);//some work should to do  
  43.     printf("线程编号为%d  全局资源值为%d\n", nThreadNum, g_nNum);  
  44.     LeaveCriticalSection(&g_csThreadCode);//离开各子线程互斥区域  
  45.     return 0;  
  46. }  

由上面可以看出:当23行代码调用 EnterCriticalSection(&g_csThreadParameter)进入子线程序号关键域,而在36行调用LeaveCriticalSection(&g_csThreadParameter)离开子线程序号关键区域,此时就可以执行25行的++i;当++i执行完后下一个循环到来,此时就又执行23行和36行,接着执行25行++i,这时就可能出现做了多次++i,而线程函数Fun只执行了一次,这样就线程就不同步了。得出的结论就是:关键段只能用于线程的互斥而不能用于同步。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值