首页 理论教育 线程同步实例-VisualC++高级编程技术

线程同步实例-VisualC++高级编程技术

时间:2023-11-16 理论教育 版权反馈
【摘要】:使隶属于同一个进程的各线程协调一致地工作称之为线程的同步。通过这些类,我们可以比较容易地做到线程同步。事件是一个允许一个线程在某种情况发生时,唤醒另外一个线程的同步对象。线程监视位于其中的CEvent类对象的状态,并在相应的时候采取相应的操作。对于自动事件,当其获得信号后,就会释放下一个可用的线程。若为0,则该函数立即返回;若为INFINITE,则线程一直被挂起,直到hHandle所指向的对象变为有信号状态时为止。

线程同步实例-VisualC++高级编程技术

虽然线程能给我们带来好处,但是也有不少问题需要解决。例如,对于像磁盘驱动器这样独占性系统资源,由于线程可以执行进程的任何代码段,且线程的运行是由系统调度程序自动完成的,具有一定的不确定性,因此就有可能出现两个线程同时对磁盘驱动器进行操作,从而出现操作错误;又例如,对于银行系统的计算机来说,可能使用一个线程来更新其用户数据库,而用另外一个线程来读取数据库以响应储户的需要,结果会如何呢?极有可能读数据库的线程读取的是未完全更新的数据库,因为可能在读的时候只有一部分数据被更新过。

使隶属于同一个进程的各线程协调一致地工作称之为线程的同步。MFC提供了多种同步对象,如CEvent、CCriticalSection、CSemaphore、CMutex等。另外,MFC也提供了线程同步辅助类CSingleLock和CMutiLock。通过这些类,我们可以比较容易地做到线程同步。

1. 使用CEvent类

CEvent类提供了对事件的支持。事件是一个允许一个线程在某种情况发生时,唤醒另外一个线程的同步对象。事件告诉线程何时去执行某一给定的任务,从而使多个线程流平滑。例如在某些网络应用程序中,一个线程(记为A)负责监听通信端口,另一个线程(记为B)负责更新用户数据。通过使用CEvent类,线程A可以通知线程B何时更新用户数据,这样线程B可以尽快地更新用户数据。每一个CEvent对象可以有两种状态:有信号状态(Signaled)和无信号状态(NonSignaled)。线程监视位于其中的CEvent类对象的状态,并在相应的时候采取相应的操作。

在MFC中,CEvent类对象有两种类型,分别是所谓的人工事件和自动事件。对于自动事件,当其获得信号后,就会释放下一个可用的线程。一个自动CEvent对象在被至少一个线程释放后会自动返回到无信号状态;而人工事件对象获得信号后,释放所有可利用线程,直到调用成员函数ResetEvent()将其设置为无信号状态时为止。注意,在创建CEvent类的对象时,默认创建的是自动事件。

CEvent的各成员函数的原型与参数说明如下:

bInitiallyOwn:若bInitiallyOwn为TRUE,则使CMultilock类对象和CSingleLock类对象的线程可用;否则,要访问资源的线程必须等待。该参数的默认值为FALSE。

bManualReset:指定要创建的CEvent对象是属于手工事件还是自动事件。为TRUE,则为手工事件,否则为自动事件。该参数默认值为FALSE。

lpszName:指定要创建的事件对象的名,如果该事件对象将跨进程使用,则此参数不能为NULL。如果该参数和一个已经存在的CEvent对象相同,则该构造函数返回一个对这个已存在对象的引用;如果参数和一个已存在的非CEvent类的同步对象(如CMutex)相同,则对象创建失败;

lpsaAttribute:指向SECURITY_ATTRIBUTES结构的指针,该参数决定要创建的事件对象的安全属性,一般置为NULL。

在事件对象建成后,可以调用其成员函数来改变其状态。

BOOL CEvent::SetEvent ();

将CEvent类对象的状态设置为有信号状态,并且释放所有等待的线程;如果该事件是人工事件,则CEvent类对象保持为有信号状态,直到调用成员函数ResetEvent()将其重新设为无信号状态时为止,这样该事件就可以释放多个线程;如果CEvent类对象为自动事件,则在SetEvent()将事件设置为有信号状态后,CEvent类对象由系统自动重置为无信号状态,除非一个线程被释放。

如果该函数执行成功,则返回非零值,否则返回零。

BOOL CEvent::ResetEvent();

