MFC学习笔记——多线程学习

一、多线程编程思想

1、 Windows线程

(1)在Windows平台上,从根本上可以利用CPU执行代码的最小实体就是线程

(2)首先从内核角度看,线程是一个内核对象,系统用它来存储一些关于线程的统计信息等(比如运行时间)

(3)其次从编程的角度看,线程是一堆寄存器状态以及线程栈的一个结构体对象,本质上可以理解为一个函数调用器(寄存器状态用于控制CPU执行,栈用于存储局部变量和函数调用参数及函数返回地址)

(4)最后需要知道的就是线程还可以带有几个队列(简单的理解为异步函数调用队列):

  •     消息队列(GUI线程系统内部会创建)
  •     APC队列(调用APC函数时会创建)
  •    (注意:这些队列在线程创建时并不存在)
2、对线程的理解

        要讲解线程,不得不说一下进程,进程是应用程序的执行实例,每个进程是由私有的虚拟地址空间、代码、数据和其它系统资源组成。进程在运行时创建的资源随着进程的终止而死亡。线程的基本思想很简单,它是一个独立的执行流,是进程内部的一个独立的执行单元,相当于一个子程序,它对应于Visual C++中的CwinThread类对象。单独一个执行程序运行时,缺省地包含的一个主线程,主线程以函数地址的形式出现,提供程序的启动点,如main()或WinMain()函数等。当主线程终止时,进程也随之终止。根据实际需要,应用程序可以分解成许多独立执行的线程,每个线程并行的运行在同一进程中。

        一个进程中的所有线程都在该进程的虚拟地址空间中,使用该进程的全局变量和系统资源。操作系统给每个线程分配不同的CPU时间片,在某一个时刻,CPU只执行一个时间片内的线程,多个时间片中的相应线程在CPU内轮流执行,由于每个时间片时间很短,所以对用户来说,仿佛各个线程在计算机中是并行处理的。操作系统是根据线程的优先级来安排CPU的时间,优先级高的线程优先运行,优先级低的线程则继续等待。

        线程被分为两种:用户界面线程和工作线程(又称为后台线程)。用户界面线程通常用来处理用户的输入并响应各种事件和消息,其实,应用程序的主执行线程CWinAPP对象就是一个用户界面线程,当应用程序启动时自动创建和启动,同样它的终止也意味着该程序的结束,进程终止。工作线程用来执行程序的后台处理任务,比如计算、调度、对串口的读写操作等,它和用户界面线程的区别是它不用从CWinThread类派生来创建,对它来说最重要的是如何实现工作线程任务的运行控制函数。工作线程和用户界面线程启动时要调用同一个函数的不同版本;最后需要读者明白的是,一个进程中的所有线程共享它们父进程的变量,但同时每个线程可以拥有自己的变量。


3、什么时候使用线程?

    (1)当一个算法本身是严格串行化的时候,也即计算的每一步都严重依赖前一个操作步骤的结果时,这种算法一般比较难改为并行化的,因此也不适合应用于多线程(例外情况是,针对不同的初始参数,可以利用多线程运行同一个算法来得到各自的结果)

    (2)当有多个功能任务也具有比较严格的先后逻辑关系时,不宜采用多线程,此时,若使用多线程,并且试图让每个线程处理不同的任务时,为了照顾先后的逻辑顺序,则必须要使用线程同步方法严格控制几个线程的执行顺序,最终其实跟用一个线程来顺序执行这些功能逻辑几乎没什么区别,而且多线程加同步控制还降低了效率(这是很多初学者应用多线程时常犯的错误)。

    (3)还有一种特殊的情况,比如一个服务器需要处理成千上万个客户端链接,并处理不同的请求时,这种情况下应当优先考虑线程池,而不是简单的多线程(很多程序员喜欢为每个客户端链接创建一个线程,殊不知这样不但资源浪费严重、而且性能十分低下)


4、什么是主线程?

谈到多线程,就不得不说说主线程与子线程的关系:

(1)其实从本质上讲进程的入口函数其实就是主线程的入口函数;

(2)主线程是进程内第一个可执行的线程实体,它可以用来创建别的子线程;

