MFC中线程相关知识
MFC中把线程分为两种类型,UI线程和工作者线程。
MFC中启动一个线程的最好方法是调用AfxBeginThread,有两个版本,一个用于启动Ui线程,另外一个用于启动工作者线程。在MFC程序中,只有在线程不使用MFC库时,才可以使用Win32的CreateThread函数来创建线程。AfxBeginThread不仅仅是对CreateThread函数的封装,它还会初始化主结构使用的内部状态信息,在不同的地方执行合理的检查,确保以线程安全的方式访问运行时库中的函数。
工作线程的创建
调用AfxBeginThread函数,它将创建一个新的CwinThread对象,启动一个线程并返回一个CwinThread对象,该对象拥有这个线程。
CwinThread * pThread = AfxBeginThread(ThreadFunc, &threadInfo);
ThreadFunc(LPVOID pParam)
{}
ThreadFunc是线程函数,threadInfo是用于包含线程的输入。
CWinThread* AfxBeginThread(
AFX_THREADPROC pfnThreadProc,
LPVOID pParam,
int nPriority = THREAD_PRIORITY_NORMAL,
UINT nStackSize = 0,
DWORD dwCreateFlags = 0,
LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL
);
nPriority:指定线程的执行优先级别,是相对于该线程所属的进程优先级别而定的。
nStackSize:指定了线程最大的堆栈的尺寸,默认值0允许堆栈增加到1MB大小,限制1MB,几乎对所有的应用程序都比较适合。
dwCreateFlags:默认值0告诉系统立即开始执行线程,如果指定了CREATE_SUSPENDED,则线程开始时处于暂停状态,直到调用者执行pThread->ResumeThread()函数。
lpSecurityAttrs:该结构指定了新线程的安全属性,默认值NULL意味着新线程与创建它的线程具有相同的属性。
线程函数是回调函数,因此它必须是静态函数或是在类外部声明的全局函数,
UNIT ThreadFunc(LPVOID pParam);
pParam是一个32位值,等于传递给AfxBeginThread的pParam。
UI线程的创建
工作者线程是由其线程函数定义的,而UI线程的行为却是由CwinThread派生来的可动态创建类控制的,该类与CwinApp派生的应用程序类很类似。
下面给出的UI线程类创建一个框架窗口,并调用AfxBeginThread函数启动CUIThread。
//声明一个CwinThread类
class CUIThread : public CWinThread
{
DECLARE_DYNCREATE(CUIThread)
protected:
CUIThread(); // 动态创建所使用的受保护的构造函数
virtual ~CUIThread();
public:
virtual BOOL InitInstance();
virtual int ExitInstance();
protected:
DECLARE_MESSAGE_MAP()
};
BOOL CUIThread::InitInstance()
{
// TODO: 在此执行任意逐线程初始化
m_pMainWnd = new MyWindow();
m_pMainWnd->ShowWindow(SW_SHOW);
m_pMainWnd->UpdateWindow();
return TRUE;
}
//window class
class MyWindow : public CFrameWnd
{
DECLARE_DYNCREATE(MyWindow)
public:
MyWindow(); // 动态创建所使用的受保护的构造函数
virtual ~MyWindow();
protected:
DECLARE_MESSAGE_MAP()
};
IMPLEMENT_DYNCREATE(MyWindow, CFrameWnd)
MyWindow::MyWindow(){
Create(NULL, _T("UI Thread Window"));
}
MyWindow::~MyWindow()
{}
BEGIN_MESSAGE_MAP(MyWindow, CFrameWnd)
END_MESSAGE_MAP()
通过AfxBeginThread(RUNTIME_CLASS(CUIThread));函数,启动UI线程。AfxBeginThread的UI线程版本接收与工作线程版本中相同的4个可选参数,但是不接收pParam参数,一旦启动,UI线程就会与创建它的线程异步运行。
线程的其他操作和信息
调用CwinThread::SuspendThread可以让运行的线程挂起,ResumeThread则让挂起的线程继续执行。对于每个线程,系统都维持一个“计数器”,suspendThread加1,resumeThread减1,只有计数器为0时,才会给线程调度处理时间。
API函数::Sleep可以让当前线程挂起指定的时间,::sleep还可以用来放弃剩余的线程时间片。
::sleep(0);暂停当前线程并允许调度程序运行其他相同或更高的优先级别的线程,如果没有优先级相同或者更高的线程处于等待状态,函数调用会立即返回并继续执行当前的线程。注意:sleep指定的挂起时间并不精确。
当工作线程的线程函数执行到return,或者线程中调用AfxEndThread函数时,工作者线程结束。当给消息队列发送WM_QUIT或是调用了AfxEndThread时,UI线程就会结束。AfxEndThread、::PostQuitmessage、return都接收一个 32位的出口代码,可以使用::GetExitCodeThread检索得到线程的退出码。
MFC会在线程结束后通过指针调用delete,CwinThread的析构函数会使用::closeHandleAPI函数来关闭线程句柄。线程句柄必须以显示的方式被关闭,因为即使与句柄相关的线程终止了,但是句柄仍然打开着。它们必须保持为打开状态,否则::GetExitCodeThread函数就不能正常工作了。所以当CwinThread结束后,再调用GetExitCodeThread函数获得退出码,这是很危险的事情,因为这时候的线程句柄已经被关闭。
我们可以CWinThread对象的m_bAutoDelete数据成员为FALSE可防止MFC删除CwinThread对象。默认值是TRUE允许自动删除。这时我们需要手动调用delete。这时创建线程时,需要使它处于暂停状态。
MFC中关于跨线程界限调用MFC成员函数的问题
如果线程A给线程B传递了一个CWnd指针,而线程B要调用CWnd对象的成员函数,那么MFC在调试状态下可能就会出现断言错误。
首先,我们可能有这样的错觉,很多MFC成员函数都能够在其他线程创建的对象中得到使用。那是因为这些函数大多数是内联函数,它们仅仅是API函数的简单封装。
例如:GetParent函数的原型
AFXWIN_INLINE CWnd* CWnd::GetParent()const{
ASSERT(::IsWindow(m_hWnd));
Return CWnd::Fromhandle(::GetParent(m_hWnd));
}
这里的m_hWnd毫无疑问是有效,因为m_hWnd是CWnd的成员对象,但是我们如果调用GetParentFrame函数,则会出现断言错误,造成错误的一条语句是:
ASSERT_VALID(this);该语句会执行有效的检查,来确保与this关联的HWND出现在了主结构用来将HWND转换为CWnd的映射表中,该表仅对本线程可见,因此其他线程创建的CWnd对象和他的映射表在本线程中是查不到的,所以会出现断言的错误。当然我们可以人为的给映射表添加上我们新增的HWND。我们将句柄通过线程传入,在线程函数中使用,FromHandle((HWND)pParam),这样就可以了。这就是为什么窗口、GDI对象、其他线程对象应尽量使用句柄而不是指针在线程中传递的原因。
那些没有封装HWND、HDC或其他句柄类型的类,也无法确保其正常工作。总之,在实际工作中,多线程MFC程序趋向于将大量的用户界面工作交给主线程来执行,后台线程想要更新用户界面,将消息发送给主线程,让主线程更新即可。
线程同步
Windows支持4中类型的同步对象,可以用来同步由并发运行的线程所执行的操作:
1 临界区
2 互斥量
3 事件
4 信号量
MFC在名为CcriticalSection、CMutex、CEvent和CSemaphone中的类封装了这些对象,除此以外还包括CSingleLock和CMultiLock类。
临界区
临界区是最简单的线程同步对象,它用来在同一个进程中,对两个或者多个线程共享资源进行串行化访问。基本思想是,每个独占性地访问一个资源的线程可以在访问那个资源之前锁定临界区,访问完成后解除锁定。如果有线程B试图锁定线程A锁定的临界区,那么线程B将阻塞知道该临界区空闲。使用CcriticalSection的lock函数可以锁定临界区,unlock函数则解除对临界区的锁定,如下展示了线程A和线程B如何串行化访问ListA的。
// Global data
CCriticalSection g_cs;
...
// Thread A
g_cs.Lock();
// write to List A
g_cs.UnLock();
// Thread B
g_cs.Lock();
// read from List A
g_cs.UnLock();
互斥量
和临界区一样,互斥量也是用来获得对两个或者多个线程建共享资源的独占性访问,但是Mutex的作用范围更大,不仅能同步同一进程内的线程,还能同步不同进程上的线程,因此开销比使用临界区大。通常情况下,同一进程内的同步我们使用临界区,而不同进程间的线程同步我们使用互斥量。
假定两个应用程序使用一块共享内存来交换数据,则该共享数据内部必须防止并发线程访问。
// Global data
CMutex g_mutex(FALSE, _T("MyMutex"));
...
g_mutex.Lock();
//Read or Wrtie the data
g_mutex.UnLock();
传递给CMutex构造函数的第一个参数指定互斥量的初始状态是锁定(TRUE)还是没有锁定(FALSE),第二个参数用来指定互斥量的名称,如果你要同步不同进程间的线程,请确保互斥量的名称相同,这样才能引用Windows内核中相同的互斥量对象。
此外,如果一个线程锁定了临界区,当线程结束时,如果没有释放临界区,则该临界区将永远被锁定,其他想访问该临界区的线程只能无限期阻塞。但是,如果一个线程获得了锁之后,线程结束时没有释放锁,那么系统将人为该锁被抛弃了,释放该锁,这样其他等待线程将得到该锁,并继续执行。
事件
MFC的CEvent类封装了Win32事件对象,一个事件不只是操作系统内核中的一个标记,在特定的事件,事件只有两个状态中的一种,要么是处于信号状态(设置)要么就是非信号状态(重置)。
Windows支持两种不同类型的事件:自动重置事件和手动重置事件。当在自动重置事件上阻塞的线程被唤醒时,该事件被自动重置为非信号状态。而手动重置事件不能自动重置,它必须使用编程的方式重置。一般自动重置事件和手动重置事件的一般使用规则如下:
如果事件只触发一个线程,那么使用自动重置事件和使用SetEvent来唤醒等待线程即可,这里我们不需要使用ResetEvent函数,因为线程被唤醒的那一刻,事件将被自动重置。
如果事件将要触发两个或者多个线程,那么使用手动重置事件和PulseEvent唤醒所有的等待线程,也无需使用ResetEvent,因为PulseEvent在唤醒线程后将为您重置事件。
PulseEvent函数不仅能够设置和重置事件,还确保了所有在事件上等待的线程在重置事件之前被唤醒,而SetEvent和ResetEvent显然无法保证能够做到这一点。
CEvent类构造函数原型:
CEvent(
BOOL bInitiallyOwn = FALSE,
BOOL bManualReset = FALSE,
LPCTSTR lpszName = NULL,
LPSECURITY_ATTRIBUTES lpsaAttribute = NULL);
bInitiallyOwn:指定对象被初始化为信号状态(True)还是无信号状态(False)
bManualReset:指定对象是手动重置事件(True)还是自动重置事件(False)
lpszName:指定了事件对象的名称,和mutex一样,如果事件用来同步不同进程间的线程,必须给同步事件指定一个名字。如果事件是用来同步同一个进程内的线程的,则该值应该指定为NULL。
lpsaAttribute:描述事件对象的安全属性,NULL表示接受默认值,适用于大部分应用程序。
事件对象使用Lock函数来阻塞当前线程,等待事件被Set。
注意:对于自动重置事件对象来说,只要该对象被使用,就会重置。比如此时一个自动重置事件对象g_event有信号状态,调用检查事件的函数::WaitForSingleObject(g_event.m_hObject,0),
这时,事件的状态会被自动重置。
信号量
临界区、互斥量和事件,具有这样的共性,只能表示两种状态,但是信号量不同,它始终保存有代表可用资源数量的资源数。锁定信号量会减少资源数,释放信号量会增加资源数,只有线程试图锁定资源数为0的信号量时,线程才会被阻塞。直到其他线程释放信号量,资源数增加或者超时时间期满,该线程才被释放。信号量可以用来同步化同一进程中的线程也可以同步化不同进程中的线程。Csemaphore的构造函数如下:
CSemaphore(
LONG lInitialCount = 1,
LONG lMaxCount = 1,
LPCTSTR pstrName = NULL,
LPSECURITY_ATTRIBUTES lpsaAttributes = NULL);
lInitialCount:初始可访问的资源数
lMaxCount:最大可访问资源数
pstrName和lpsaAttributes同事件一致。
我们使用Lock函数锁定资源,使用Unlock函数释放资源。Unlock函数等价的API函数为::ReleaseSemaphore函数。信号量的传统用法是,它允许一组线程(m个)共享n个资源,其中m比n大。
CsingLock和CmultiLock类
可以使用CsingLock对象来包装临界区、互斥对象、事件和信号量。那为什么需要去额外的包装这么一层呢?考虑如下代码,如果调用lock和unlock直接发生异常时,那么临界区将用于被锁定。
CcriticalSection g_cs;//全局变量
g_cs.Lock();
......
g_cs.UnLock();
但是如果我们使用CsingleLock的话,就不存在这个问题了。
CsingleLock lock(&g_cs);
lock.Lock();
......
lock.UnLock();
因为CsingleLock对象在堆栈上创建时,如果异常就会调用它的析构函数,析构函数会调用Unlock函数。
CmultiLock函数则完全不同,通过使用CmultiLock函数,一个线程可以一次阻塞最多64个同步化对象,并且根据Lock方式的不同,按不同条件从阻塞状态中返回。注意:只有事件、互斥对象和信号量可以封装在CmultiLock对象中,而临界区不行。
Cmutex g_mutex;
CEvent g_event[2];
CsyncObject * g_pObjects[3] = {&g_mutex, &g_event[0], &g_event[1]};
....
CmultiLock multiLock(g_pObject,3);
multiLock.Lock();//当前线程阻塞,直到三个对象都有信号状态或者释放锁为止。
....
multiLock.Lock(INFINITE, FALSE); //当前线程阻塞,直到三个对象中有一个都有信号状态或者释放锁为止。
Lock函数原型:
DWORD Lock(
DWORD dwTimeOut = INFINITE,
BOOL bWaitForAll = TRUE,
DWORD dwWakeMask = 0);
dwTimeOut:指定等待时间,默认为无限期等待。
bWaitForAll:指定是否等待所有同步对象解锁(TRUE)还是只要有一个解锁(FALSE)。
dwWakeMask:掩码,指定唤醒线程的其他条件,例如WM_PAINT或是鼠标消息等。
关于Windows中多任务和多线程的小知识点
关于进程的几个API函数:
CreateProcess:创建进程
GetExitCodeProcess:获得进程的退出码
WaitForInputIdle函数:等到创建的进程窗口开始处理消息并清空它的消息队列。
WaitForMultipleObject:该函数是Win32中CmulitLock::Lock的等价函数,能够监视多个对象。
posted on 2018-04-14 16:04 merlinzjl 阅读(3156) 评论(0) 编辑 收藏 举报