该函数将事件的状态设置为无信号状态,并保持该状态直至SetEvent()被调用时为止。由于自动事件是由系统自动重置,故自动事件不需要调用该函数。

如果该函数执行成功,返回非零值,否则返回非零。

BOOL CEvent::PulseEvent();

发送一个事件脉冲,该函数完成一系列操作后才返回。对于自动事件,PulseEvent()将事件设置为有信号状态,等待一个线程被释放,将事件重置为无信号状态,然后PulseEvent()返回;对于人工事件,则将等待该事件的所有线程被释放,事件被自动重置为无信号状态,然后PulseEvent()返回。

一个CEvent对象在线程中被创建后,自动处于无信号状态,但在另一个线程中可以调用Win32 API WaitForSingleObject()函数来监视其状态。

该函数的原型及参数说明如下:

DWORD WaitForSingleObject(HANDLE hHandle, DWORD dwMilliseconds);

其中hHandle为指向要监视的同步对象的句柄,dwMilliseconds为监视hHandle所指向的对象所设置的超时值,单位为毫秒。当在线程的执行函数中调用该函数时,线程暂时挂起,系统监视hHandle所指向的对象的状态。如果经过dwMilliseconds毫秒后,hHandle指向的对象变为有信号状态,则WaitForSingleObject()返回,线程被释放,且返回值为WAIT_TIMEOUT;如果在挂起的dwMilliseconds毫秒内,线程所等待的对象在某一时刻变为有信号,则该函数立即返回,返回值为WAIT_OBJECT_0。参数dwMilliseconds有两个具有特殊意义的值:0和INFINITE。若为0,则该函数立即返回;若为INFINITE,则线程一直被挂起,直到hHandle所指向的对象变为有信号状态时为止。

下面我们将以自动事件为例,编写一个用CEvent类来进行线程同步的简单例子,步骤如下:

(1)运行AppWizard创建一个基于对话框的应用程序MultiThreadC,并接受其默认设置,对话框的界面设计如图5-12所示。

对话框中各控件的相关属性设置见表5-3。

表5-3 对话框中控件的设置

(2)用资源编辑器为该应用程序添加如图5-13所示的两个图标资源,它们的ID分别为IDI_CREATED和IDI_NOTCREATED。

图5-12 对话框界面

图5-13 图标资源

(3)在StdAfx.h文件内加入afxmt.h头文件,如程序清单5-11所示。

程序清单5-11 StdAfx.h文件

(4)在MultiThreadCDlg.h内的MultiThreadCDlg类中添加布尔型变量 m_bStopped、m_bBegin和m_bCreated,它们的访问权限为公有的。添加HICON类型变量m_hCreated和m_hNotCreated,分别用于标记线程的状态,它们的访问权限为保护的。

(5)修改CMultiThreadCDlg类的构造函数如程序清单5-12所示。

程序清单5-12 CMultiThreadCDlg类的构造函数

在MultiThreadCDlg类 的OnInitDialog()中添加如程序清单5-13所示的语句。

程序清单5-13 OnInitDialog函数

在文件MultiThreadCDlg.cpp上部添加如程序清单5-14所示的代码。

程序清单5-14 CMultiThreadCDlg.cpp文件中添加的代码

在CMultiThreadCDlg类中添加自定义消息WM_USER_ENDED的处理函数OnThreadEnded();在MultiThreadCDlg.h中的由ClassWizard所维护的消息映射处理函数外面添加,如程序清单5-15所示。

程序清单5-15 MultiThreadCDlg.h文件中的修改

在MultiThreadCDlg.cpp文件中添加自定义消息WM_USER_ENDED与处理函数OnThreadEnded()的映射,并添加函数的实现,如程序清单5-16所示。

程序清单5-16 MultiThreadCDlg.cpp文件中的修改

使用ClassWizard在MultiThreadCDlg类中添加四个按钮的消息处理函数,代码如程序清单5-17所示。

程序清单5-17 按钮的消息响应函数

编译并运行上述程序。依次单击“创建线程”按钮来创建线程并进入循环,接着按下“开始”按钮,线程开始执行用户的操作。这里我们使用Sleep()函数进行模拟,如图5-14所示。

接着按下“停止”按钮,用户定义的操作被终止,执行结果如图5-15所示。

图5-14 运行程序

图5-15 执行结果

按下“结束线程”按钮则终结线程。(www.xing528.com)

