您现在的位置:首页 >> 基础算法 >> window基础 >> 内容

Delphi中多线程分析详解

时间:2011/9/3 15:35:57 点击:

  核心提示:0. 前言多线程是多任务操作系统下一个重要的组成部分,它能够提高应用程序的效率,然而,我们想利用好多线程,必须要了解很多的东西,比如操作系统的原理,堆栈概念和使用方法。然而,使用不当,将会造成无尽的痛...

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是栈的基地址,EBP是当前栈顶的位置(栈顶永远是小于等于栈底的)。栈的主要作用就是保存现场,函数参数传递。对于栈的操作汇编中有两条指令:PUSH和POP,分别用于数据入栈和出栈。这两条指令可以影响ESP的值,当然你也可以直接使用SUB ESP XXX、ADD ESP XXX这种方式来更改栈顶的位置。我们来看看函数的调用过程(这里不考虑调用惯例,仅仅是个示意):

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

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

PUSH         参数                             // 参数入栈

CALL          SomeAddress           // 调用函数

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

POP            EBP                             // 恢复EBP的位置

600) this.width = 600;">

每个线程有自己的栈,在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的值混乱了。所以,我们必须对共享资源进行保护。
600) this.width = 600;">

在进行了线程同步时,当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(…);

  }

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

上一页123下一页

作者:James.Zhai 来源:转载
共有评论 0相关评论
发表我的评论
  • 大名:
  • 内容:
  • 盒子文章(www.2ccc.com) © 2024 版权所有 All Rights Reserved.
  • 沪ICP备05001939号