day9 – 同步

windows支持4种类型的同步对象,能够用来同步由并发运行的线程所执行的操做:c++

  • 临界区编程

  • 互斥量windows

  • 事件api

  • 信号量数组

    MFC在名为CCriticalSection、CMutex、CEvent和CSemaphore的类中封装了这些对象。下面分别对这些同步对象进行介绍。安全

  • 临界区多线程

    最简单类型的线程同步对象就是临界区。临界区用来串行化对由两个或者多个线程共享的资源的访问。这些线程必须属于相同的进程,由于临界区不能跨越进程的边界工做。并发

    临界区背后的思想就是,每一个独占性地访问一个资源的线程能够在访问那个资源以前锁定临界区,访问完成以后解除锁定。若是线程B试图锁定当前线程A锁定的临界区,线程B将阻塞直到该临界区空闲。阻塞时,线程B处在一个十分有效的等待状态,它不消耗处理器时间。函数

  • 互斥量工具

    Mutex是单词mutually和exclusive的缩写。与临界区同样,互斥量也是用来得到对由两个或者更多线程共享的资源的独占性访问的。与临界区不一样的是,互斥量能够用来同步在相同进程或者不一样进程上运行的线程。对于进程内线程同步的须要,临界区通常要优于互斥量,由于临界区更快,可是若是但愿同步在两个或者多个不一样进程上运行的线程,那么互斥量就更合适了。

    互斥量和临界区还有另外有一个差异。若是一个线程锁定了临界区而终止时没有解除临界区的锁定,那么等待临界区空闲的其余线程将无限期地阻塞下去。然而,若是锁定互斥量的线程不能在其终止前解除互斥量的锁定,那么系统将认为互斥量被“放弃”了,并自动释放该互斥量,这样等待进程就能够继续进行。

  • 事件

    MFC的CEvent类封装了Win32事件对象。一个事件不仅是操做系统内核中的一个标记。在任何特定的时间,事件只能处在两种状态中的一种:设置或者重置。设置状态事件也能够认为是处于信号状态,重置状态事件也能够认为是处于非信号状态。CEvent::SetEvent设置一个事件,而CEvent::ResetEvent将事件重置。相关函数CEvent::PulseEvent能够在一次操做中设置和清除一个事件。

    有时事件被描述为“线程触发器”。一个线程调用CEvent::Lock在一个事件上阻塞,等待该事件变为设置状态。另外一个线程设置事件,从而唤醒该等待线程。设置事件就像按下触发器。它解除等待线程的阻塞并容许该线程继续执行。一个事件可能有一个或者多个在事件上阻塞的线程,若是你的代码正确,那么当该事件变为设置状态时,全部的等待线程都将被唤醒。

    Windows支持两种不一样类型的事件:自动重置事件手动重置事件。它们之间的差异很是细微,但其意义倒是深远的。当在自动重置事件上阻塞的线程被唤醒时,该事件被自动重置为信号状态。手动重置事件不能自动重置,它必须使用编程方式重置。用于选择自动重置事件仍是手动重置事件——以及一旦作出选择以后如何使用它们——的规则以下:

    \1) 若是事件只触发一个线程,那么使用自动重置事件和使用SetEvent唤醒等待线程。这里不须要调用ResetEvent,由于线程被唤醒那一刻事件将自动重置。

    \2) 若是事件将触发两个或者多个线程,那么使用手动重置线程和使用PulseEvent唤醒全部的等待线程。并且,不须要调用ResetEvent,由于PulseEvent在唤醒线程后将重置事件。

    使用手动重置事件来触发多个线程是相当重要的。为何?由于自动重置事件将在其中一个线程被唤醒的那一刻被重置,所以它只触发一个线程。使用PulseEvent来按下手动重置事件上的触发器也是至关重要的。若是使用SetEvent和ResetEvent,就有保证全部的等待线程都被唤醒。PulseEvent不只可以设置和重置事件,并且还确保了全部在事件上等待的线程在重置事件以前被唤醒。

    与互斥量同样,事件能够用来协调在不一样进程上运行的线程,对于跨越进程边界的事件,必须给它指定一个名称。

    那么,怎样使用事件来同步线程呢?例如,线程A向缓冲区填充数据,而线程B须要对缓冲区的数据进行处理。假定线程B必须等待来自线程A的一个信号(缓冲区已初始化并准备工做)。这时,自动重置事件是完成这项工做的绝好工具。

    自动重置事件适用于触发单线程,但若是与线程B平行的线程对C缓冲的数据进行了彻底不一样的操做,那会怎么样呢?这就须要手动重置事件一同唤醒线程B和C,由于自动重置事件只能唤醒其中的一个或者另外一个,而不能都唤醒 。

    再次重申,自动重置事件和CEvent::SetEvent释放在事件上阻塞的单个线程,手动重置事件和CEvent::PulseEvent释放多个线程。

  • 信号量

    最后一种同步化对象是信号量。若是任何一个线程锁定了事件、临界区和互斥对象,Lock就会阻塞它们,在这个意义上,这3种对象具备这样的特性:”要么有,要么什么都没有“。信号量则不一样,它始终保存有可用资源数量的资源数。锁定信号量会减小资源数,释放信号量则增长资源数。只有在线程试图锁定资源数为0的信号量时,线程才会被阻塞。在这种状况下,直到另外一个线程释放信号量,资源数随之增长时,或者直到指定的超时时间期满时,该线程才会被释放。信号量能够用来同步同一进程中的线程,也能够同步不一样进程中的线程。

 为何使用线程同步?

  同步能够保证在一个时间内只有一个线程对某个资源(如操做系统资源等共享资源)有控制权。共享资源包括全局变量、公共数据成员或者句柄等。同步还可使得有关联交互做用的代码按必定的顺序执行。

  线程同步的方式?

  同步对象有:CRITICAL_SECTION (临界区),Event(事件),Mutex(互斥对象),Semaphores(信号量)。

  本文重点讲解CRITICAL_SECTION (临界区)。

  临界区,说白了,就是“锁”。看过星爷的《破坏王》的朋友都知道,那个送外卖的小子,就是靠自创绝招“无敌风火轮”将大师兄战胜,抱得美人归。“无敌风火轮”的本质就是:锁!

  怎么锁?

  这里有四个关键函数:InitializeCriticalSection EnterCriticalSection LeaveCriticalSection DeleteCriticalSection来完成此机制。

  使用临界区对象的时候,首先要定义一个临界区对象CriticalSection:

  CRITICAL_SECTION CriticalSection;

  而后,初始化该对象:InitializeCriticalSection(&CriticalSection);

  若是一段程序代码须要对某个资源进行同步保护,则这是一段临界区代码。在进入该临界区代码前调用EnterCriticalSection函数,这样,其余线程都不能执行该段代码,若它们试图执行就会被阻塞。

  完成临界区的执行以后,调用LeaveCriticalSection函数,其余的线程就能够继续执行该段代码。

  简要实例

  下面的代码中,若是不加CRITICAL_SECTION ,有可能形成在线程1给data设置完名字后,线程2给data设置年龄,形成了数据紊乱,因此有必要使用同步机制,将其锁住,保证数据的安全。

