Delphi线程基础知识

参考http://blog.chinaunix.net/uid-10535208-id-2949323.html

一、概述

  Delphi提供了好几种对象以方便进行多线程编程。多线程应用程序有以下几方面的功能:

  1.避免性能瓶颈:单线程应用程序在进行比较慢的操作如磁盘读写的时候,CPU必须停下来等待,直到该操作执行完毕。而多线程应用程序在进行比较慢的操作如磁盘读写的时候,即系执行另一个单独的线程;

  2.组织应用才程序的行为:通常,一个程序的行为可以组织成几个功能相互独立的平行的处理过程。将这些独立的处理组织成单独的几个线程,就可以同时启动这几个处理过程。还可以使用线程的优先级来控制那些任务获得更多的CPU时间

二、定义线程对象

  在大多数情况下,可以使用线程对象来代表一个线程。线程对象封装了常用的线程操作,从而简化了线程对象

  注意:线程对象不允许控制线程的安全属性和栈的大小。如果你确实要控制这些东西,那只能够使用BeginThread函数来创建线程了。

  要在程序中使用线程对象,必须自定义一个从TThread派生的类。可以使用想到来帮助创建,生成的代码如下:

unit Unit2;
 
interface
uses
    Classes;
type
    TMyThread = class(TThread)
    private
    {Private declarations}
    protected
    procedure Execute; overrride;
    end;
implementation
    {TMyThread}
    procedure TMyThread.Execute;
    begin
        {Place thread code here}
    end;
end.

  

  在上面自动生成的代码里,可以:

    加入自己的线程初始化代码(可选)

    将线程执行的操作写入Execute函数

    加入自己的线程初始化代码(可选)

三、初始化线程

  如果你需要为你新创建的线程类加入初始化代码,就必须重写Create方法。你可以在Create方法里面给线程赋优先级,并可以决定线程在任务完成后是否自动销毁

  1.给线程赋一个默认优先级

  可以使用Priority属性给线程赋予优先级

  在Windows下,Priority可以取下面的值:  

  tpIdle  The thread executes only when the system is idle.Windows won't interrupt other threads to execute thread with tpIdle  priority

  tpLowest  The thread's priority is two points below normal

  tpLower  The thread's priority is one point below normal

  tpNormal  The thread has normal priority

  tpHigher  The thread's priority is one point above normal

  tpHighest  The thread's priority is two point above normal

  tpTimeCritical   The thread gets highest priority

  下面的代码演示了一个低优先级的线程的构造函数,该线程在后台执行一些任务

constructor TMyThread.Create(CreateSuspended : Boolean);
begin
    inherited Create(CreatedSuspended);
    Priority := tpIdle;
end;

  

2.指示线程是否自动销毁

  通常,当一个线程完成了它的操作之后,他们就应该被销毁。如果是这样的话,最好的方式就是让线程自动销毁。要让一个线程自动销毁的话,可以在线程构造函数里面将它的FreeOnTerminate属性设置为True

  然而有些时候,一个线程的结束必须和其他线程同步。如你或许正在线程A里等待线程B的返回值,这个时候你肯定不想线程B在线程A收到返回值前就自动销毁。在这种情况下,就必须将线程B的FreeOnTerminate属性设置为Flase,然后再线程A接收到返回值后,显式销毁线程B

四、线程函数