(3)一般主线程退出后,进程也会退出(因为VS嵌入入口在主线程返回后会调用ExitProcess终止其它线程的执行,所以最终主线程也是进程退出的最后一个线程,当使用自定义的入口时,这个行为就要在自定义入口即自定义主线程中自行来维护);

(4)一般情况下,如果使用了自定义入口,那么进程将在最后一个线程退出后,才退出,这是Win32本身的进程退出行为;


二、线程的管理和操作

1、 线程的启动

  创建一个用户界面线程,首先要从类CwinThread产生一个派生类,同时必须使用DECLARE_DYNCREATE和IMPLEMENT_DYNCREATE来声明和实现这个CwinThread派生类。第二步是根据需要重载该派生类的一些成员函数,如: ExitInstance()、InitInstance()、OnIdle()、PreTranslateMessage()等函数。最后调用AfxBeginThread()函数的一个版本:

CWinThread* AfxBeginThread( CRuntimeClass* pThreadClass, intnPriority = THREAD_PRIORITY_NORMAL,

UINT nStackSize = 0, DWORD dwCreateFlags = 0,LPSECURITY_ATTRIBUTES  lpSecurityAttrs = NULL ) 

        启动该用户界面线程,其中第一个参数为指向定义的用户界面线程类指针变量,第二个参数为线程的优先级,第三个参数为线程所对应的堆栈大小,第四个参数为线程创建时的附加标志,缺省为正常状态,如为CREATE_SUSPENDED则线程启动后为挂起状态。

        对于工作线程来说,启动一个线程,首先需要编写一个希望与应用程序的其余部分并行运行的函数如Function1(),接着定义一个指向CwinThread对象的指针变量*pThread,调用AfxBeginThread(Function1,param,priority)函数,返回值赋给pThread变量的同时一并启动该线程来执行上面的Function1()函数,其中Function1是线程要运行的函数的名字,也既是上面所说的控制函数的字,param是准备传送给线程函数Function1的任意32位值,priority则是定义该线程的优先级别,它是预定义的常数,具体可参考MSDN。


2、 线程调度优先级

        在线程内核对象中,还有一个可以影响系统调度程序行为的值就是线程的优先级,它影响线程在待调度队列中排队的情况。通过调用SetThreadPriority方法可以设置一个高于或低于基本优先级THREAD_PRIORITY_NORMAL 的值。

        这个优先级基于线程所属的进程优先级来确定真实的优先级,因此线程的优先级是一个相对(相对于进程优先级)优先级,即使指定了实时优先级(进程优先级最高、线程优先级也为最高)的线程,系统调度程序仍然有能力将其调度出当前执行环境,这有力的防止了一些恶意程序抢占系统CPU的行为。

        现在尤其在NT内核的Windows平台上一般建议不再考虑利用优先级来影响线程优先调度的行为,尤其在多核系统中,更不要依赖于这个值,因为根本没法预期一个高优先级的线程必然会先于一个低优先级的线程执行,所以优先级也就失去了控制线程调度的价值。替代的使用一些线程的暂停/恢复或同步方法来更强力的控制线程的运行。

  以下的CwinThread类的成员函数用于线程优先级的操作:

int GetThreadPriority();//获取线程的优先级

BOOL SetThradPriority()(int nPriority);//为线程设置优先级

  上述的两个函数分别用来获取和设置线程的优先级,这里的优先级,是相对于该线程所处的优先权层次而言的,处于同一优先权层次的线程,优先级高的线程先运行;处于不同优先权层次上的线程,谁的优先权层次高,谁先运行。至于优先级设置所需的常数,可以参考MSDN,要注意的是要想设置线程的优先级,这个线程在创建时必须具有THREAD_SET_INFORMATION访问权限。对于线程的优先权层次的设置,CwinThread类没有提供相应的函数,但是可以通过Win32的SDK函数GetPriorityClass()和SetPriorityClass()来实现。


