C++多线程同步技术(MFC)

1. 何时使用同步类

    MFC 提供的多线程类分为两类:同步对象(CSyncObject、CSemaphore、CMutex、CCriticalSection 和 CEvent)和同步访问对象(CMultiLock 和 CSingleLock)。
当必须控制对资源的访问以确保资源的完整性时,使用同步类。同步访问类用于获取对这些资源的访问权。

    若要确定应使用的同步类,请询问以下一系列问题:

    1)应用程序必须等到发生某事才能访问资源(例如,在将数据写入文件之前,必须先从通信端口接收它)吗?
如果是,请使用 CEvent
    2)同一应用程序内一个以上的线程可以同时访问此资源(例如,应用程序允许在同一文档上最多同时打开五个带有视图的窗口)吗?
如果是,请使用 CSemaphore
    3)可以有一个以上的应用程序使用此资源(例如,资源在 DLL 中)吗?
如果是,请使用 CMutex
如果不是,请使用 CCriticalSection
    从不直接使用 CSyncObject。它是其他四个同步类的基类。

2. 何时使用同步访问类

    如果应用程序只与访问单个受控资源有关,请使用 CSingleLock
    如果需要访问多个受控资源中的任何一个,则使用 CMultiLock

3. 如何使用同步类

    写入多线程应用程序时,线程间的同步资源访问是一个常见问题。两个或多个线程同时访问同一数据会导致不合需要的、不可预知的结果。例如,一个线程可能正在更新结构的内容,而另一个线程正在读取同一结构的内容。无法得知读取线程将会收到何种数据:旧数据、新写入的数据或两种数据都有。MFC 提供了多个同步类和同步访问类以帮助解决此问题。
    典型的多线程应用程序具有代表各个线程间要共享的资源的类。正确设计的完全线程安全类不需要调用任何同步函数。该类的任何事情都在内部处理,使您可以将精力集中于如何更好地使用类,而不是它如何会损坏。创建完全线程安全类的有效技术是将同步类合并到资源类中。将同步类合并到共享类是一个简单的过程。
    以维护链接的帐户列表的应用程序为例。此应用程序允许在独立的窗口中最多检查三个帐户,但是在任何特定的时间,只能更新一个帐户。更新帐户后,通过网络将更新的数据发送到数据存档。
    此示例应用程序使用所有这三种类型的同步类。因为它一次最多允许检查三个帐户,所以它使用 CSemaphore 限制对三个视图对象的访问。当试图查看第四个帐户时,应用程序或者等到前三个窗口中有一个关闭,或者该尝试失败。更新帐户时,应用程序使用 CCriticalSection 确保一次只更新一个帐户。更新成功后,发出信号 CEvent 以释放等待该事件信号发送的线程。此线程将新数据发送到数据存档。

4. 设计线程安全类

    若要使类完全线程安全,首先将适当的同步类作为数据成员添加到共享类中。在前面的帐户管理示例中,将CSemaphore 数据成员添加到视图类,将CCriticalSection 数据成员添加到链接的列表类,将 CEvent 数据成员添加到数据存储类。
    下一步,将同步调用添加到修改类中的数据或访问受控资源的所有成员函数中。应该在每个函数中创建 CSingleLock 或 CMultiLock 对象,并调用该对象的Lock 函数。当锁定对象超出范围并被销毁时,该对象的析构函数调用 Unlock 以释放资源。当然,如果愿意,可直接调用Unlock
    用这种方式设计线程安全类使得在多线程应用程序中使用该类与使用非线程安全类一样容易,但却具有更高的安全级别。将同步对象和同步访问权对象封装到资源的类将提供完全线程安全编程的所有优点,而不会有维护同步代码的缺点。
    下面的代码示例通过使用在共享资源类和 CSingleLock 对象中声明的数据成员 m_CritSectionCCriticalSection类型),对此方法进行了说明。通过使用m_CritSection 对象的地址创建 CSingleLock 对象,来试图同步共享资源(从 CWinThread 派生)。试图锁定资源,一旦锁定,即完成了共享对象上的工作。完成工作后,即调用 Unlock 取消锁定资源。

[cpp]  view plain copy
  1. CSingleLock singleLock(&m_CritSection);  
  2. singleLock.Lock();  
  3. // resource locked  
  4. //.usage of shared resource...  
  5.   
  6. singleLock.Unlock();  

此方法的缺点是此类将要比没有添加同步对象的相同类慢一些。而且,如果有一个以上的线程可能删除对象,合并方法不一定始终有效。在这种情况下,最好维持单独同步对象。