TThread的Execute方法是线程函数。你可以将线程函数看作是一个由你的程序启动的独立程序。只不过它共享你的程序的进程空间。写线程函数比写一个单独的程序要复杂,因为你必须确保线程函数没有覆盖你的程序当中的其他线程所使用的内存区域从另一个方面看,由于多个线程之间是共享同一个进程空间的,因此你可以在多个线程之间通过共享内存进行交流

  当TThread的Create()被调用时,需要传递一个布尔型的参数CreateSuspended。如果把这个参数设成False,那么当调用Create()后,Excute()会被自动地调用,也就是自动地执行线程代码。

  如果该参数设为True,则需要运行TThread的Resume()来唤醒线程。一般情况下,当你调用Create()后,还会有一些其他的属性要求设置。所以,应当把CreateSuspended参数设为True,因为在TThread已执行的情况下设置TThread的属性可能会引起麻烦。

  再深入一点讲,在构造函数Create()中隐含调用了一个RTL例程BeginThread(),而它又调用了一个API函数CreateThread()来创建一个线程对象的实例。CreateSuspended参数表明是否传递CREATE_ SUSPEDED 标志给CreateThread()

  1.使用主线程

  当你使用VCL中的对象时,他们的属性和方法都不保证是线程安全的。即访问属性或执行方法时涉及的一些操作或许会用到别的线程可能正在使用的未保护的内存区域。考虑到这一情况,Delphi专门设置了一个主线程来访问VCL对象。主线程处理程序中所有组件接收到的Windows消息。  

  如果所有的对象都在同一个线程中访问它们的属性以及执行它们的方法,那么你就不必担心这些对象之间互相影响。可是如果你想在主线程以外的线程中访问VCL对象,那怎么办?可以创建一个单独的方法并在这个方法执行访问主线程VCL对象的操作,然后使用Synchronize方法来调用这个方法。如

procedure TMyThread.PushTheButton;
begin
    Button1.Click;
end;
 
procedure TMyThread.Execute;
begin
    ...
    Synchronize(PushTheButton);    //注意这个函数的使用的形式
    ...
end;

  

  Synchronize会等待主线程进入消息循环时才执行传入的方法。

  当你确信某个对象的方法是线程安全的时候,可以省略Synchronize,这样可以提高程序的效率,因为不必等待主线程进入消息队列。

  建议在主线程中周期的调用CheckSynchronize,以便后台线程能够执行它们的Synchronize方法。

  当然调用CheckSynchronize方法的最佳时机应当是当主线程处于Idle时(可以在OnIdle事件处理程序中调用CheckSynchronize)

2.使用线程局部变量

  线程函数以及在线程函数里面调用的任何其他的函数都可以定义它们自己的局部变量,也可以访问任何全局变量。事实上,全局变量是在多个线程之间进行交流的一个很有效的手段。

  有些时候,你可以需要这样的一些变量:同一个线程对象里面的所有函数都可以访问,但是其他的线程对象里面的函数则不能访问。可以声明线程局部变量,线程局部变量是在threadvar程序段进行声明的,如

threadvar
    x : Integer;

  threadvar程序段只能声明全局变量。指针和函数变量不能成为线程变量。使用了copy-on-write技术的类型(如long strings)也不能声明为线程变量。

  3.Terminate  

  你可以允许其他线程来向一个线程发出结束的信号。当其他的线程想要结束该线程时,可以调用它的Terminate方法。Terminate方法将Terminated属性设置为True。如

procedure TMyThread.Execute;
begin
    while not Terminated do
        PerformSomeTask;
end;

  

  4.在线程函数里面处理异常

  Execute函数必须捕获其中发生的所有异常。如果你在线程函数里面漏掉了某个异常的话,那么应用程序就会出现非法访问的情况。

  要捕获线程函数里面所有的异常,可以在Execute方法里面使用try...except语句块,如:

procedure TMyThread.Execute;
begin
    try
        while not Terminated do
            PerformSomeTask;
    except
        {do something with exceptions}
    end;
end;

  

五、销毁线程

  在线程结束的时候,会触发OnTerminate事件。可以将clean-up代码放在OnTerminate事件的处理程序里OnTerminate事件处理程序不是作为线程的一部分来运行,而是运行在主线程的上下文中。因此:

    你不能在OnTerminate事件处理程序中使用线程变量

    你可以在OnTerminate事件处理程序中安全的访问任何VCL对象