3、 线程的挂起(暂停)和恢复

        在Windows上,提供了一些方法可以控制线程的执行状态(影响调度),SuspendThread和ResumeThread就是用来控制线程暂停和恢复运行的方法。

        (1)SuspendThreand用来暂停一个线程的执行,线程暂停时,不会被调度执行。暂停线程总是立即被暂停,而不管被暂停的线程执行到了那个指令;

        (2)ResumeThread用来恢复一个暂停线程的执行(注意一个暂停的线程无法调用这个方法来恢复自己,因为暂停线程不可能被执行)

        (3)线程内核对象内部,存储了一个暂停计数的值,每调用一次SuspendThread方法该值就加1,调用ResumeThread一次该值就减一当该值为0时,就表示该线程可以被调度执行了,但不会被立即执行,所以多次被Suspend的线程不能期望调用一次Resume方法就恢复。

        注意:以上这两个方法入口参数为线程的句柄。线程函数内部可以通过调用Sleep或SleepEx方法自行暂停一定时间后自动恢复执行,但是这个暂停时间对于系统调度程序来说只是个参考值调度程序不能保证精确的暂停指定的时长,通常暂停时间会长于指定的暂停时长。


4、 结束线程

    (1)下列事件之一发生时线程会终止运行:

  •         调用ExitThread;
  •         该线程函数返回,即主线程隐含调用ExitProcess或其它线程调用ExitThread;
  •         ExitProcess显式或隐含的由进程的任何线程调用;
  •         用线程的句柄调用TerminateThread;
  •         用线程的进程句柄调用TerminateProcess;

    (2)当线程终止时,线程对象的状态为有信号状态,释放所有一直等待该线程终止信号的线程,然后线程终止状态从STILL_ACTIVE改变为线程的退出码。

    (3)TerminateThread终止线程时,将不会通知DLL的DLLMain函数某个线程退出,这可能导致一些资源无法释放等问题。

调用GetExitCodeThread可以得到一个退出线程的退出代码(由ExitThread、ExitProcess、TerminateThread指定的值或线程函数的返回值)。

    (4)终止线程有三种途径,

  •       线程可以在自身内部调用AfxEndThread()来终止自身的运行;
  •     可以在线程的外部调用BOOL TerminateThread( HANDLE hThread, DWORD dwExitCode )来强行终止一个线程的运行,然后调用CloseHandle()函数释放线程所占用的堆栈;
  •       第三种方法是改变全局变量,使线程的执行函数返回,则该线程终止。

下面以第三种方法为例,给出部分代码:

/////Set to True to end thread
Bool bend=FALSE;//定义的全局变量,用于控制线程的运行;
//线程函数
UINT ThreadFunction(LPVOID pParam)
{
    while(!bend)
    {
      Beep(100,100);
      Sleep(1000);
    }
 return 0;
}

/////////////////////////////////////////////////////////////

CwinThread *pThread;
HWND hWnd;
Void CtestView::OninitialUpdate()
{
    hWnd=GetSafeHwnd();
    pThread=AfxBeginThread(ThradFunction,hWnd);//启动线程
    pThread->m_bAutoDelete=FALSE;//线程为手动删除
    Cview::OnInitialUpdate();
}

////////////////////////////////////////////////////////////////

Void CtestView::OnDestroy()
{
    bend=TRUE;//改变变量,线程结束
    WaitForSingleObject(pThread->m_hThread,INFINITE);//等待线程结束
    delete pThread;//删除线程
    Cview::OnDestroy();
}

5、 线程间的通信

        通常情况下,一个次级线程要为主线程完成某种特定类型的任务,这就隐含着表示在主线程和次级线程之间需要建立一个通信的通道。一般情况下,有下面的几种方法实现这种通信任务:使用全局变量、使用事件对象、使用消息。

    (一) 利用用户定义的消息通信

        在Windows程序设计中,应用程序的每一个线程都拥有自己的消息队列,甚至工作线程也不例外,这样一来,就使得线程之间利用消息来传递信息就变的非常简单。首先用户需要定义一个用户消息,如下所示:

#define WM_USERMSG WMUSER+100;

在需要的时候,在一个线程中调用

::PostMessage( (HWND)param , WM_USERMSG ,0,0) 或

CwinThread::PostThradMessage()