class Data  
{  
private:  
    CString Name;  
    int Age;  
public:  
    void SetName(const CString& name)  
    {  
        Name = name;  
    }  
      
    void SetAge(int age)  
    {  
        Age = age;  
    }  
      
    void GetName(CString &name)  
    {  
        name = Name;  
    }  
      
    void GetAge(int &age)  
    {  
        age = Age;  
    }  
};  
  
Data g_data; //全局变量  
CRITICAL_SECTION CriticalSection;   
  
//线程函数  
DWORD WINAPI ThreadProc( LPVOID lpParameter )  
{  
    EnterCriticalSection(&CriticalSection);   
    data.SetName("赵星星");  
    data.SetAge(20);  
    LeaveCriticalSection(&CriticalSection);  
}  
  
int main()  
{  
  
    InitializeCriticalSection(&CriticalSection);   
      
    //建立线程,执行线程函数  
    //......  
      
    DeleteCriticalSection(&CriticalSection);  
  
    return 0;  
}

真锁?假锁?

  能够定义CRITICAL_SECTION 数组:CRITICAL_SECTION g_Critical[10];

  CRITICAL_SECTION 没有超时的概念,若是函数LeaveCriticalSection不被调用,则其余线程将无限期的等待。容易形成死锁。

  CRITICAL_SECTION 属于轻量级的线程同步对象,相对于mutex来讲,它的效率会高不少。mutex能够用于进程之间的同步,CRITICAL_SECTION只在同一个进程有效。

  实际上,CRITICAL_SECTION 锁的是代码段,若是代码段中有对资源的占用,只是间接的锁住了该资源,咱们也能够称之为“假锁”。

Windows下进程内部的各个线程之间的同步不须要借助内核对象,Windows提供的默认在用户模式下的线程同步工具。