本程序使用了三个事件对象,eventKill、eventBegein和eventStop,并且给WaitForSingleOject函数中的第二个参数都用了0, 而没有使用INFINITE。ThreadProc线程过程中的判断结构比较复杂;三个布尔值的状态要注意,在OnInitDialog函数中初始化成FALSE,在程序的运行当中,都要使用这三个布尔值。本程序提供了一个很好的框架读者只要把Sleep函数替换成自定义的过程就可以了。

2. 使用CCriticalSection类

当多个线程访问一个独占性共享资源时,可以使用“临界区”(即Critical Section)对象。任一时刻只有一个线程可以拥有临界区对象,拥有临界区的线程可以访问被保护起来的资源或代码段,其它希望进入临界区的线程将被挂起等待,直到拥有临界区的线程放弃临界区时为止,这样就保证了不会在同一时刻出现多个线程访问共享资源。例如,假设有两个线程A和B都读写某一数据结构,如果在同一时刻出现两个线程同时访问该数据,则可能会出现所谓的“数据污染”。由于线程调度是由系统自动完成的,不能确定何时线程被调入CPU中运行,故可能出现线程A正在更新数据(且尚未完成)时,线程B被调度程序装入CPU中运行,并且更改了线程A尚未完成更新的数据。如果我们用CCriticalSection类的对象将该数据保护起来,则任一时刻只有一个线程可以读写该数据,这样就不会出现上述问题。

CCriticalSection类的用法也相当简单,有两种用法。

方法一:单独使用CCriticalSection对象,步骤如下:

(1)定义CCriticalSection类的一个全局对象(以使各个线程均能访问),如:

CCriticalSection critical_section;

CCriticalSection类的构造函数只有一种形式,即不带任何参数,如上述代码所示。

(2)在访问需要保护的资源或代码之前,调用CCriticalSection类的成员Lock()获得临界区对象。

critical_section.Lock();

其中Lock()的原型如下:

BOOL CCriticalSection::Lock ();

在线程中调用该函数来使线程获得它所请求的临界区。如果此时没有其它线程占有临界区对象,则调用Lock() 的线程获得临界区;否则,线程将挂起,并放入到一个系统队列中等待,直到当前拥有临界区的线程释放了临界区时为止。

(3)访问临界区完毕后,使用CCriticalSection的成员函数Unlock()来释放临界区。

critical_section.Unlock ();

方法二:与同步辅助类CSingleLock或CMutiLock类一起使用,步骤如下(以类CSingleLock为例):

(1)定义CCriticalSection类的一个全局变量critical_section:

CCriticalSection critical_section;

(2)在访问需要保护的资源之前,定义CSingleLock类的一个变量,并将critical_section的地址传送给构造函数CSingleLock singlelock (&critical_section);使用CSingleLock类的成员函数Lock()并请求获得临界区。

其中,CSingleLock类的成员函数Lock()原型如下:

BOOL CSingleLock::Lock (DWORD dwTimeOut = INFINITE);

该函数替它所在的线程申请获得临界区。如果临界区已经被其它线程占用,则本线程挂起,等待临界区被释放。获得临界区(返回TRUE)或等待时间超过dwTimeOut0毫秒(返回FALSE)后均返回。

(3)在本线程中访问临界区中的共享资源。

(4)调用CSingleLock类的成员函数singlelock. Unlock ()来释放临界区。

下面给出一个简单的实例MultiThreadE,线程的执行函数显示一个消息框,标题指出了当前拥有临界区的线程。下面的程序指出,由于在同一时刻只有一个线程可以访问临界区的资源,故任一时刻只弹出一个消息框。该程序的具体编写步骤如下:

(1)用AppWizard生成一个基于对话框的名为MultiThreadE的MFC应用程序。

(2)在对话框中增加两个按钮,分别为“Thread 1”和“Thread 2”。

(3)在StdAfx.h文件中添加包含语句:#include <afxmt.h>,在CMultiThreadEDlg类的实现文件MultiThreadEDlg.cpp的文件中添加全局的临界区对象和线程的执行函数,如程序清单5-18所示。

程序清单5-18 临界区对象和线程的执行函数

(4)为上述两个按钮添加消息处理函数如程序清单5-19所示。

程序清单5-19 按钮的消息处理函数