来向另外一个线程发送这个消息,上述函数的四个参数分别是消息将要发送到的目的窗口的句柄、要发送的消息标志符、消息的参数WPARAM和LPARAM。下面的代码是对上节代码的修改,修改后的结果是在线程结束时显示一个对话框,提示线程结束:

UINT ThreadFunction(LPVOID pParam)
{
    while(!bend)
    {
      Beep(100,100);
      Sleep(1000);
    }
    ::PostMessage(hWnd,WM_USERMSG,0,0);
   return 0;
}

//WM_USERMSG消息的响应函数为OnThreadended(WPARAM wParam,LPARAM lParam)
LONG CTestView::OnThreadended(WPARAM wParam,LPARAM lParam)
{
    AfxMessageBox("Thread ended.");
    Retrun 0;
}

        上面的例子是工作者线程向用户界面线程发送消息,对于工作者线程,如果它的设计模式也是消息驱动的,那么调用者可以向它发送初始化、退出、执行某种特定的处理等消息,让它在后台完成。在控制函数中可以直接使用::GetMessage()这个SDK函数进行消息分检和处理,自己实现一个消息循环。GetMessage()函数在判断该线程的消息队列为空时,线程将系统分配给它的时间片让给其它线程,不无效的占用CPU的时间,如果消息队列不为空,就获取这个消息,判断这个消息的内容并进行相应的处理。

    (二)用事件对象实现通信

        事件对象是一个状态通过使用SetEvent或PulseEvent函数设置成有信号状态的同步对象。在其它情况下,该事件对象它都是无信号的,这样等待它的线程都不会被唤醒,只有调用SetEvent()方法后才会被唤醒。

        CEvent事件可以创建为手工重置状态或自动重置状态:

  • 手工重置的Event在有信号状态之后程序在手动调用ResetEvent之前一直都是有信号状态的;
  • 自动重置的Event当有信号状态在唤醒至少一个等待这个Event的线程之后,立刻变成无信号状态,如果没有线程等待这个Event那么它将一直有信号。

        线程可以监视处于有信号状态的事件,以便在适当的时候执行对事件的操作。上述例子代码修改如下:

CEvent threadStart ,threadEnd;

UINT ThreadFunction(LPVOID pParam)
{
    ::WaitForSingleObject(threadStart.m_hObject,INFINITE);
    AfxMessageBox("Thread start.");
   while(!bend)
   {
      Beep(100,100);
      Sleep(1000);
      Int result=::WaitforSingleObject(threadEnd.m_hObject,0);
      //等待threadEnd事件有信号,无信号时线程在这里悬停
      If(result==Wait_OBJECT_0)
         Bend=TRUE;
   }
   ::PostMessage(hWnd,WM_USERMSG,0,0);
   return 0;
}

Void CtestView::OninitialUpdate()
{
    hWnd=GetSafeHwnd();
   threadStart.SetEvent();//threadStart事件有信号
   pThread=AfxBeginThread(ThreadFunction,hWnd);//启动线程
   pThread->m_bAutoDelete=FALSE;
   Cview::OnInitialUpdate();
}

Void CtestView::OnDestroy()
{
    threadEnd.SetEvent();
   WaitForSingleObject(pThread->m_hThread,INFINITE);
   delete pThread;
   Cview::OnDestroy();
}

6、 线程间的同步

        (1)Windows线程同步模型:

  •     在Windows系统中,线程通过一些API可以主动放弃被执行的机会,直到某个事件或时间点到达时再去执行;
  •     这些行为直接被Windows内核线程调度程序支持;
  •     多个线程可以通过这样的方式,以一种预先安排好的顺序或逻辑执行,从而可以达到以安全的方式共享使用公共资源的目的;
  •     这些特殊的事件和时间点,通过内核对象的方式提供给线程使用,而线程则通过调用被称为等待函数的一组API来释放CPU时间片进入一种"等待状态";
  •     当内核对象表示的事件或时间点到达时,内核调度程序会立即为等待该事件或时间点的线程分配时间片使其立即得到执行。

    (2)等待函数

  •     Windows平台提供了一组能使线程阻塞其自身执行的等待函数(放弃CPU时间片,暂停进入等待状态);
  •     这组函数的执行直到由等待函数的参数指定的一组条件满足后才返回;
  •     等待函数在等待条件满足之前,将使线程立即进入有效等待状态(不会被调度,几乎不耗费CPU时间);
  •     等待时可以设定一个等待时间值,当时间到达还未等到需要的条件时,就超时返回
  •     通过传递INFINITE值可以让线程一直等待直到条件达到之后再执行;

