关键段

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

下面的代码展示了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);//释放表示”队列“的变量的访问权限
    }
}

劳动果实,转载请注明出处:http://www.cnblogs.com/P_Chou/archive/2012/06/20/critical-section-in-thread-sync.html

分类: C\C++,Windows Programming

Windows线程同步--关键段和旋转锁相关推荐

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

    上一篇<秒杀多线程第四篇 一个经典的多线程同步问题>提出了一个经典的多线程同步互斥问题,本篇将用关键段CRITICAL_SECTION来尝试解决这个问题. 本文首先介绍下如何使用关键段,然 ...

  2. 经典线程同步 关键段CS

    本文参考http://blog.csdn.net/morewindows/article/details/7442639 关键段CRITICAL_SECTION一共就四个函数,使用很是方便.下面是这四 ...

  3. 线程同步--关键代码段(一)

    线程同步有四种方式 但是在一个进程中,效率最高的,方式是 :关键代码段 #include <iostream> #include <windows.h> #include &l ...

  4. 线程同步--关键代码段(二)

    在我们接触到的多线程书籍里面,提到最多的就是线程同步问题了. 但是,我们看到最多的例子也是对一个临界资源的访问. 但是当我们自认为感觉靴子很好的时候,问题出现了,怎么才能够使我们让线程按照一定的顺序访 ...

  5. windows线程同步-原子操作-Interlocked系列函数(用户模式)

    Interlocked系列函数用来保证原子访问. InterlockedExchangeAdd提供保证long类型的原子操作. InterlockedExchangeAdd64提供long long ...

  6. Windows线程同步、CreateThread与_beginthread的区别

    进程与线程的区别 一个exe是可执行程序文件,文件被系统加载到内存并将相关资源(比如内存.文件)准备好,并在内存中开始运行后,此时叫做一个进程.进程进行的是负责与操作系统资源的协调,进程所需要的资源在 ...

  7. Windows线程同步机制的区别与比较及进程通信方法

    原文:http://blog.csdn.net/eulb/article/details/2177500 多线程同步机制 (Windows) 线程的同步机制: 1.   Event 用事件(Event ...

  8. Windows线程同步API

    本文主要总结创建.结束线程和WIN32 API提供的一些线程同步方法.同步方法包括用户态同步方式:InterLock.CriticalSection.SRWLock和内核态同步方式:Event.Sem ...

  9. Linux线程同步与Windows线程同步

    简介 线程同步概念:在多线程下,在一段时间内只允许一个线程访问资源,不允许其它线程访问. 在WIN32中,同步机制主要有以下几种: (1)事件(Event); (2)信号量(semaphore); ( ...

最新文章

  1. Camtasia Studio 7 试用笔记
  2. xcode8控制台输出大量不用的log的问题解决NSLog失效的解决
  3. 数据库中char与varchar类型的区别
  4. Java中的24种设计模式与7大原则
  5. python字符串截取方法_如何使用python语言中的字符串方法截取字符串
  6. 什么?程序员还要了解经济学?! 1
  7. opencv 实现图像时钟
  8. recv函数阻塞_socket缓冲区以及阻塞模式详解
  9. 源码安装mysql初始化报错_源码安装MySQL5.6.39后,修改配置文件启动报错
  10. 0到50带圆圈的数字序号有需要的吗:)
  11. Pop猫 回收站图标
  12. oreo另一个意思_孑孓、仄亾、片爿…看起来天生一对的字,意思竟然大不同
  13. 【机器学习基础】样本类别不平衡的解决办法
  14. 树莓派CM4开机准备开发环境
  15. 教你利用VMM虚拟机安装LEDE旁路由实现软路由超强功能的方法教程
  16. 低功耗SD\SPI NAND Flash芯片
  17. 第五篇:Spring源码篇-ApplicationContext
  18. 手把手教你安装Juniper 模拟器
  19. 化学中的机器学习方法1
  20. 头条一面:亿级数据怎么统计?

热门文章

  1. ps图片如何实现渐变
  2. swagger2使用步骤
  3. java守护线程与用户线程_详解Java线程-守护线程与用户线程
  4. data image java,类 java.awt.image.DataBuffer 的使用 (Java 2 Platform SE 6)
  5. 命令进入mysql创建jira_JIRA使用教程:连接数据库—MySQL_MySQL
  6. c 直接访问mysql_C语言访问MySQL数据库的方法
  7. html代码大全贴音乐,网页音乐代码大全
  8. php反序列化总结与学习
  9. vue编程式导航,命名路由
  10. javeWeb springMvc获取到的参数附带特殊符号,接收后被转义