当前位置: 首页 > news >正文

引用 引用 理解多线程

 

引用

东海 的 引用 理解多线程

 

引用

一意孤行 的 理解多线程

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. 线程同步

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

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

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

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

PUSH         参数                             // 参数入栈

CALL          SomeAddress           // 调用函数

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

POP            EBP                             // 恢复EBP的位置

理解多线程 - 一意孤行 - 听泉居

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

A开始运行获取S1A运行  -> A被挂起 -> 此时线程AS的值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,然后利用SetWindowsHookExHook线程的消息,处理我们发送的消息(这种方式是我在做注入后对注入进行控制时想到的方法),如下:

发送方: ::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) 使用同一资源未加保护引起问题。

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

理解多线程 - 一意孤行 - 听泉居

出现的现象:界面不再刷新,变成黑色。(最好不要在子线程中去更新界面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重新被唤醒,开始执行步骤23 而这时,线程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下的软件开发中。幸好,现在终于走出了这片森林,看到了森林的一角。可以欣慰的说一声:我终于找到进入软件开发的大门了。

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

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

 

James.Zhai  2009-03-19 @ NetMarch 

转载于:https://www.cnblogs.com/zhihaowang/archive/2010/06/15/10128671.html

相关文章:

  • 诊断RAC数据库的启动
  • 异步备份和还原数据库:.NET发现之旅(六)
  • 【转载】一位大学老师写给即将毕业的大学生的100条忠告
  • GPON故障总结(四)
  • Css Hack
  • windows7下Windows Live Messenger 托盘问题
  • 图解Windows xp—FTP服务器配置
  • [经验总结] 关于单元测试
  • 我的OSPF学习笔记
  • 软件项目经理应有的能力和素质
  • 动态创建GridView的列(第二部分)
  • visual studio数据集dataset.xsd文件使用
  • java反射总结
  • 临 元 刘堪 《蔬林远山图》
  • Linux系统配置VI或VIM的技巧
  • 《深入 React 技术栈》
  • 【译】理解JavaScript:new 关键字
  • 10个最佳ES6特性 ES7与ES8的特性
  • Docker 笔记(1):介绍、镜像、容器及其基本操作
  • ES10 特性的完整指南
  • es的写入过程
  • isset在php5.6-和php7.0+的一些差异
  • js中的正则表达式入门
  • linux安装openssl、swoole等扩展的具体步骤
  • passportjs 源码分析
  • python学习笔记-类对象的信息
  • TiDB 源码阅读系列文章(十)Chunk 和执行框架简介
  • Vue2 SSR 的优化之旅
  • 安装python包到指定虚拟环境
  • 给自己的博客网站加上酷炫的初音未来音乐游戏?
  • 聊聊flink的TableFactory
  • 每天10道Java面试题,跟我走,offer有!
  • 使用Envoy 作Sidecar Proxy的微服务模式-4.Prometheus的指标收集
  • 怎么把视频里的音乐提取出来
  • ionic入门之数据绑定显示-1
  • # 透过事物看本质的能力怎么培养?
  • #QT(一种朴素的计算器实现方法)
  • (3)llvm ir转换过程
  • (8)Linux使用C语言读取proc/stat等cpu使用数据
  • (JSP)EL——优化登录界面,获取对象,获取数据
  • (论文阅读32/100)Flowing convnets for human pose estimation in videos
  • (没学懂,待填坑)【动态规划】数位动态规划
  • (三)Pytorch快速搭建卷积神经网络模型实现手写数字识别(代码+详细注解)
  • (转)如何上传第三方jar包至Maven私服让maven项目可以使用第三方jar包
  • ******IT公司面试题汇总+优秀技术博客汇总
  • 、写入Shellcode到注册表上线
  • ... 是什么 ?... 有什么用处?
  • .NET 使用 JustAssembly 比较两个不同版本程序集的 API 变化
  • .net打印*三角形
  • .NET轻量级ORM组件Dapper葵花宝典
  • .NET中winform传递参数至Url并获得返回值或文件
  • ??如何把JavaScript脚本中的参数传到java代码段中
  • @private @protected @public
  • @vue/cli 3.x+引入jQuery
  • @拔赤:Web前端开发十日谈