可以被等待的对象

对象名称

中文意义

有信号状态

Change notification

文件、目录变更通知

被监视的文件或文件夹发生变更

Console input

控制台输入

标准控制台接收到输入

Event

事件

调用SetEvent方法置为有信号状态

Memory resource notification

内存资源通知

监视系统可用内存高于或低于某个临界值时

Mutex

互斥

另一个线程调用ReleaseMutex

Process

进程

进程退出时

Semaphore

信标量

另一线程调用ReleaseSemaphore

Thread

线程

线程退出时

Waitable timer

计时器

设定的时间周期到达时

等待函数详解

分类

含义

函数

备注

single-object

一次等待一个对象(调用线程进入等待状态)

SignalObjectAndWait
WaitForSingleObjec
WaitForSingleObjectEx

 

multiple-object

一次等待多个对象(调用线程进入等待状态)

WaitForMultipleObjects
WaitForMultipleObjectsEx
MsgWaitForMultipleObjects
MsgWaitForMultipleObjectsEx

MsgWaitForMultipleObjects
MsgWaitForMultipleObjectsEx

等待对象的同时还等待消息,UI编程推荐使用

alertable

等待单个对象或多个对象的同时使线程进入"等待+可警告状态"

MsgWaitForMultipleObjectsEx
SignalObjectAndWait
WaitForMultipleObjectsEx
WaitForSingleObjectEx

 

registered

注册一个等待回调函数,当等待成功(或失败),由线程池中线程负责调用该回调函数(调用线程立即返回继续执行)

RegisterWaitForSingleObject

 

    (3)MFC提供了多种同步对象,下面我们只介绍最常用的四种:临界区(CCriticalSection)、事件(CEvent)、互斥量(CMutex)、信号量(CSemaphore),通过这些类,我们可以比较容易地做到线程同步。


A、使用CCriticalSection类(临界区对象)

        临界区是保证在某一个时间只有一个线程可以访问数据的方法。使用它的过程中,需要给各个线程提供一个共享的临界区对象,无论哪个线程占有临界区对象,都可以访问受到保护的数据,这时候其它的线程需要等待,直到该线程释放临界区对象为止,临界区被释放后,另外的线程可以强占这个临界区,以便访问共享的数据。临界区对应着一个CcriticalSection对象,当线程需要访问保护数据时,调用临界区对象的Lock()成员函数;当对保护数据的操作完成之后,调用临界区对象的Unlock()成员函数释放对临界区对象的拥有权,以使另一个线程可以夺取临界区对象并访问受保护的数据。
        使用步骤:

        ①定义CCriticalSection类的一个全局对象(以使各个线程均能访问);

CCriticalSection critical_section;

        ②在访问需要保护的资源或代码之前,调用CCriticalSection类的成员Lock()获得临界区对象。在线程中调用该函数来使线程获得它所请求的临界区。如果此时没有其它线程占有临界区对象,则调用Lock()的线程获得临界区;否则,线程将被挂起,并放入到一个系统队列中等待,直到当前拥有临界区的线程释放了临界区时为止。

critical_section.Lock();

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

critical_section.Unlock();

        再通俗一点讲,就是线程A执行到critical_section.Lock();语句时,如果其它线程(B)正在执行critical_section.Lock();语句后且critical_section. Unlock();语句前的语句时,线程A就会等待,直到线程B执行完critical_section. Unlock();语句,线程A才会继续执行。

B、使用CEvent类(事件对象)