互锁函数为多线程同步访问共享变量提供了一个简单的机制。若是变量在共享内存,不一样进程的线程也可使用此机制。

互锁函数对共享变量的操做是原子的,这个原子性体如今保证多线程在同一个时刻只能有一个线程得到对该同步变量的操做权限。

(1)InterlockedExchangeAdd()

LONG __cdecl InterlockedExchangeAdd(  
  _Inout_  LONG volatile *Addend,  
  _In_     LONG Value  
);  
LONGLONG __cdecl InterlockedExchangeAdd64(  
  _Inout_  LONGLONG volatile *Addend,  
  _In_     LONGLONG Value  
);  
//Addend:指向一个32位变量的指针;  
//Value:共享变量上要加的值;  
//Return value:返回修改前变量的值;

InterlockedExchangeAdd互锁函数提供了对变量进行加法操做,保证了同一时刻只有一个线程对这个变量进行加法操做。Value是正数的时候进行加法操做,是负数的时候进行减法操做。

(2)InterlockedIncrement()&InterlockedDecrement()

LONG __cdecl InterlockedIncrement(  
  _Inout_  LONG volatile *Addend  
);  
LONG __cdecl InterlockedDecrement(  
  _Inout_  LONG volatile *Addend  
);  
//Addend:指向一个32位变量的指针;  
//Return value:修改前变量的值;

InterlockedIncrement互锁函数对一个32变量进行增1操做,InterlockedDecrement则进行减1操做。保证线程之间的互斥的进行访问。这两个函数都是16位和64位版本。

(3)InterlockedExchange()

LONG __cdecl InterlockedExchange(  
  _Inout_  LONG volatile *Target,  
  _In_     LONG Value  
);  
//Target:指向一个32位变量的指针;  
//Value:要替换的值;  
//Return Value:修改以前的值;  
PVOID __cdecl InterlockedExchangePointer(  
  _Inout_  PVOID volatile *Target,  
  _In_     PVOID Value  
);  
//Target:指向一个32位变量的指针的指针;  
//Value:要替换的指针的值;  
//ReturnValue:修改以前的值;

InterlockedExchange函数把第一个参数指向的内存地址的值,以原子的方式替换为第二个参数的值。并返回原来的值。InterlockedExchangePointer替换的是指针而已。

InterlockedExchange函数还有8位,16位和64位的版本;

(4)InterlockedCompareExchange()

LONG __cdecl InterlockedCompareExchange(  
  _Inout_  LONG volatile *Destination,  
  _In_     LONG Exchange,  
  _In_     LONG Comparand  
);  
//Destination:指向当前值的指针;  
//Exchange:比较成功后要替换的值;  
//Comparand:和当前值进行比较的值;  
//Return Value:修改以前的值;  
PVOID __cdecl InterlockedCompareExchangePointer(  
  _Inout_  PVOID volatile *Destination,  
  _In_     PVOID Exchange,  
  _In_     PVOID Comparand  
);

InterlockedCompareExchange函数会将Destination指向的当前值和Comparand进行比较,若是相同会将Destination指向的值替换为Exchange的值,不然*Destination保持不变。函数的返回值为修改以前的值。

当你建立一个线程时,其实那个线程是一个循环,不像上面 那样只运行一次的。这样就带来了一个问题,在那个死循环里要找到合适的条件退出那个死循环,那么是怎么样实现它的呢?

在Windows里每每是采用事件的 方式,它的实现原理以下:在那个死循环里不断地使用 WaitForSingleObject函数来检查事件是否知足,若是知足就退出线程,不知足就继续运行。

对函数进行解释。

CreateEvent是建立windows事件的意思,做用主要用在判断线程退出,线程锁定方面.

​ 返回值:若是函数调用成功,函数返回事件对象的句柄,若是函数调用失败,函数返回值为NULL

​ EVENT有两种状态:发信号,不发信号。

SetEvent:将EVENT置为发信号。

ResetEvent:将EVENT置为不发信号。

WaitForSingleObject():等待,直到参数所指定的OBJECT成为发信号状态时才返回,OBJECT能够是EVENT,也能够是其它内核对象。

WaitForSingleObject用法:

(1)函数功能描述:用来检测hHandle事件的信号状态,在某一线程中调用该函数时,线程暂时挂起,若是在挂起的dwMilliseconds毫秒内,线程所等待的对象变为有信号状态,则该函数当即返回;若是超时时间已经到达dwMilliseconds毫秒,但hHandle所指向的对象尚未变成有信号状态,函数照样返回。参数dwMilliseconds有两个具备特殊意义的值:0和INFINITE。若为0,则该函数当即返回;若为INFINITE,则线程一直被挂起,直到hHandle所指向的对象变为有信号状态时为止。