5. CSemaphore

    一个CSemaphore类对象代表一个“信号”——一个同步对象,它允许有限数目的线程在一个或多个进程中访问同一个资源。一个CSemaphore对象保持了对当前访问某一指定资源的线程的计数。
    CSemaphore对象用于控制有限数量的用户使用共享资源。它的当前计数是指还可以允许的其它用户的数目。当这个计数达到零的时候,所有对这个由CSemaphore对象控制的资源的访问尝试都将被插入到一个系统队列中等待,直到它们的时间用完或计数值不再为零。这个被控制的资源可以同时接受访问的最大用户数目是在CSemaphore对象的构造期间被指定的。 
    要使用一个CSemaphore对象,则在需要的时候构造这个对象,指定你想要等待的信号的名字,应用程序应该在最初就拥有它。然后你就可以在构造函数返回时访问这个信号。当你要访问这个被控制的资源时,调用CSyncObject::Unlock。 
    使用CSemaphore对象的另一种方法是,将一个CSemaphore类型的变量添加到你想要控制的类中作为一个数据成员。在被控制对象的构造期间,调用CSemaphore数据成员的构造函数来指定访问计数的初始值,访问计数的最大值,信号的名字(如果它要在整个进程中使用),以及需要的安全标志。
    要访问由CSemaphore对象用这种方式控制的资源,首先要在你的资源的访问成员函数中创建一个CSingleLock类型或CMultiLock类型的变量。然后调用加锁对象的Lock成员函数(例如,CSingleLock::Lock)。这时,你的线程将达到对资源的访问,等待资源被释放并访问它,或者是在等待资源被释放的过程中超过了时间,对资源的访问失败。不管是哪一种情况,你的资源都是以一种线程安全(thread-safe)方式被访问的。要释放资源,可以使用加锁对象的Unlock成员函数(例如,CSingleLock::Unlock),或者是让加锁对象超越范围,析构时释放资源。
    另外,你可以单独创建一个CSemaphore对象,并且在尝试访问被控制的资源之前显式地访问CSemaphore对象。这种方法虽然对阅读你的源代码的人来说更加清楚,但是却更易于出错。
    CSemaphore的构造:

[cpp]  view plain copy
  1. CSemaphore(   
  2.         LONG lInitialCount = 1,     
  3.         LONG lMaxCount = 1,     
  4.         LPCTSTR pstrName = NULL,     
  5.         LPSECURITY_ATTRIBUTES lpsaAttributes = NULL     
  6.         );   

    1)lInitialCount:初始访问计数值,必须不小于0,不大于最大计数值
    2)lMaxCount :允许访问计数的最大值,必须大于0
    3)pstrName:“信号量”的名字,如果该对象用于进程间的处理,必须提供。如果为NULL,该对象是未命名的。如果指定名字匹配一个已存在的信号,构造函数将引用该名称的信号对象构造一个新的信号对象,如果指定的名字匹配一个已存在的非semaphore的同步对象,构造失败。
    4)lpsaAttributes:对象的安全属性。通常为NULL。具体参见MSDN关于SECURITY_ATTRIBUTES

6. CMutex

    CMutex类的对象代表“哑程(mutex)”——它为一个同步对象,只允许某一个线程独自访问同一资源。在仅仅一个线程被允许用于修改数据或其它被控制的资源时,哑程将变得非常有用。例如,给链接的列表增添一个结点就是只允许一个线程的过程。通过使用CMutex对象来控制链接列表,此时只有一个线程能够获得列表的访问权。
    若要使用CMutex 对象,首先要构造一个所需的CMutex 对象。然后指定希望等待的哑程的名称,那么应用最初就将拥有它。可以在构造函数返回时,访问哑程。当你已经访问了被控制的资源后,再调用CSyncObject::Unlock函数。 
    另外一种使用CMutex 对象的方法就是一个CMutex类型的变量,将其作为你希望控制类的数据成员。在被控制对象的构造过程中,若哑程最初拥有了哑程的名称或期待的安全属性,那么就调用CMutex数据成员指定的构造函数。以这种方式访问由CMutex 对象控制的资源,首先要在资源访问的成员函数中创建CSingleLock类型或CMultiLock类型的变量。然后调用封锁对象的Lock成员函数(例如, CSingleLock::Lock)。这样,你的线程要么就获得资源的访问权,以等待将要释放的资源,并获取访问权,要么就等待将要释放的资源,当超时后,返回失败。在任何一种情况下,都可以在线程安全的模式下访问资源。若要释放这些资源,使用封锁对象的Unlock成员函数(例如, CSingleLock::Unlock),或允许封锁对象越界。
    CMutex的构造函数:

[cpp]  view plain copy
  1. CMutex(  
  2.    BOOL bInitiallyOwn = FALSE,  
  3.    LPCTSTR lpszName = NULL,  
  4.    LPSECURITY_ATTRIBUTES lpsaAttribute = NULL   
  5. );  

    1)bInitiallyOwn:如果线程创建CMutex对象最初就通过mutex控制访问资源,就指定该值
    2)lpszName :CMutex的名字。如果该对象用于进程间处理,必须指定该名字,如果为NULL,则该对象是未命名的。如果已存在另一个同名的mutex,构造器将引用那个已存在的对象创建新的CMutex对象,如果已存在另一个同名但非mutex对象的同步类,构造将失败。
    3)lpsaAttribute:安全属性,通常为NULL