CEvent 类提供了对事件的支持。事件是一个允许一个线程在某种情况发生时,唤醒另外一个线程的同步对象。例如在某些网络应用程序中,一个线程(记为A)负责监听通讯端口,另外一个线程(记为B)负责更新用户数据。通过使用CEvent 类,线程A可以通知线程B何时更新用户数据。每一个CEvent 对象可以有两种状态:有信号状态和无信号状态。线程监视位于其中的CEvent 类对象的状态,并在相应的时候采取相应的操作。

在MFC中,CEvent 类对象有两种类型:人工事件和自动事件。一个自动CEvent 对象在被至少一个线程释放后会自动返回到无信号状态;而人工事件对象获得信号后,释放可利用线程,但直到调用成员函数ReSetEvent()才将其设置为无信号状态。在创建CEvent 类的对象时,默认创建的是自动事件。 CEvent 类的各成员函数的原型和参数说明如下:

CEvent( BOOL bInitiallyOwn=FALSE,
        BOOL bManualReset=FALSE,
        LPCTSTR lpszName=NULL,
        LPSECURITY_ATTRIBUTES lpsaAttribute=NULL
      );

bInitiallyOwn:指定事件对象初始化状态,TRUE为有信号,FALSE为无信号;

bManualReset:指定要创建的事件是属于人工事件还是自动事件。TRUE为人工事件,FALSE为自动事件;

后两个参数一般设为NULL,在此不作过多说明。

        使用方法——方法一

        ①定义CEvent类的一个全局对象(以使各个线程均能访问);

CEvent ThreadSyncEvent;

        ②将 CEvent 类对象的状态设置为有信号状态。

ThreadSyncEvent.SetEvent();

        ③  该函数将事件的状态设置为无信号状态,并保持该状态直至SetEvent()被调用时为止。

ThreadSyncEvent.ResetEvent();

        使用方法——方法二

BOOL CEvent::SetEvent();

将 CEvent 类对象的状态设置为有信号状态。如果事件是人工事件,则 CEvent 类对象保持为有信号状态,直到调用成员函数ResetEvent()将 其重新设为无信号状态时为止。如果CEvent 类对象为自动事件,则在SetEvent()将事件设置为有信号状态后,CEvent 类对象由系统自动重置为无信号状态。

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


BOOL CEvent::ResetEvent();

        该函数将事件的状态设置为无信号状态,并保持该状态直至SetEvent()被调用时为止。由于自动事件是由系统自动重置,故自动事件不需要调用该函数。如果该函数执行成功,返回非零值,否则返回零。我们一般通过调用WaitForSingleObject函数来监视事件状态。前面我们已经介绍了该函数。由于语言描述的原因,CEvent 类的理解确实有些难度,但您只要通过仔细玩味下面例程,多看几遍就可理解。

C、使用CMutex

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

D、使用CSemaphore

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

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

CSemaphore (LONG lInitialCount=1,
            LONG lMaxCount=1,
            LPCTSTR pstrName=NULL,
            LPSECURITY_ATTRIBUTES lpsaAttributes=NULL
           );
            lInitialCount:信号量对象的初始计数值,即可访问线程数目的初始值;
            lMaxCount:信号量对象计数值的最大值,该参数决定了同一时刻可访问由信号量保护的资源的线程最大数目;

            后两个参数在同一进程中使用一般为NULL,不作过多讨论;

        在用CSemaphore 类的构造函数创建信号量对象时要同时指出允许的最大资源计数和当前可用资源计数。一般是将当前可用资源计数设置为最大资源计数,每增加一个线程对共享资源的访问,当前可用资源计数就会减1,只要当前可用资源计数是大于0的,就可以发出信号量信号。但是当前可用计数减小到0时,则说明当前占用资源的线程数已经达到了所允许的最大数目,不能再允许其它线程的进入,此时的信号量信号将无法发出。线程在处理完共享资源后,应在离开的同时通过ReleaseSemaphore()函数将当前可用资源数加1。

声明:

主要部分内容均为非原创,但内容的调理和整合为个人整理,如需转载,请注明出处!

部分内容转载自:https://blog.csdn.net/arcsinsin/article/details/16832417

原文地址:http://www.vckbase.com/index.php/wv/1416

你可能感兴趣的:(MFC,C++学习)