zoukankan      html  css  js  c++  java
  • 理解多线程引用自 http://eagletff.blog.163.com/blog/static/11635092820105158493975/

    引用

    东海引用 理解多线程

    引用

    一意孤行理解多线程

    0. 前言 

    多线程是多任务操作系统下一个重要的组成部分,它能够提高应用程序的效率,然而,我们想利用好多线程,必须要了解很多的东西,比如操作系统的原理,堆栈概念和使用方法。然而,使用不当,将会造成无尽的痛苦。曾经刚刚接触的时候,我也为之恐惧,迷惑了好久。在无数次的失败和查找资料解决问题之后,稍有感触,故写下此文,总结一下自己,同时,也给后学者一点启示,希望让他们少走弯路。

    1.  基础知识。 

        线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。一个线程可以创建和撤消另一个线程,同一进程中的多个线程之间可以并发执行。由于线程之间的相互制约,致使线程在运行中呈现出间断性。线程也有就绪、阻塞和运行三种基本状态。

    线程的生死。在windows中,我们可以通过调用API  CreateThread/CreateRemoteThread创建一个线程(其实,在Windows内部,CreateThread最终是调用了CreateRemoteThread创建线程)。当线程函数执行退出时,可以说这个线程已经完成了它的使命。调用ExitThread可以结束一个线程,同时调用CloseHandle来释放Windows分配给它的句柄资源。GetExitCodeThread可以用来检测线程是否已经退出。

    HANDLE CreateThread(

      LPSECURITY_ATTRIBUTES lpThreadAttributes,            // SD,线程的属性

      DWORD dwStackSize,                                    // initial stack size,线程堆栈的大小

      LPTHREAD_START_ROUTINE lpStartAddress,           // thread function,线程函数

      LPVOID lpParameter,                                      // thread argument,参数

      DWORD dwCreationFlags,                               // creation option,创建时的标志

      LPDWORD lpThreadId                                   // thread identifier,线程的ID

    );

    线程的控制。线程的有三种状态:就绪,阻塞,运行。当我们在CreateThread的时候,第5个参数为CREATE_SUSPENDED标志时,线程创建后就处于挂起,即阻塞状态,否则,线程就会调用线程函数立即执行。ResumeThread可以让线程阻塞的线程继续运行,SuspendThread可以让线程挂起。(具体用法参考MSDN)

    2. 线程同步 

    不同线程间公用同一个资源的时候,就需要进行线程同步。

    为何要同步?要回答好这个问题我们要从栈说起。这里说的栈,和数据结构中的堆栈是不一样的。(穿插一个小的知识: 堆和栈的区别。以前看过一个帖子,里面有个很精辟的回复,说明了堆和栈的区别:“堆就像自己在家里做饭,想做什么就做什么,但是,最后的锅碗等还需要自己去收拾;而栈就像是去餐馆吃饭,只要你点好菜,餐馆就给你提供,吃完之后锅碗什么的都不需要自己管。”,这说明堆和栈的区别以及如何使用它们:堆,可以自己完全控制,用完之后需要自己清理,处理不好就会造成内存泄漏;栈,由操作系统分配,不需要进行管理,不用担心内存泄漏)。简单的说,栈就是一块内存区域,它是从大到小增长的,它遵循后进先出的原则(FILO,First In Last Out)。通常,CPU的EBP和ESP是用作栈的,EBP是栈的基地址,ESP是当前栈顶的位置(栈顶永远是小于等于栈底的)。栈的主要作用就是保存现场,函数参数传递。对于栈的操作汇编中有两条指令:PUSH和POP,分别用于数据入栈和出栈。这两条指令可以影响ESP的值,当然你也可以直接使用SUB ESP XXX、ADD ESP XXX这种方式来更改栈顶的位置。我们来看看函数的调用过程(这里不考虑调用惯例,仅仅是个示意):

    PUSH         EBP                             // 将当前栈底的位置压入栈

    SUB            ESP, XXXX                   // 为函数开辟栈,XXXX为栈的大小

    PUSH         参数                             // 参数入栈

    CALL          SomeAddress           // 调用函数

    ADD            ESP, XXXX                   // 释放为函数开辟的栈(这里就解释了为什么我们不需要去管在栈上分配的内存)

    POP            EBP                             // 恢复EBP的位置

    每个线程有自己的栈,在CreateThread的时候,第二个参数就是用来指定线程的栈的大小,传入0时,系统会自动分配栈的大小。现在看多线程使用共享资源(可以是公共变量,也可以是公共代码等)时的情况。如图,A和B共享一个资源S,A首先获取到了资源S,得到S的状态S1,线程A开始运行,当A运行了一段时间后,A的线程时间片用完,于是A被操作系统挂起,在挂起的时候系统会将A的运行状态记录到A的堆栈中,以便下次唤醒A是能正常运行。这是共享资源S的状态S1也被保存到了A的堆栈中。接下来,线程B获得了运行权利,开始运行,它也得到了S的状态S2,B开始运行,并且改变了S的状态,假设改变成S3。B运行结束后。A重新被唤醒了,A从栈中取出S的状态S1继续运行,而这时,S的实际状态已经变成S3,而A并不知道,于是,A运行的结果就错误了。也许有些混乱,我们举个更简单的例子:线程A和B共用一个公共变量S(假设为int,初始值为1)。我们再来看这个过程:

    A开始运行获取S值1,A运行  -> A被挂起 -> 此时线程A中S的值1被保存到A的栈中 >  B 开始运行,并且修改S的数值为100 > A被唤醒 > A获取S的值1 -> A 将运行的结果保存到S。

    我们看这个过程中,S的值混乱了。所以,我们必须对共享资源进行保护。

    在进行了线程同步时,当A获取到S后,其它任何线程将不能获取和修改S,这样就保证S不再混乱。

    总结一下,线程实现了进程并发运行的效果,线程同步是为了解决线程并发的“冲突”问题(共享资源读写)。

    (小知识:调用栈在程序调试中有重要的作用,当程序发生异常时,我们可以调出它来追查原因。VC中按下Alt + 7可以调出调用栈窗口,Delphi中按下Ctrl + Alt + S可以调出调用栈)

    如何同步?在Windows系统中,我们可以使用互斥量信号量事件重要区段等方式进行线程同步。重要区段仅仅可以用于同一个进程中的不同线程之间的同步,它运行与用户态,其效率是最高的。其余的运行与内核态,可以用于不同进程间(需要在用户态和内核态进行切换)。信号量可以允许多个线程同时访问同一资源,互斥量是信号量的一种特殊情况。具体的用法可以参考MSDN的帮助。写个简单使用重要区段的一个例子:

    // 初始化

    InitializeCriticalSection(FLock);           // 初始化重要区段

    // 使用方法

    EnterCriticalSection(FLock);                // 进入保护区

      //.. 需要保护的数据

    LeaveCriticalSection(FLock);               // 释放

    // 释放资源

    DeleteCriticalSection(FLock);              // 删除重要区段

    另外,消息也可以作为同步的一种手段。也许你会说,消息必须要有UI,也就是说必须要有窗体才可以,其实不然,使用PostThreadMessage,然后利用SetWindowsHookEx来Hook线程的消息,处理我们发送的消息(这种方式是我在做注入后对注入进行控制时想到的方法),如下:

    发送方: ::PostThreadMessage(hThread, WM_XXX, wPar, lPar);

    接收方:

      ::SetWindowsHookEx(WH_GETMESSAGE, GetMsgProc, hInstance, dwThreadID);

      GetMsgProc(int code, WPARAM wParam, LPARAM lParam);

      {

        if (PMSG (lParam)^.message == WM_XXX)

        {

          // Process

        }

        return ::CallNextHookEx();

      }

    这种方法的好处就是我们可以发送两个参数给目标。

     

    3. 线程中常见的问题。 

       1) 回调函数引起的死锁。

          A回调线程B中的函数,而在线程B中,再去对线程A进行操作(比如删除A)。

    发生的现象:程序死掉。

         2) 使用同一资源未加保护引起问题。

            A和B同时去对窗体上进行绘图操作,界面可能花掉,也可能黑掉。

    出现的现象:界面不再刷新,变成黑色。(最好不要在子线程中去更新界面UI,可以使用消息来更新)

      3) 线程锁使用不当造成死锁。

        线程A利用线程锁锁住资源A后,再去试图访问资源B,线程B利用线程锁锁住资源B后试图去访问资源A。这样就发生了线程互锁。

    程序结果:线程死掉。

      4) 未加线程保护产生异常。

         线程A获取到了对象X(步骤1)的引用后被挂起(步骤2),而接下来线程B却删除了X(步骤3),线程A再次唤醒后访问对象X出错(步骤4)。这个问题是多线程中最容易被忽略的地方,也是异常最可能发生的情况。


    程序结果:线程异常。

      5) 消息在线程同步中的问题。

    先说说消息的一些基本问题(有关消息的处理部分在Windows 2000源码private\ntos\w32\ntuser\kernel\input.c文件中):

    消息队列的建立:线程在刚建立的时候,是没有消息队列的。当有界面UI操作函数被调用的时候(比如CreateWindow),Windows就会为该程序建立一个消息队列,同样,通过调用PeekMessage/GetMessage可以强制操作系统为线程建立一个消息队列(参看MSDN关于PostThreadMessage的说明)。

    消息的正常处理流程:线程通过GetMessage/PostMessage获取消息,然后通过TranslateMessage进行字符转换,接下来,通过User32.dll模块的帮助最后调用到相应窗口的窗口过程(RegisterClass时传入的窗口过程)。

    消息重入问题:所谓的消息重入,就是在消息处理过程中再调用SendMessage发送消息给目标窗口。如果控制不好,就会引发异常。一个简单的例子:在WM_PAINT中再去SendMessage(hWin, WM_PAINT, 0, 0)。其结果就是堆栈溢出异常了(stack overflow)。(相当于WndProc在进行无限递归),Delphi代码如下(可以自己感受一下“Stack Overflow”是怎么一回事,等异常后调出CallStack看看^_^):

    procedure OnPaint(var tMsg: TMessage); message WM_PAINT;                 // Interface

    // Implementation

    procedure TForm1.OnPaint(var tMsg: TMessage);

    begin

      SendMessage(Self.Handle, WM_PAINT, 0, 0);

    end;

    SendMessage & PostMessage: SendMessage发送一个消息给窗口,同时,操作系统会去直接调用窗口的窗口过程而不经过线程的消息队列。而PostMessage则仅仅是将消息投递到消息队列,应用程序通过GetMessage/PeekMessage获取消息处理后再交给系统分发消息。

    消息在多线程中的问题: 

    分析一个具体过程说明在多线程中因消息而引起的问题:

    线程A(主线程)通过GetMessage->DispatchMessage,接下来通过User32模块的帮助调用WndProc进行消息处理的过程对RichEdit中插入一张图片。其步骤如下:

    1. 在RichEdit中定位要插入的位置(X, Y)。RichEdit->SetSel(X, Y)

    2. 创建OLE对象。

    3. 获取ClientSite接口插入对象。

    线程B通过SendMessage调用WndProc要求在RichEdit的末尾添加一段文字。其步骤如下:

    1. 定位到RichEdit末尾。RichEdit->SetSel(-1, -1);

    2. 调用RichEdit->ReplaceSel(sText)插入文本。

    假如线程A在步骤1刚运行完毕后,其运行时间片结束,线程被挂起,当前的状态被保存到线程A的堆栈中。线程B开始运行,线程B插入文本完成返回,线程A重新被唤醒,开始执行步骤2,3, 而这时,线程B已经改变了当前的插入位置,线程A并不知道,于是,就会出现插入的图片错位现象。

    归根结底,是线程的同步问题。

    结论:尽量用PostMessage而不是SendMessage。

    6) 异常引起的问题。

       看这段代码:

       CCriticalSection  m_cLock;

      

       m_cLock.Lock;

       // Do something

       m_cLock.Unlock();

        初看是没有什么问题,但是,如果我们在DoSomeThing的时候产生了异常,那么UnLock代码将不会被执行,于是,线程锁将一直处与加锁状态,其他线程将无法访问。

    m_cLock.Lock;

    try

    {

             // Do something

             m_cLock.Unlock();

    }

    catch(…)

    {

    m_cLock.Unlock();

    }

    在Delphi中处理这种情况很简单:

    m_cLock.Lock;

    try

      // Do something

    finally

     m_cLock.Unlock;

    end;

    以上是我在多线程中所遇到的一些问题的总结,希望能对大家有用。

    4、线程效率 

       虽然线程能够提高我们的程序的效率,然而,如果使用不当,反而会降低程序效率。特别是在线程同步的过程中,对于公共资源的读写保护部分。

    看下面的例子(假设这是一个网络多线程下载程序,一个线程负责将下载的内容保存到文件,另外几个线程负责将数据加到队列中,下面写出保存线程的示例):

    typedef struct

    {

       char           m_pBuf[1024];           // … Data

    }TNode, *PNode;

    private:

       CPtrList                                m_plTmpList;

    CCriticalSection                 m_cLock;

     POSITION       posTmp;

              PNode             pItem = NULL;

    m_cLock.Lock();

    try

    {

       posTmp = m_plTmpList.GetTailPosition();

       while(posTmp)

       {

         posPrev = posTmp;

             pItem = (PNode)m_plTmpList.GetPrev(posTmp);

          // 对于pItem进行处理,保存到磁盘

          m_plTmp.RemoveAt(posPrev);

         delete pItem;

       }

      m_cLock.Unlock();

    }

    catch(…)

    {

           m_cLock.Unlock();

    }

        通常情况下,我们会使用这种方式来遍历整个列表,然而,如果我们对于pItem的处理需要很长时间,比如我们要将pItem中的数据存放到硬盘上,那么这个过程将会非常耗时。其它线程将无法访问该列表。那么如何才能提高效率呢?我们可以使用两个队列,一个队列设置为下载队列,另外一个是当前保存队列。当保存队列中的所有内容全部保存到磁盘后,我们将下载队列和保存队列进行交换,即下载队列变成保存队列,保存队列变成下载队列。

    总之,在线程同步操作中,提高线程效率最重要的就是减少线程公共资源操作的时间,或者是采用其它方法避免同步。

     

    5、后记(题外话) 

    当我们习惯于Windows下的RAD开发工具的时候,我们往往忽略了对于整个RDA环境的封装以及系统底层的探究,隐藏在操作系统内部的东西或机制往往被我们忽略。RAD工具大大提高了开发效率,然后它也助长了我们的惰性。很多人在抱怨大学里学习的东西都没有用,然而,现在看来,当初的很多东西都是很有用的,比如操作系统原理,数据结构和算法。我不是计算机的专业出身,我很庆幸当初自己对哪些东西略微了解了一些,以至于我现在理解起来一些东西不再那么困难。

    理解Windows的运行原理,Windows的主要模块作用,对于软件开发有很大的帮助。很长一段时间里,我被Windows的华丽外衣所迷惑,整天还沉浸在DOS下的单任务环境,迷失在Windows下的软件开发中。幸好,现在终于走出了这片森林,看到了森林的一角。可以欣慰的说一声:我终于找到进入软件开发的大门了。

    软件开发,是人和机器的交互过程。我们想让机器更好的为我们工作,我们就需要对机器有较多的认识,同时也要对我们所处的开发环境有较多的了解。

    软件开发,不仅仅是一门技术,更是一门艺术。然后,在现在,能把它当成一门艺术来看待的人已经不多了

  • 相关阅读:
    入门菜鸟
    FZU 1202
    XMU 1246
    Codeforces 294E Shaass the Great 树形dp
    Codeforces 773D Perishable Roads 最短路 (看题解)
    Codeforces 814E An unavoidable detour for home dp
    Codeforces 567E President and Roads 最短路 + tarjan求桥
    Codeforces 567F Mausoleum dp
    Codeforces 908G New Year and Original Order 数位dp
    Codeforces 813D Two Melodies dp
  • 原文地址:https://www.cnblogs.com/linyawen/p/2080592.html
Copyright © 2011-2022 走看看