7. CCriticalSection

    类CCriticalSection的对象表示一个“临界区”,它是一个同步对象,同一时刻只允许一个线程访问资源或代码区。临界区用于控制一次只有一个线程修改数据或其它的控制资源。例如,在链表中增加一个结点就只允许一次一个线程进行。通过使用CCriticalSection对象来控制链表,就可以达到这个目的。
    CCriticalSection对象的功能由当前的Win32对象--CRITICAL_SECTION提供。
    在运行性能比较重要而且资源不会跨进程使用时,建议采用临界区代替Mutex。使用CCriticalSection对象之前,需要构造它。在构造函数返回后,就可以使用临界区了。在使用完之后要调用UnLock函数。
    存取由CCriticalSection控制的资源时,要在资源的存取函数中定义一个CSingleLock型的变量。然后调用加锁对象的Lock成员函数(如CSingleLock::Lock)。此时,调用的线程要么获得对资源的存取权,要么等待他人释放资源等待加锁,或者等待他人释放资源,但又因为超时而加锁失败。这样就保证了一次只有一个线程在存取临界资源。释放资源只需调用成员函数UnLock(例如CSingleLock:Unlock),或让锁对象在作用范围之外。
 此外,可以单独地建立一个CCriticalSection对象,并在存取临界资源之前显式地存取它。这种方式有助于保持代码的清晰,但是更容易出错,因为程序员要记住在存取临界资源前加锁,存取之后开锁。

8. CEvent

    CEvent类对象,表示一个“事件”——一个允许一个事件发生时线程通知另一个线程的同步对象。在一个线程需要了解何时执行任务时,事件是十分有用的。例如,拷贝数据到数据文档时,线程应被通知何时数据是可用的。当新数据可用时,通过运用CEvent对象来通知拷贝线程,线程才可能尽快地执行。 
    CEvent对象有两种类型:自动和手工。一个手工CEvent对象存在于由ResetEvent 或SetEvent设置的状态中,直到另一个函数被调用。一个自动CEvent对象在至少一个线程被释放后自动返回一个无标记(无用的)状态。
    要使用一个CEvent对象,应在需要时构造一个CEvent对象。指定要等待的事件的名字,最初拥有它的进程,就可以在构造函数返回时访问事件。调用SetEvent标记(使可用)事件对象,然后当访问完控制资源时,调用Unlock函数。
    另一个使用CEvent对象的方法是添加一个CEvent类型的变量,使之成为希望控制的类的一个数据成员。在控制对象被构造期间,可调用CEvent数据成员的构造函数,它指明时间是否是最初就被标记、需要的事件对象类型、事件名称(如果在进程中要使用)和所希望的安全属性。 
    按以下方式访问一个被CEvent对象控制的资源:首先创建在资源访问成员函数中构造一个CSingleLock或CMultiLock类型的变量,然后调用封锁对象的Lock成员函数(如CMultiLock::Lock)。此时,线程要么可以访问资源,等待资源释放后访问;要么等待资源释放而超时,访问资源失败。在各种情况下,资源都被以线程安全方式访问。要释放资源,可调用SetEvent来标识一个事件对象,然后使用封锁对象的Unlock成员函数(如CMultiLock::UnLock),或允许封锁对象超过范围。

9. CSingleLock

    它代表用于多线程程序中资源访问的控制机制。
    为了使用同步对象,必须创建同步访问类对象CSingleLock或CMultiLock 等待或释放该同步对象。当只需要同一时间等待一个同步对象,使用CSingleLock;当某一特定时刻需要等待多个同步对象,使用CMultiLock。
    要使用CSingleLock,在资源访问类得成员函数中构造该对象,然后调用该对象的成员函数IsLocked检测资源是否可用,如果是,继续执行资源访问类的该成员函数的剩余部分,如果资源不可用,等待特定是时间直到资源访问控制被释放或者访问资源失败。当资源访问完成时,调用Unlock释放资源控制权,或使构造的CSingleLock对象析构。

10. CMultiLock

    它代表用于多线程程序中资源访问的控制机制。
    为了使用同步对象,必须创建同步访问类对象CSingleLock或CMultiLock 等待或释放该同步对象。当只需要同一时间等待一个同步对象,使用CSingleLock;当某一特定时刻需要等待多个同步对象,使用CMultiLock。
    要使用CMultiLock,首先创建你想要等待的同步对象数组,接着,在再资源访问类的成员函数中构造CMultiLock对象,然后,调用CMultiLock的Lock 检测资源是否有效(有信号)。如果有资源有效,继续资源访问,如果没有资源有效,等待资源访问控制权释放或访问资源失败。访问资源完成后,调用Unlock 释放资源控制权,或使CMultiLock超出作用域而析构。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值