(2)函数原型:

DWORD WINAPI WaitForSingleObject
(
__in HANDLE hHandle,      // 句柄
__in DWORD dwMilliseconds   // 时间间隔
);

(3)参数:

hHandle:对象句柄。能够指定一系列的对象,如Event、Job、Memory resource notification、Mutex、Process、Semaphore、Thread、Waitable timer等。

dwMilliseconds:定时时间间隔,单位为milliseconds(毫秒).

(1)若是指定一个非零值,函数处于等待状态直到hHandle标记的对象被触发,或者时间到了。

(2)若是dwMilliseconds为0,对象没有被触发信号,函数不会进入一个等待状态,它老是当即返回。

(3)若是dwMilliseconds为INFINITE,对象被触发信号后,函数才会返回。

(4)返回值:执行成功,返回值指示出引起函数返回的事件。可能的返回值:

WAIT_ABANDONED 0x00000080:当hHandle为mutex时,若是拥有mutex的线程在结束时没有释放核心对象会引起此返回值。

WAIT_OBJECT_0 0x00000000 :指定的对象出有信号状态。

WAIT_TIMEOUT 0x00000102:等待超时。

WAIT_FAILED 0xFFFFFFFF :出现错误,可经过GetLastError获得错误代码。

具体实例

//test.h
#include <windows.h>          // for HANDLE
HANDLE m_HandleWaitForTest;
m_HandleWaitForTest= CreateEvent(NULL,FALSE,FALSE,NULL);
// test.cpp
void fun()
{
    ......//其余操做
    ResetEvent(m_HandleWaitForTest);//程序刚开始事件清零,设为无信号状态
    ......//其余操做
    QtConcurrent::run(this, &MotionTest::TestEvent);//开启一个线程
    ......//其余操做
    if (!(WaitForSingleObject(m_HandleWaitForTest, 5000) == WAIT_OBJECT_0))
    {
        return;
    }
}
void MotionTest::TestEvent()
{
    if(Test())
    {
        SetEvent(m_HandleWaitForTest);//设置为有信号,为后续判断该线程是否执行完作准备
    }
    else
    {
        MessageInformation(tr("test failed"), false);
    }
}

WaitForMultipleObjects的用法

DWORD WaitForMultipleObjects( DWORD nCount, const HANDLE* lpHandles, BOOL bWaitAll, DWORD dwMilliseconds);

其中参数

nCount 句柄的数量 最大值为MAXIMUM_WAIT_OBJECTS(64)

HANDLE 句柄数组的指针。

HANDLE 类型能够为(Event,Mutex,Process,Thread,Semaphore )数组

BOOL bWaitAll 等待的类型,若是为TRUE 则等待全部信号量有效在往下执行,FALSE 当有其中一个信号量有效时就向下执行

DWORD dwMilliseconds 超时时间 超时后向执行。 若是为WSA_INFINITE 永不超时。若是没有信号量就会在这死等。

具体事例

m_threadShow = std::thread(std::mem_fn(&MainWindow::ShowData), this);
MainWindow::~MainWindow()
{
    SetEvent(m_KillEvent);
    if(m_threadShow.joinable())
        m_threadShow.join();
    delete ui;
}
//好比开一个线程,可用WaitForMultipleObjects,在析构中SetEvent killEvent事件,便可释放线程函数
//其余状况只要有m_showEvent则会向下执行
void MainWindow::ShowData()
{
    while(1)
    {
        HANDLE Status[2] = {m_KillEvent, .m_showEvent};
        if(WaitForMultipleObjects(2, Status, FALSE, INFINITE) == WAIT_OBJECT_0)//第一个事件发生
        {
            break;
        }
        //...........
    }
}

windows api中提供了一个互斥体,功能上要比临界区强大。Mutex是互斥体的意思,当一个线程持有一个Mutex时,其它线程申请持有同一个Mutex会被阻塞,所以能够经过Mutex来保证对某一资源的互斥访问(即同一时间最多只有一个线程访问)。

调用CreateMutex能够建立或打开一个Mutex对象,其原型以下

HANDLE CreateMutex
(
  LPSECURITY_ATTRIBUTES lpMutexAttributes,
  BOOL bInitialOwner,
  LPCTSTR lpName
);
相关文章
相关标签/搜索