WIN32下DELPHI中的多线程【变量存储】(三)

来源:岁月联盟 编辑:zhuzhu 时间:2007-01-16

线程中的变量
    由于每个线程都代表了一个不同的执行路径,因此,最好有一种只限于一个线程内部使用的数据,
    要实现上述目的有以下几种方式:
    1、局部变量(基于栈),很简单,在你的线程函数中你定义的变量既是如此。由于每个线程都在各自的栈中,各个线程将都有一套局部变量的副本,这样,就不会相互影响。对于那些只在过程或函数的生存期有意义的变量,应当把它们声明为局部变量。
    2、存储在线程对象中。还记得CreateThread函数中的lpParameter参数吗,它可以接受一个无类型的指针。结合本文第一章的内容,你应该还记得,它被存储在线程内核对象的上下文结构中,你可以通过CONTEXT结构中的CONTEXT_INTEGER部分的ebx来读取它的地址。
    下面是一段示例代码,用来演示读取CONTEXT结构,这段代码一般用不到,但它可以说明CrateThread函数中的lpParameter被存储的位置

...{
    作者:wudi_1982
    联系方式:wudi_1982@hotmail.com
    转载请著名出处,本代码为演示代码,只贴出了一些关键部分
}

type
  //传递给线程函数的结构和指针的声明
  Tinfo = record
    count : integer;
    x : integer;
    y : integer;
  end;
  Pinfo= ^Tinfo;

var
MyThreadHad : THandle;//一个全局变量,用来保存线程的句柄

//线程函数
function MyThread(info : Pointer):DWORD; stdcall;
var
  i : integer;
begin
  //根据传递来信息决定在窗口的那个位置输出什么信息
  for i := 0 to Pinfo(info)^.count-1 do
    Form1.Image1.Canvas.TextOut(Pinfo(info)^.x,Pinfo(info)^.y,inttostr(i));
  //FreeMem(info);
  Result := 0;
end;

//创建一个线程
procedure TForm1.Button4Click(Sender: TObject);
var
  ppi : Pinfo;
  MyThreadId : DWORD;
begin
  //分配空间并赋初值
  ppi :=AllocMem(sizeof(tinfo));
  ppi^.count := 1000000;
  ppi^.x := 10;
  ppi^.y := 10;
  //创建
  MyThreadHad := CreateThread(nil,0,@MyThread,ppi,CREATE_SUSPENDED,MyThreadId);
  //在窗体上显示线程函数的地址和传递给它的参数的地址
  labThreadAddr.Caption := inttostr( integer(@MyThread));
  labThreadPvparam.Caption := inttostr(integer(ppi));
end;

//读取CONTEXT结构,注意CONTEXT结构是和CPU有关的,我这里测试时,工作在intel的CPU上
procedure TForm1.btnRContextClick(Sender: TObject);
var
  con : _CONTEXT;
begin
  //初始化结构
  con.ContextFlags := CONTEXT_FULL;
  //读取
  GetThreadContext(MyThreadHad,con);
  //显示在窗体的listbox上
  with lbxContextInfo.Items do
  begin
  //  Clear;
    Add('------------CONTEXT--------------');
    Add('');
    Add('CONTEXT_DEBUG_REGISTERS-----');
    Add('dr0:'+#9+IntToStr(con.Dr0));
    Add('dr1:'+#9+IntToStr(con.Dr1));
    Add('dr2:'+#9+IntToStr(con.Dr2));
    Add('dr3:'+#9+IntToStr(con.Dr3));
    Add('dr6:'+#9+IntToStr(con.Dr6));
    Add('dr7:'+#9+IntToStr(con.Dr7));
    add('CONTEXT_SEGMENTS---------');
    Add('segGs:'+#9+inttostr(con.SegGs));
    Add('segFs:'+#9+inttostr(con.Segfs));
    Add('segEs:'+#9+inttostr(con.Seges));
    Add('segDs:'+#9+inttostr(con.Segds));
    Add('CONTEXT_INTEGER.---------');
    Add('edi: '+#9+IntToStr(con.Edi));
    Add('esi: '+#9+IntToStr(con.Esi));
    Add('ebx: '+#9+IntToStr(con.Ebx));
    Add('edx: '+#9+IntToStr(con.Edx));
    Add('ecx: '+#9+IntToStr(con.Ecx));
    Add('eax: '+#9+IntToStr(con.Eax));
    Add('CONTEXT_CONTROL----------');
    Add('Ebp: '+#9+IntToStr(con.Ebp));
    Add('Eip: '+#9+IntToStr(con.Eip));
    Add('segcs: '+#9+IntToStr(con.SegCs));
    Add('EFlags: '+#9+IntToStr(con.EFlags));
    Add('Esp: '+#9+IntToStr(con.Esp));
    Add('SegSs: '+#9+IntToStr(con.SegSs));
  end;

end;

       把上面代码整理之后,添加到你的程序中,你可以发现(如果也是intel的CPU),那么你可以从Eax寄存器读取到线程函数的地址,从Ebx中读取到传递给线程函数的参数地址。
    在DELPHI中的TThread对象的构造函数中,你可以看到这段代码
FHandle := BeginThread(nil, 0, @ThreadProc, Pointer(Self), CREATE_SUSPENDED, FThreadID);
再观察BeginThread的实现,你会发现TThread的调用CreateThread时,将Pointer(Self),也就是TThread对象本身当作线程函数的参数传递过去,换言之,你在TThread的派生类中定义的变量,对于一个线程而言,将存储在这个线程单独的堆栈中,而它在堆栈的地址存储在线程的上下文结构中。
    可以做一个简单的试验,将一个线程生成多次,你可以发现存储在线程对象内部的变量将互不影响。
    说到这里,必须谈论一个问题,效率的问题,我在一本书上曾经看到过这样一段话“由于访问线程对象中的数据比访问线程局部变量要快10倍,因此,你应当尽可能地把线程专用的信息保存在线程对象中。”对此,我一直没有特别理解。如果一定要相信这句话,那我会这么理解,就是存储在线程对象中的变量因为上下文结构记录了它的地址等原因,所以它更快。尽信书不如无书,我还在思考,不过好在这种速度的影响对于通常的使用而言影响不大。

3、在DELPHI中,用Object Pascal的关键字threadvar来声明变量,以利用操作系统级的线程局部存储。
    在前面我们了解到:虽然对于局部变量,在每个线程中都一个副本,然而应用程序的全局变量是被所有线程所共享的。当多个线程对这个全局变量进行访问时,将可能出现很多未知的问题,Win32提供了一种称为线程局部存储的方式,它能使你在第一个运行的线程中创建一个全局变量的拷贝。Delphi利用关键字threadvar封装此功能。在threadvar关键字下你可以声明任何局部存储的变量。
4、全局变量,多线程最让人头疼的地方就是全局变量了,好的同步方式将决定你高效、安全的访问全局变量,虽然上述的threadvar是解决全局变量线程局部存储的一个办法,但在我实际的编码工作中,几乎很少用它,它的局限性太多。多线程访问全局变量的方法将在下一文中详细描述。