Delphi管理多线程之线程局部存储:threadvar

  尽管多线程能够解决许多问题,但是同时它又给我们带来了很多的问题。其中主要的问题就是:对全局变量或句柄这样的全局资源如何访问?另外,当必须确保一个线程中的某些事件要在另一个线程中的其他时间之前(或之后)发生时,该怎么办?这里将讲解通过使用由 Delphi提供的线程局部存储和 A P I为线程提供同步的方法。

  这里先讲线程局部存储,下一篇再讲线程同步

线程局部存储

  由于每个线程都代表一个不同的执行路径,因此,最好有一种只限于一个线程内部使用的数据存储方式。要实现上述目的有三种方式:

1)第一种也是最简单的一种方式是局部变量((基于栈)。

  由于每个线程都在各自的栈中,各个线程都将有一套局部变量的副本,这样,就不会互相影响。

2)第二种方式就是把有关信息保存在各自的线程对象中

3)第三种方式就是用Delphi的关键字 threadvar 来声明变量,以利用操作系统级的线程局部存储

1.把信息保存在TThread派生对象中

  将相关信息保存在TThread派生对象中,这是一种线程局部存储可选的技术。相对于使用关键字threadvar的技术,这种方式更加简单、更加有效。例如,你可以在一个线程对象中加入下列信息

type
    TMyThread = class(TThread)
    private
        FLocalInt: Integer;
        FLocalStr: String;
        ..
        ..
    end;

  提示:由于访问线程对象中的数据比访问线程局部变量快10倍,因此,你应当尽可能地把线程专用的信息保存在线程对象中。对于那些只在过程或函数的生存期有意义的变量,应当把它们声明为局部变量

2.threadvar:API线程局部存储

  在前面讲到:虽然对于局部变量,在每个线程中都有一个副本,然而应用程序的全局变量是被所有线程所共享的。例如,假设现在还有一个用于设置和显示一个全局变量的值的过程。如果传递一个字符串给它,就设置这个全局变量;如果传递一个空字符串给它,就显示这个全局变量的值。这个过程定义如下:

var
    GolbalStr: String;
procedure SetShowStr(const S: String);
begin
    if S = '' then
        MessageBox(0, PChar(GlobalStr), 'The string is ...', MB_OK)
    else
        GlobalStr:= S;
end;

  如果在某段代码中调用这个过程,则可能出现一些问题。因为在调用它时,可以先设置而后显示。可问题是,有可能有两个或更多的线程存在。当一个线程调用此过程来设置字符串时,而另一个线程也可能调用此过程来设置GlobalStr 变量的值。当操作系统把CPU时间又分配给前一个线程时,该线程所设置的值有可能已经令人失望的丢失了

  为了解决这个问题,Win32 提供了一种称为线程局部存储的方式,它能使你在第一个运行的线程中创建一个全局变量的拷贝。Delphi利用关键字 threadvar封装此功能。

  在threadvar关键字下你可以声明任何局部存储的变量。下面是局部变量GlobalStr的声明:

threadvar
    GlobalStr: String;

  下面演示了线程局部存储的用法。在程序的主窗体上有一个按钮,单击此按钮,就设置并显示 GlobalStr变量的值。然后再创建一个线程,GlobalStr变量的值有被设置并显示。在创建了一个线程之后,从主线程再次设置并显示GlobalStr变量的值

  先后用var 和 threadvar来声明 GlobalStr变量并运行此程序,你会在输出中看到不同

interface
uses
    Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms,
    Dialogs, StdCtrls;

type
    TMainForm = class(TForm)
        Button1:TButton;
        procedure Button1Click(Sender: TObject);
    private
        {...}
    public
        {...}
    end;

var
    MainForm: TMainForm;

implementation
{$R *.DFM}

var
//threadvar
    GlobalStr: String;

type
    TTLSThread = class(TThread)
    private
        FNewStr: String;
    protected
        Procedure Execute; override;
    public
        cpnstructor Create(Const ANewStr: String);
    end;

procedure SetShowStr(const S: String);
begin
    if S = '' then
        MessageBox(0, PChar(GlobalStr), 'The string is ...', MB_OK)
    else
        GlobalStr:= S;
end;

constructor TTLSThread.Create(const ANewStr: String);
begin
    FNewStr:= ANewStr;
    inherited Create(False);
end;

procedure TTSThread.Execute;
begin
    FreeOnTerminate:= True;
    SetShowStr(FNewStr);
    SetShowStr('');
end;

procedure TMianForm.Button1Click(Sender: TObject);
begin
    SetShowStr('Hello World');
    SetShowStr('');
    TTLSThread.Create('Dilbert');
    Sleep(100);
    SetShowStr('');
end;

end.

  注意:演示程序中,在创建了线程之后,调用了一个Win32 API过程 Sleep()。此过程的声明如下

procedure Sleep(dwMillseconds: DWORD); stdcall;

  Sleep()过程用来告诉操作系统,当前的线程在参数 dwMillseconds指定的时间内不需要分配任何CPU时间。插入这个调用是使很多的任务在发生时,使执行哪个线程有一些随机性

  通常,可以把参数dwMillseconds设置为0,尽管,这并没有使当前的线程真的“睡眠”,但是它使操作系统把CPU时间分给其他优先级相等或者更高的线程

  要小心Sleep()神秘的时间调整问题。Sleep()可能会是你的机器出现特别的问题。这种问题在另一台机器上可能无法再现