(5)编译上述程序,并执行。当单击了两个按钮时,只弹出一个消息框,可见另一个线程被挂起。当关闭该消息框时,马上又弹出一个消息框,这表明第二个线程获得了临界区对象。总之,在同一时刻,只能弹出一个消息框,也就是在同一时刻,只有一个线程可以访问被临界区保护起来的资源或代码。

3. 使用CMutex类

互斥对象与临界区对象很像。互斥对象源于“mutual exclusion”的组合。临界区与互斥对象的不同在于:互斥对象可以在进程间使用,而临界区对象只能在同一进程的各线程间使用。当然,互斥对象也可以用于同一个进程的各个线程间,但是在这种情况下,使用临界区会更节省系统资源,更有效率

CMutex的使用只能用类似于CCriticalSection用法中的第二种用法,在此不再列出。

4. 使用CSemaphore类

当需要一个计数器来限制可以使用某个资源的线程数目时,可以使用“信号量”对象(即CSemaphore类的对象)。CSemaphore的一个对象保存了对当前访问某一指定资源的线程的计数值,该计数值是当前还可以使用该资源的线程的数目。如果这个计数达到了零,则所有对这个CSemaphore类对象所控制的资源的访问尝试都被放入到一个队列中等待,直到超时或计数值不为零时为止。一个线程被释放以访问了被保护的资源时,计数值减1;一个线程完成了对被控共享资源的访问时,计数值增1。这个被CSemaphore类对象所控制的资源可以同时接受访问的最大线程数在该对象的构建函数中指定。

CSemaphore类的构造函数原型及参数说明如下。

lInitialCount:信号量对象的初始计数值,决定了在信号量对象建成后,能同时访问其中资源的最大线程数目。LInitialCount必须不小于零,不大于lMaxCount。

lMaxCount:信号量对象计数值的最大值,该参数决定了同一时刻可访问由信号量保护的资源的线程最大数目。pstrName:指定要创建的信号量名字。如果该信号量跨进程使用,则该参数不能为空。如果名为pstrName的信号量对象已经存在,构造函数返回一个已存在对象的引用。如果存在名为pstrName的其它线程同步类,则构造函数调用失败。

lpsaAttributes:指向信号量对象的安全属性的指针,一般置为NULL。

CSemaphore类一般也与线程同步辅助类CSingleLock或CMutiLock类结合使用。其用法与CCriticalSection类似,在此不再赘述。

下面给出一个简单实例MultiThreadD来说明CSemaphore类的用法,步骤如下:

(1)用AppWizard建立一个基于对话框的应用程序,并接受默认设置。

(2)将对话框资源布局设置成如图5-16所示的形式,其中按钮“新建线程”的ID设为IDC_NEWHTREAD。

(3)在CMultiThreadDDlg类的实现文件CMultiThreadDDlg.cpp头部的其它包含语句下方添加下列包含语句:

#include <afxmt.h>

在该文件中的消息映射后面添加定义语句:

图5-16 对话框资源布局

CSemaphore semaphore (2,2);

该语句将信号量对象semaphore的初始计数值设置为2,最大计数值也设为2,并插入进程的执行函数ThreadProc()。

UINT ThreadProc (LPVOID lpParam)

注意:在上面的线程执行函数中,由于singlelock是在函数中定义的,故函数执行完毕后,将被自动销毁,并在其析构函数中调用CSingleLock::Unlock ()成员函数来释放其占用的信号量对象,所以我们在此没有显式地调用Unlock()成员函数。

(4)双击“新建线程”按钮,为其添加消息响应函数OnNewThread(),代码如下:

(5)编译并运行上述程序,按两下“新建线程”按钮,弹出两个如图5-17所示的消息框。

根据该程序的编写,这表示有两个线程(记为A和B)正在访问受semaphore保护的代码。如果再按一下,虽然开了第三个线程(记为C),但不会弹出第三个对话框,因为我们在前面构造semaphore时计数值最大设为2。线程A和B正在访问受保护的代码,线程C则被挂起。这时,如果我们关掉一个消息框(例如对应线程A的对话框),马上又会弹出一个消息框(对应挂起的线程C),因为关掉线程A所对应的消息框后,线程A访问受信号量保护的代码完毕,信号量计数值增1,线程C执行。

图5-17 运行结果

免责声明:以上内容源自网络,版权归原作者所有,如有侵犯您的原创版权请告知,我们将尽快删除相关内容。

我要反馈