六、多线程并发执行

  当你在写线程代码的时候,必须要考虑到其他线程可能会与当前线程同时执行这一事实。尤其是需要避免多个线程同时访问相同的全局变量。此外,一个线程里面的代码可能会依赖其他线程的执行结果。

  1.避免同时访问

  在访问全局对象时候,为了避免和其他的线程发生冲突,需要阻塞其他线程的操作直到当前线程的访问代码执行完毕。需要注意不要无谓的阻塞其他线程的操作,阻塞操作会极大降低程序的执行效率。无谓的阻塞会有被使用线程的初衷。

  VCL支持三种方法来组织其他线程访问当前线程正在访问的内存区域:锁定对象、使用关键区、使用multi-read exclusive-write Synchronizer  

    1)锁定对象

    一些对象有内置的锁机制。比如,Canvas对象就有一个Lock方法,可以组织其他线程的访问,直到调用UnLock方法

    2)关键段

    如果对象没有提供内置的锁机制,你可以使用关键段。关键段就像一个们,同一时间只允许一个线程进入。要使用关键段,先创建一个TCriticalSection类的全局对象。TCriticalSection有两个方法,Acquire(阻塞其他方法)和Release(移除阻塞)

    每个关键段与你要保护的全局区域相关。每个访问全局内存区域的线程都必须首先使用Acquire方法来保证没有其他别的线程在使用它。当访问结束之后,线程调用Release方法以便其他的线程能够通过Acquire方法来访问该全局内存区域

    注意:关键段只能在所有的线程都使用它们时才起作用。如果有线程忽略关键段,不通过Acquire访问内存区域,则会导致并发问题

  2.等待其他线程

  如果当前线程需要等待另一个线程完成某个任务,你可以让当前线程暂停执行

    1)等待另一个线程结束

    可以使用WaitFor方法等待另一个线程结束。WaitFor直到另一个线程结束后才返回。如:

if ListFillingThread.WaitFor then
begin
    with ThreadList1.LockList do
    begin
        for I :=0 to Count -1 do
            ProcessItem(Items[I]);
    end;   
    ThreadList1.UnlockList;
end;

         2)等待一个任务结束

    有时候,需要等待另一个线程执行完某些操作之后而不是完全结束。为此,应当使用一个事件对象。事件对象(TEvent)应当在全局范围内被创建以便它们能够被所有的线程访问

  当一个线程完成了一个操作,并且该操作是其他线程所依赖,那么它就调用TEvent.SetEvent打开信号,于是所有的其他线程都可以通过检测直到该操作执行完毕了。要关闭信号,使用ReseEvent操作

七、执行线程对象

  启动和停止线程

  一个线程在使用它的Create()方法进行初始化的时候,如果传入的参数是Fasle,那么该线程的Execute()方法就会被调用,也就是说使用Create(False)方法之后,这个线程就已经跑起来了。

  一个线程在它完成之前,可以被停止和启动任意次。临时停止一个线程,使用Suspend函数。调用Resume方法恢复线程。Suspend函数会增加内部计数器,而Resume则会减少内部计数器,因此你可以嵌套多次调用Suspend和Resume。调用Resume只有当线程计数器为0时,线程才会真正启动起来

  可以调用线程的Terminate方法来永久的终止线程。Terminate设置线程的Terminated属性为True

八、调试多线程应用程序

  当调试多线程应用程序的时候,跟踪所有同时执行的线程状态,甚至只是想知道在断点处哪一个线程还在执行都是一件很郁闷的事情,可以使用Thread status Box来帮助跟踪和操纵应用程序的所有线程。要显示Thread status Box,从主菜单选择View|Threads

  当一个调试事件(断点、异常、暂停……)发生的额时候,Thread status Box将会指示各个线程的状态。右击Thread status Box来访问……

  Thread status Box按照线程ID列出了程序中的所有线程。如果你使用线程对象,那么线程ID是属性ThreadID的值。如果使用的不是线程对象,那么线程ID是BeginThread函数的返回值