本文的内容取自网络,并重新加以整理,在此留存仅仅是方便自己学习和查阅。所有代码均亲自测试 delphi7下测试有效。图片均为自己制作。

多线程应该是编程工作者的基础技能, 但这个基础我从来没学过,所以仅仅是看上去会一些,明白了2+2的时候,其实我还不知道1+1。

开始本应该是一篇洋洋洒洒的文字, 不过我还是提倡先做起来, 在尝试中去理解.
先试试这个:

 
procedure TForm1.Button1Click(Sender: TObject); 
var 
  i: Integer; 
begin 
  for i := 0 to 500000 do 
  begin 
    Canvas.TextOut(10, 10, IntToStr(i)); 
  end; 
end; 

上面程序运行时, 我们的窗体基本是 "死" 的, 可以在你在程序运行期间拖动窗体试试...

Delphi 为我们提供了一个简单的办法(Application.ProcessMessages)来解决这个问题:

 
procedure TForm1.Button1Click(Sender: TObject); 
var 
  i: Integer; 
begin 
  for i := 0 to 500000 do 
  begin 
    Canvas.TextOut(10, 10, IntToStr(i)); 
    Application.ProcessMessages; 
  end; 
end; 

这个 Application.ProcessMessages; 一般用在比较费时的循环中, 它会检查并先处理消息队列中的其他消息.

但这算不上多线程, 譬如: 运行中你拖动窗体, 循环会暂停下来...

在使用多线程以前, 让我们先简单修改一下程序:

 
function MyFun: Integer; 
var 
  i: Integer; 
begin 
  for i := 0 to 500000 do 
  begin 
    Form1.Canvas.Lock; 
    Form1.Canvas.TextOut(10, 10, IntToStr(i)); 
    Form1.Canvas.Unlock; 
  end; 
  Result := 0; 
end; 
 
procedure TForm1.Button1Click(Sender: TObject); 
begin 
  MyFun; 
end; 

细数上面程序的变化:
1、首先这还不是多线程的, 也会让窗体假 "死" 一会;
2、把执行代码写在了一个函数里, 但这个函数不属于 TForm1 的方法, 所以使用 Canvas 是必须冠以名称(Form1);
3、既然是个函数, (不管是否必要)都应该有返回值;
4、使用了 500001 次 Lock 和 Unlock.

Canvas.Lock 好比在说: Canvas(绘图表面)正忙着呢, 其他想用 Canvas 的等会;
Canvas.Unlock : 用完了, 解锁!

在 Canvas 中使用 Lock 和 Unlock 是个好习惯, 在不使用多线程的情况下这无所谓, 但保不准哪天程序会扩展为多线程的; 我们现在学习多线程, 当然应该用.

在 Delphi 中使用多线程有两种方法: 调用 API、使用 TThread 类; 使用 API 的代码更简单.

 
function MyFun(p: Pointer): Integer; stdcall; 
var 
  i: Integer; 
begin 
  for i := 0 to 500000 do 
  begin 
    Form1.Canvas.Lock; 
    Form1.Canvas.TextOut(10, 10, IntToStr(i)); 
    Form1.Canvas.Unlock; 
  end; 
  Result := 0; 
end; 
 
procedure TForm1.Button1Click(Sender: TObject); 
var 
  ID: THandle; 
begin 
  CreateThread(nil, 0, @MyFun, nil, 0, ID); 
end; 

代码分析:
CreateThread 一个线程后, 算上原来的主线程, 这样程序就有两个线程、是标准的多线程了;
CreateThread 第三个参数是函数指针, 新线程建立后将立即执行该函数, 函数执行完毕, 系统将销毁此线程从而结束多线程的故事.

CreateThread 要使用的函数是系统级别的, 不能是某个类(譬如: TForm1)的方法, 并且有严格的格式(参数、返回值)要求, 不管你暂时是不是需要都必须按格式来;
因为是系统级调用, 还要缀上 stdcall, stdcall 是协调参数顺序的, 虽然这里只有一个参数没有顺序可言, 但这是使用系统函数的惯例.

CreateThread 还需要一个 var 参数来接受新建线程的 ID, 尽管暂时没用, 但这也是格式; 其他参数以后再说吧.

这样一个最简单的多线程程序就出来了, 咱们再用 TThread 类实现一次

 
type 
  TMyThread = class(TThread) 
  protected 
    procedure Execute; override; 
  end; 
 
procedure TMyThread.Execute; 
var 
  i: Integer; 
begin 
  FreeOnTerminate := True; {这可以让线程执行完毕后随即释放} 
  for i := 0 to 500000 do 
  begin 
    Form1.Canvas.Lock; 
    Form1.Canvas.TextOut(10, 10, IntToStr(i)); 
    Form1.Canvas.Unlock; 
  end; 
end; 
 
procedure TForm1.Button1Click(Sender: TObject); 
begin 
  TMyThread.Create(False); 
end; 
 

TThread 类有一个抽象方法(Execute), 因而是个抽象类, 抽象类只能继承使用, 上面是继承为 TMyThread.

继承 TThread 主要就是实现抽象方法 Execute(把我们的代码写在里面), 等我们的 TMyThread 实例化后, 首先就会执行 Execute 方法中的代码.

按常规我们一般这样去实例化:

 
procedure TForm1.Button1Click(Sender: TObject); 
var 
  MyThread: TMyThread; 
begin 
  MyThread := TMyThread.Create(False); 
end; 

因为 MyThread 变量在这里毫无用处(并且编译器还有提示), 所以不如直接写做 TMyThread.Create(False);

我们还可以轻松解决一个问题, 如果: TMyThread.Create(True) ?
这样线程建立后就不会立即调用 Execute, 可以在需要的时候再用 Resume 方法执行线程, 譬如:

 
procedure TForm1.Button1Click(Sender: TObject); 
var 
  MyThread: TMyThread; 
begin 
  MyThread := TMyThread.Create(True); 
  MyThread.Resume; 
end; 
 
//可简化为: 
procedure TForm1.Button1Click(Sender: TObject); 
begin 
  with TMyThread.Create(True) do Resume; 
end; 

一、入门
㈠、

 
function CreateThread( 
  lpThreadAttributes: Pointer;           {安全设置} 
  dwStackSize: DWORD;                    {堆栈大小} 
  lpStartAddress: TFNThreadStartRoutine; {入口函数} 
  lpParameter: Pointer;                  {函数参数} 
  dwCreationFlags: DWORD;                {启动选项} 
  var lpThreadId: DWORD                  {输出线程 ID } 
): THandle; stdcall;                     {返回线程句柄} 

在 Windows 上建立一个线程, 离不开 CreateThread 函数;
TThread.Create 就是先调用了 BeginThread (Delphi 自定义的), BeginThread 又调用的 CreateThread.
既然有建立, 就该有释放, CreateThread 对应的释放函数是: ExitThread, 譬如下面代码:

 
procedure TForm1.Button1Click(Sender: TObject); 
begin 
  ExitThread(0); {此句即可退出当前程序, 但不建议这样使用} 
end; 

代码注释:
当前程序是一个进程, 进程只是一个工作环境, 线程是工作者;
每个进程都会有一个启动线程(或叫主线程), 也就是说: 我们之前大量的编码都是写给这个主线程的;
上面的 ExitThread(0); 就是退出这个主线程;
系统不允许一个没有线程的进程存在, 所以程序就退出了.
另外: ExitThread 函数的参数是一个退出码, 这个退出码是给之后的其他函数用的, 这里随便给个无符号整数即可.

或许你会说: 这个 ExitThread 挺好用的; 其实不管是用 API 还是用 TThread 类写多线程, 我们很少用到它; 因为:
1、假如直接使用 API 的 CreateThread, 它执行完入口函数后会自动退出, 无需 ExitThread;
2、用 TThread 类建立的线程又绝不能使用 ExitThread 退出; 因为使用 TThread 建立线程时会同时分配更多资源(譬如你自定义的成员、还有它的祖先类(TObject)分配的资源等等), 如果用 ExitThread 给草草退出了, 这些资源将得不到释放而导致内存泄露. 尽管 Delphi 提供了 EndThread(其内部调用 ExitThread), 这也不需要我们手动操作(假如非要手动操作也是件很麻烦的事情, 因为很多时候你不知道线程是什么时候执行完毕的).
除了 CreateThread, 还有一个 CreateRemoteThread, 可在其他进程中建立线程, 这不应该是现在学习的重点;
现在先集中精力把 CreateThread 的参数搞彻底.

倒着来吧, 先谈谈 CreateThread 将要返回的 "线程句柄".

"句柄" 类似指针, 但通过指针可读写对象, 通过句柄只是使用对象;
有句柄的对象一般都是系统级别的对象(或叫内核对象); 之所以给我们的是句柄而不是指针, 目的只有一个: "安全";
貌似通过句柄能做很多事情, 但一般把句柄提交到某个函数(一般是系统函数)后, 我们也就到此为止很难了解更多了; 事实上是系统并不相信我们.

不管是指针还是句柄, 都不过是内存中的一小块数据(一般用结构描述), 微软并没有公开句柄的结构细节, 猜一下它应该包括: 真实的指针地址、访问权限设置、引用计数等等.

既然 CreateThread 可以返回一个句柄, 说明线程属于 "内核对象".
实际上不管线程属于哪个进程, 它们在系统的怀抱中是平等的; 在优先级(后面详谈)相同的情况下, 系统会在相同的时间间隔内来运行一下每个线程, 不过这个间隔很小很小, 以至于让我们误以为程序是在不间断地运行.

这时你应该有一个疑问: 系统在去执行其他线程的时候, 是怎么记住前一个线程的数据状态的?
有这样一个结构 TContext, 它基本上是一个 CPU 寄存器的集合, 线程是数据就是通过这个结构切换的, 我们也可以通过 GetThreadContext 函数读取寄存器看看.

附上这个结构 TContext(或叫: CONTEXT、_CONTEXT) 的定义:

 
PContext = ^TContext; 
_CONTEXT = record 
  ContextFlags: DWORD; 
  Dr0: DWORD; 
  Dr1: DWORD; 
  Dr2: DWORD; 
  Dr3: DWORD; 
  Dr6: DWORD; 
  Dr7: DWORD; 
  FloatSave: TFloatingSaveArea; 
  SegGs: DWORD; 
  SegFs: DWORD; 
  SegEs: DWORD; 
  SegDs: DWORD; 
  Edi: DWORD; 
  Esi: DWORD; 
  Ebx: DWORD; 
  Edx: DWORD; 
  Ecx: DWORD; 
  Eax: DWORD; 
  Ebp: DWORD; 
  Eip: DWORD; 
  SegCs: DWORD; 
  EFlags: DWORD; 
  Esp: DWORD; 
  SegSs: DWORD; 
end; 

CreateThread 的最后一个参数是 "线程的 ID";
既然可以返回句柄, 为什么还要输出这个 ID? 现在我知道的是:
1、线程的 ID 是唯一的; 而句柄可能不只一个, 譬如可以用 GetCurrentThread 获取一个伪句柄、可以用 DuplicateHandle 复制一个句柄等等.
2、ID 比句柄更轻便.

在主线程中 GetCurrentThreadId、MainThreadID、MainInstance 获取的都是主线程的 ID.
㈡、启动选项

 
function CreateThread( 
  lpThreadAttributes: Pointer; 
  dwStackSize: DWORD; 
  lpStartAddress: TFNThreadStartRoutine; 
  lpParameter: Pointer; 
  dwCreationFlags: DWORD; {启动选项} 
  var lpThreadId: DWORD 
): THandle; stdcall;

CreateThread 的倒数第二个参数 dwCreationFlags(启动选项) 有两个可选值:
0: 线程建立后立即执行入口函数;
CREATE_SUSPENDED: 线程建立后会挂起等待.

可用 ResumeThread 函数是恢复线程的运行; 可用 SuspendThread 再次挂起线程.
这两个函数的参数都是线程句柄, 返回值是执行前的挂起计数.

什么是挂起计数?
SuspendThread 会给这个数 +1; ResumeThread 会给这个数 -1; 但这个数最小是 0.
当这个数 = 0 时, 线程会运行; > 0 时会挂起.
如果被 SuspendThread 多次, 同样需要 ResumeThread 多次才能恢复线程的运行.

在下面的例子中, 有新线程不断给一个全局变量赋随机值;
同时窗体上的 Timer 控件每隔 1/10 秒就把这个变量写在窗体标题;
在这个过程中演示了 ResumeThread、SuspendThread 两个函数.

 
//上面图片中演示的代码。 
unit Unit1; 
 
interface 
 
uses 
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, 
  Dialogs, StdCtrls, ExtCtrls; 
 
type 
  TForm1 = class(TForm) 
    Button1: TButton; 
    Button2: TButton; 
    Button3: TButton; 
    Timer1: TTimer; 
    procedure Button1Click(Sender: TObject); 
    procedure Button2Click(Sender: TObject); 
    procedure Button3Click(Sender: TObject); 
    procedure FormCreate(Sender: TObject); 
    procedure Timer1Timer(Sender: TObject); 
  end; 
 
var 
  Form1: TForm1; 
 
implementation 
 
{$R *.dfm} 
 
var 
  hThread: THandle; {线程句柄} 
  num: Integer;     {全局变量, 用于记录随机数} 
 
{线程入口函数} 
function MyThreadFun(p: Pointer): Integer; stdcall; 
begin 
  while True do {假如线程不挂起, 这个循环将一直循环下去} 
  begin 
    num := Random(100); 
  end; 
  Result := 0; 
end; 
 
{建立并挂起线程} 
procedure TForm1.Button1Click(Sender: TObject); 
var 
  ID: DWORD; 
begin 
  hThread := CreateThread(nil, 0, @MyThreadFun, nil, CREATE_SUSPENDED, ID); 
  Button1.Enabled := False; 
end; 
 
{唤醒并继续线程} 
procedure TForm1.Button2Click(Sender: TObject); 
begin 
  ResumeThread(hThread); 
end; 
 
{挂起线程} 
procedure TForm1.Button3Click(Sender: TObject); 
begin 
  SuspendThread(hThread); 
end; 
 
procedure TForm1.FormCreate(Sender: TObject); 
begin 
  Timer1.Interval := 100; 
end; 
 
procedure TForm1.Timer1Timer(Sender: TObject); 
begin 
  Text := IntToStr(num); 
end; 
 
end.

㈢、入口函数的参数

 
function CreateThread( 
  lpThreadAttributes: Pointer; 
  dwStackSize: DWORD; 
  lpStartAddress: TFNThreadStartRoutine; 
  lpParameter: Pointer;  {入口函数的参数} 
  dwCreationFlags: DWORD; 
  var lpThreadId: DWORD 
): THandle; stdcall;

线程入口函数的参数是个无类型指针(Pointer), 用它可以指定任何数据; 本例是把鼠标点击窗体的坐标传递给线程的入口函数, 每次点击窗体都会创建一个线程.

运行效果图:

 
//上面演示的代码 
unit Unit1; 
 
interface 
 
uses 
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, 
  Dialogs; 
 
type 
  TForm1 = class(TForm) 
    procedure FormMouseUp(Sender: TObject; Button: TMouseButton; 
      Shift: TShiftState; X, Y: Integer); 
  end; 
 
var 
  Form1: TForm1; 
 
implementation 
 
{$R *.dfm} 
 
var 
  pt: TPoint; {这个坐标点将会已指针的方式传递给线程, 它应该是全局的} 
 
function MyThreadFun(p: Pointer): Integer; stdcall; 
var 
  i: Integer; 
  pt2: TPoint;       {因为指针参数给的点随时都在变, 需用线程的局部变量存起来} 
begin 
  pt2 := PPoint(p)^; {转换} 
  for i := 0 to 1000000 do 
  begin 
    with Form1.Canvas do begin 
      Lock; 
      TextOut(pt2.X, pt2.Y, IntToStr(i)); 
      Unlock; 
    end; 
  end; 
  Result := 0; 
end; 
 
procedure TForm1.FormMouseUp(Sender: TObject; Button: TMouseButton; 
  Shift: TShiftState; X, Y: Integer); 
var 
  ID: DWORD; 
begin 
  pt := Point(X, Y); 
  CreateThread(nil, 0, @MyThreadFun, @pt, 0, ID); 
  {下面这种写法更好理解, 其实不必, 因为 PPoint 会自动转换为 Pointer 的} 
  //CreateThread(nil, 0, @MyThreadFun, Pointer(@pt), 0, ID); 
end; 
 
end.

这个例子还有不严谨的地方: 当一个线程 Lock 窗体的 Canvas 时, 其他线程在等待; 线程在等待时, 其中的计数也还在增加. 这也就是说: 现在并没有去处理线程的同步; 同步是多线程中最重要的课题, 快到了.

另外有个小技巧: 线程函数的参数是个 32 位(4个字节)的指针, 仅就本例来讲, 可以让它的 "高16位" 和 "低16位" 分别携带 X 和 Y; 这样就不需要哪个全局的 pt 变量了.
其实在 Windows 的消息中就是这样传递坐标的, 在 Windows 的消息中一般高字节是 Y、低字节是 X; 咱们这么来吧, 这样还可以使用给消息准备的一些方便的函数.

重写本例代码(当然运行效果和窗体文件都是一样的):

 
unit Unit1; 
 
interface 
 
uses 
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, 
  Dialogs; 
 
type 
  TForm1 = class(TForm) 
    procedure FormMouseUp(Sender: TObject; Button: TMouseButton; 
      Shift: TShiftState; X, Y: Integer); 
  end; 
 
var 
  Form1: TForm1; 
 
implementation 
 
{$R *.dfm} 
 
function MyThreadFun(p: Pointer): Integer; stdcall; 
var 
  i: Integer; 
  x,y: Word; 
begin 
  x := LoWord(Integer(p)); 
  y := HiWord(Integer(p)); 
  {如果不使用 LoWord、HiWord 函数可以像下面这样: } 
  //x := Integer(p); 
  //y := Integer(p) shr 16; 
  for i := 0 to 1000000 do 
  begin 
    with Form1.Canvas do begin 
      Lock; 
      TextOut(x, y, IntToStr(i)); 
      Unlock; 
    end; 
  end; 
  Result := 0; 
end; 
 
procedure TForm1.FormMouseUp(Sender: TObject; Button: TMouseButton; 
  Shift: TShiftState; X, Y: Integer); 
var 
  ID: DWORD; 
  num: Integer; 
begin 
  num := MakeLong(X, Y); 
  {如果不使用 MekeLong、MakeWParam、MakeLParam、MakeResult 等函数, 可以像下面这样: } 
  //num := Y shl 16 + X; 
  CreateThread(nil, 0, @MyThreadFun, Ptr(num), 0, ID); 
  {上面的 Ptr 是专门将一个数字转换为指针的函数, 当然也可以这样: } 
  //CreateThread(nil, 0, @MyThreadFun, Pointer(num), 0, ID); 
end; 
 
end.

㈣、入口函数的指针

function CreateThread( 
  lpThreadAttributes: Pointer; 
  dwStackSize: DWORD; 
  lpStartAddress: TFNThreadStartRoutine; {入口函数的指针} 
  lpParameter: Pointer;  
  dwCreationFlags: DWORD; 
  var lpThreadId: DWORD 
): THandle; stdcall;

到了入口函数了, 学到这个地方, 我查了一个入口函数的标准定义, 这个函数的标准返回值应该是 DWORD, 不过这函数在 Delphi 的 System 单元定义的是: TThreadFunc = function(Parameter: Pointer): Integer; 我以后会尽量使用 DWORD 做入口函数的返回值.

这个返回值有什么用呢?
等线程退出后, 我们用 GetExitCodeThread 函数获取的退出码就是这个返回值!

如果线程没有退出, GetExitCodeThread 获取的退出码将是一个常量 STILL_ACTIVE (259); 这样我们就可以通过退出码来判断线程是否已退出.

还有一个问题: 前面也提到过, 线程函数不能是某个类的方法! 假如我们非要线程去执行类中的一个方法能否实现呢?
尽管可以用 Addr(类名.方法名) 或 MethodAddress('published 区的方法名') 获取类中方法的地址, 但都不能当做线程的入口函数, 原因可能是因为类中的方法的地址是在实例化为对象时动态分配的.
后来换了个思路, 其实很简单: 在线程函数中再调用方法不就得了, 估计 TThread 也应该是这样.

下面的例子就尝试了用线程调用 TForm1 类中的方法, 并测试了退出码的相关问题.

 
unit Unit1; 
 
interface 
 
uses 
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, 
  Dialogs, StdCtrls; 
 
type 
  TForm1 = class(TForm) 
    Button1: TButton; 
    Button2: TButton; 
    procedure Button1Click(Sender: TObject); 
    procedure Button2Click(Sender: TObject); 
    private 
      procedure FormProc; {准备给线程使用的方法} 
  end; 
 
var 
  Form1: TForm1; 
 
implementation 
 
{$R *.dfm} 
 
var 
  hThread: THandle; 
 
{线程入口函数} 
function MyThreadFun(p: Pointer): DWORD; stdcall; 
begin 
  Form1.FormProc; {调用 TForm1 类的方法} 
  Result := 99;   {这个返回值将成为线程的退出代码, 99 是我随意给的数字} 
end; 
 
{TForm1 的方法, 本例中是给线程的入口函数调用的} 
procedure TForm1.FormProc; 
var 
  i: Integer; 
begin 
  for i := 0 to 200000 do 
  begin 
    with Form1.Canvas do begin 
      Lock; 
      TextOut(10, 10, IntToStr(i)); 
      Unlock; 
    end; 
  end; 
end; 
 
{建立并执行线程} 
procedure TForm1.Button1Click(Sender: TObject); 
var 
  ID: DWORD; 
begin 
  hThread := CreateThread(nil, 0, @MyThreadFun, nil, 0, ID); 
end; 
 
{获取线程的退出代码, 并判断线程是否退出} 
procedure TForm1.Button2Click(Sender: TObject); 
var 
  ExitCode: DWORD; 
begin 
  GetExitCodeThread(hThread, ExitCode); 
 
  if hThread = 0 then 
  begin 
    Text := '线程还未启动'; 
    Exit; 
  end; 
 
  if ExitCode = STILL_ACTIVE then 
    Text := Format('线程退出代码是: %d, 表示线程还未退出', [ExitCode]) 
  else 
    Text := Format('线程已退出, 退出代码是: %d', [ExitCode]); 
end; 
 
end.

㈤、堆栈大小

function CreateThread( 
  lpThreadAttributes: Pointer; 
  dwStackSize: DWORD;  {堆栈大小} 
  lpStartAddress: TFNThreadStartRoutine;  
  lpParameter: Pointer;  
  dwCreationFlags: DWORD; 
  var lpThreadId: DWORD 
): THandle; stdcall;

CreateThread 的第二个参数是分配给线程的堆栈大小.
这首先这可以让我们知道: 每个线程都有自己独立的堆栈(也拥有自己的消息队列).

什么是堆栈? 其实堆是堆、栈是栈, 有时 "栈" 也被叫做 "堆栈".
它们都是进程中的内存区域, 主要是存取方式不同(栈:先进后出; 堆:先进先出);
"栈"(或叫堆栈)适合存取临时而轻便的变量, 主要用来储存局部变量; 譬如 for i := 0 to 99 do 中的 i 就只能存于栈中, 你把一个全局的变量用于 for 循环计数是不可以的.

现在我们知道了线程有自己的 "栈", 并且在建立线程时可以分配栈的大小.

前面所有的例子中, 这个值都是 0, 这表示使用系统默认的大小, 默认和主线程栈的大小一样, 如果不够用会自动增长;
那主线程的栈有多大? 这个值是可以设定的: Project -> Options -> linker -> memory size(如图)

栈是私有的但堆是公用的, 如果不同的线程都来使用一个全局变量有点乱套;
为解决这个问题 Delphi 为我们提供了一个类似 var 的 ThreadVar 关键字, 线程在使用 ThreadVar 声明的全局变量时会在各自的栈中留一个副本, 这样就解决了冲突. 不过还是尽量使用局部变量, 或者在继承 TThread 时使用类的成员变量, 因为 ThreadVar 的效率不好, 据说比局部变量能慢 10 倍.

在下面的例子就测试了用 var 和 ThreadVar 定义变量的不同.
使用 var 效果图:

使用 ThreadVar 效果图:

 
unit Unit1; 
 
interface 
 
uses 
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, 
  Dialogs, StdCtrls; 
 
type 
  TForm1 = class(TForm) 
    Button1: TButton; 
    procedure Button1Click(Sender: TObject); 
  end; 
 
var 
  Form1: TForm1; 
 
implementation 
 
{$R *.dfm} 
 
//var num: Integer;     {全局变量} 
threadvar num: Integer; {支持多线程的全局变量} 
 
function MyThreadFun(p: Pointer): DWORD; stdcall; 
var 
  py: Integer; 
begin 
  py := Integer(p); 
  while True do 
  begin 
    Inc(num); 
    with Form1.Canvas do begin 
      Lock; 
      TextOut(20, py, IntToStr(num)); 
      Unlock; 
    end; 
    Sleep(1000); {然线程挂起 1 秒钟再继续} 
  end; 
end; 
 
procedure TForm1.Button1Click(Sender: TObject); 
var 
  ID: DWORD; 
begin 
  {借入口函数的参数传递了一个坐标点中的 Y 值, 以让各线程把结果输出在不同位置} 
  CreateThread(nil, 0, @MyThreadFun, Ptr(20), 0, ID); 
  CreateThread(nil, 0, @MyThreadFun, Ptr(40), 0, ID); 
  CreateThread(nil, 0, @MyThreadFun, Ptr(60), 0, ID); 
end; 
 
end.

㈥、安全设置

function CreateThread( 
  lpThreadAttributes: Pointer; {安全设置} 
  dwStackSize: DWORD; 
  lpStartAddress: TFNThreadStartRoutine;  
  lpParameter: Pointer;  
  dwCreationFlags: DWORD; 
  var lpThreadId: DWORD 
): THandle; stdcall;

CreateThread 的第一个参数 lpThreadAttributes 是指向 TSecurityAttributes 结构的指针, 一般都是置为 nil, 这表示没有访问限制; 该结构的定义是:

 
//TSecurityAttributes(又名: SECURITY_ATTRIBUTES、_SECURITY_ATTRIBUTES) 
_SECURITY_ATTRIBUTES = record 
  nLength: DWORD;                {结构大小} 
  lpSecurityDescriptor: Pointer; {默认 nil; 这是另一个结构 TSecurityDescriptor 的指针} 
  bInheritHandle: BOOL;          {默认 False, 表示不可继承} 
end; 
 
//TSecurityDescriptor(又名: SECURITY_DESCRIPTOR、_SECURITY_DESCRIPTOR) 
_SECURITY_DESCRIPTOR = record 
  Revision: Byte; 
  Sbz1: Byte; 
  Control: SECURITY_DESCRIPTOR_CONTROL; 
  Owner: PSID; 
  Group: PSID; 
  Sacl: PACL; 
  Dacl: PACL; 
end;

够复杂的, 但我们在多线程编程时不需要去设置它们, 大都是使用默认设置(也就是赋值为 nil).

我觉得有必要在此刻了解的是: 建立系统内核对象时一般都有这个属性(TSecurityAttributes);
在接下来多线程的课题中要使用一些内核对象, 不如先盘点一下, 到时碰到这个属性时给个 nil 即可, 不必再费神.

 
{建立事件} 
function CreateEvent( 
  lpEventAttributes: PSecurityAttributes; {!} 
  bManualReset: BOOL; 
  bInitialState: BOOL; 
  lpName: PWideChar 
): THandle; stdcall; 
 
{建立互斥} 
function CreateMutex( 
  lpMutexAttributes: PSecurityAttributes; {!} 
  bInitialOwner: BOOL; 
  lpName: PWideChar 
): THandle; stdcall; 
 
{建立信号} 
function CreateSemaphore( 
  lpSemaphoreAttributes: PSecurityAttributes; {!} 
  lInitialCount: Longint; 
  lMaximumCount: Longint; 
  lpName: PWideChar 
): THandle; stdcall; 
 
{建立等待计时器} 
function CreateWaitableTimer( 
  lpTimerAttributes: PSecurityAttributes; {!} 
  bManualReset: BOOL; 
  lpTimerName: PWideChar 
): THandle; stdcall; 

上面的四个系统内核对象(事件、互斥、信号、计时器)都是线程同步的手段, 从这也能看出处理线程同步的复杂性; 不过这还不是全部, Windows Vista 开始又增加了 Condition variables(条件变量)、Slim Reader-Writer Locks(读写锁)等同步手段.

不过最简单、最轻便(速度最快)的同步手段还是 CriticalSection(临界区), 但它不属于系统内核对象, 当然也就没有句柄、没有 TSecurityAttributes 这个安全属性, 这也导致它不能跨进程使用; 不过写多线程时一般不用跨进程, 所以 CriticalSection 应该是最常用的同步手段.

二、临界区。
先看一段程序, 代码文件:

 
unit Unit1; 
 
interface 
 
uses 
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, 
  Dialogs, StdCtrls; 
 
type 
  TForm1 = class(TForm) 
    ListBox1: TListBox; 
    Button1: TButton; 
    procedure FormCreate(Sender: TObject); 
    procedure Button1Click(Sender: TObject); 
  end; 
 
var 
  Form1: TForm1; 
 
implementation 
 
{$R *.dfm} 
 
function MyThreadFun(p: Pointer): DWORD; stdcall; 
var 
  i: Integer; 
begin 
  for i := 0 to 99 do Form1.ListBox1.Items.Add(IntToStr(i)); 
  Result := 0; 
end; 
 
procedure TForm1.Button1Click(Sender: TObject); 
var 
  ID: DWORD; 
begin 
  CreateThread(nil, 0, @MyThreadFun, nil, 0, ID); 
  CreateThread(nil, 0, @MyThreadFun, nil, 0, ID); 
  CreateThread(nil, 0, @MyThreadFun, nil, 0, ID); 
end; 
 
procedure TForm1.FormCreate(Sender: TObject); 
begin 
  ListBox1.Align := alLeft; 
end; 
 
end.

在这段程序中, 有三个线程几乎是同时建立, 向窗体中的 ListBox1 中写数据, 最后写出的结果是这样的:

能不能让它们别打架, 一个完了另一个再来? 这就要用到多线程的同步技术.
前面说过, 最简单的同步手段就是 "临界区".

先说这个 "同步"(Synchronize), 首先这个名字起的不好, 我们好像需要的是 "异步"; 其实异步也不准确...
管它叫什么名字呢, 它的目的就是保证不冲突、有次序、都发生.

"临界区"(CriticalSection): 当把一段代码放入一个临界区, 线程执行到临界区时就独占了, 让其他也要执行此代码的线程先等等; 这和前面用的 Lock 和 UnLock 差不多; 使用格式如下:

 
var CS: TRTLCriticalSection;   {声明一个 TRTLCriticalSection 结构类型变量; 它应该是全局的} 
InitializeCriticalSection(CS); {初始化} 
EnterCriticalSection(CS);      {开始: 轮到我了其他线程走开} 
LeaveCriticalSection(CS);      {结束: 其他线程可以来了} 
DeleteCriticalSection(CS);     {删除: 注意不能过早删除} 
 
//也可用 TryEnterCriticalSection 替代 EnterCriticalSection.

用上临界区, 重写上面的代码, 运行效果图:

 
//用临界区重写后的代码文件: 
unit Unit1; 
 
interface 
 
uses 
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, 
  Dialogs, StdCtrls; 
 
type 
  TForm1 = class(TForm) 
    ListBox1: TListBox; 
    Button1: TButton; 
    procedure FormCreate(Sender: TObject); 
    procedure FormDestroy(Sender: TObject); 
    procedure Button1Click(Sender: TObject); 
  end; 
 
var 
  Form1: TForm1; 
 
implementation 
 
{$R *.dfm} 
 
var 
  CS: TRTLCriticalSection; 
 
function MyThreadFun(p: Pointer): DWORD; stdcall; 
var 
  i: Integer; 
begin 
  EnterCriticalSection(CS); 
  for i := 0 to 99 do Form1.ListBox1.Items.Add(IntToStr(i)); 
  LeaveCriticalSection(CS); 
  Result := 0; 
end; 
 
procedure TForm1.Button1Click(Sender: TObject); 
var 
  ID: DWORD; 
begin 
  CreateThread(nil, 0, @MyThreadFun, nil, 0, ID); 
  CreateThread(nil, 0, @MyThreadFun, nil, 0, ID); 
  CreateThread(nil, 0, @MyThreadFun, nil, 0, ID); 
end; 
 
procedure TForm1.FormCreate(Sender: TObject); 
begin 
  ListBox1.Align := alLeft; 
  InitializeCriticalSection(CS); 
end; 
 
procedure TForm1.FormDestroy(Sender: TObject); 
begin 
  DeleteCriticalSection(CS); 
end; 
 
end.

Delphi 在 SyncObjs 单元给封装了一个 TCriticalSection 类, 用法差不多, 代码如下:

 
unit Unit1; 
 
interface 
 
uses 
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, 
  Dialogs, StdCtrls; 
 
type 
  TForm1 = class(TForm) 
    ListBox1: TListBox; 
    Button1: TButton; 
    procedure FormCreate(Sender: TObject); 
    procedure FormDestroy(Sender: TObject); 
    procedure Button1Click(Sender: TObject); 
  end; 
 
var 
  Form1: TForm1; 
 
implementation 
 
{$R *.dfm} 
 
uses SyncObjs; 
 
var 
  CS: TCriticalSection; 
 
function MyThreadFun(p: Pointer): DWORD; stdcall; 
var 
  i: Integer; 
begin 
  CS.Enter; 
  for i := 0 to 99 do Form1.ListBox1.Items.Add(IntToStr(i)); 
  CS.Leave; 
  Result := 0; 
end; 
 
procedure TForm1.Button1Click(Sender: TObject); 
var 
  ID: DWORD; 
begin 
  CreateThread(nil, 0, @MyThreadFun, nil, 0, ID); 
  CreateThread(nil, 0, @MyThreadFun, nil, 0, ID); 
  CreateThread(nil, 0, @MyThreadFun, nil, 0, ID); 
end; 
 
procedure TForm1.FormCreate(Sender: TObject); 
begin 
  ListBox1.Align := alLeft; 
  CS := TCriticalSection.Create; 
end; 
 
procedure TForm1.FormDestroy(Sender: TObject); 
begin 
  CS.Free; 
end; 
 
end.

三、等待函数 WaitForSingleObject
一下子跳到等待函数 WaitForSingleObject, 是因为下面的 Mutex、Semaphore、Event、WaitableTimer 等同步手段都要使用这个函数; 不过等待函数可不止 WaitForSingleObject 它一个, 但它最简单.

 
function WaitForSingleObject( 
  hHandle: THandle;      {要等待的对象句柄} 
  dwMilliseconds: DWORD  {等待的时间, 单位是毫秒} 
): DWORD; stdcall;       {返回值如下:} 
 
WAIT_OBJECT_0  {等着了, 本例中是: 等的那个进程终于结束了} 
WAIT_TIMEOUT   {等过了点(你指定的时间), 也没等着} 
WAIT_ABANDONED {好不容易等着了, 但人家还是不让咱执行; 这一般是互斥对象} 
 
//WaitForSingleObject 的第二个参数一般给常数值 INFINITE, 表示一直等下去, 死等. 

WaitForSingleObject 等待什么? 在多线程里就是等待另一个线程的结束, 快来执行自己的代码; 不过它可以等待的对象可不止线程; 这里先来一个等待另一个进程结束的例子, 运行效果图:

 
//WaitForSingleObject的示例代码文件: 
 
unit Unit1; 
 
interface 
 
uses 
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, 
  Dialogs, StdCtrls; 
 
type 
  TForm1 = class(TForm) 
    Button1: TButton; 
    procedure Button1Click(Sender: TObject); 
  end; 
 
var 
  Form1: TForm1; 
 
implementation 
 
{$R *.dfm} 
 
var 
  hProcess: THandle; {进程句柄} 
 
{等待一个指定句柄的进程什么时候结束} 
function MyThreadFun(p: Pointer): DWORD; stdcall; 
begin 
  if WaitForSingleObject(hProcess, INFINITE) = WAIT_OBJECT_0 then 
    Form1.Text := Format('进程 %d 已关闭', [hProcess]); 
  Result := 0; 
end; 
 
{启动一个进程, 并建立新线程等待它的结束} 
procedure TForm1.Button1Click(Sender: TObject); 
var 
  pInfo: TProcessInformation; 
  sInfo: TStartupInfo; 
  Path: array[0..MAX_PATH-1] of Char; 
  ThreadID: DWORD; 
begin 
  {先获取记事本的路径} 
  GetSystemDirectory(Path, MAX_PATH); 
  StrCat(Path, '/notepad.exe'); 
 
  {用 CreateProcess 打开记事本并获取其进程句柄, 然后建立线程监视} 
  FillChar(sInfo, SizeOf(sInfo), 0); 
  if CreateProcess(Path, nil, nil, nil, False, 0, nil, nil, sInfo, pInfo) then 
  begin 
    hProcess := pInfo.hProcess;                           {获取进程句柄} 
    Text := Format('进程 %d 已启动', [hProcess]);  
    CreateThread(nil, 0, @MyThreadFun, nil, 0, ThreadID); {建立线程监视} 
  end; 
end; 
 
end.

http://blog.csdn.net/renxucai/article/details/5723414

delphi之多线程编程(尚未学习)的更多相关文章

  1. pthread多线程编程的学习小结

    pthread多线程编程的学习小结  pthread 同步3种方法: 1 mutex 2 条件变量 3 读写锁:支持多个线程同时读,或者一个线程写     程序员必上的开发者服务平台 —— DevSt ...

  2. clone的fork与pthread_create创建线程有何不同&pthread多线程编程的学习小结(转)

    进程是一个指令执行流及其执行环境,其执行环境是一个系统资源的集合,这些资源在Linux中被抽 象成各种数据对象:进程控制块.虚存空间.文件系统,文件I/O.信号处理函数.所以创建一个进程的 过程就是这 ...

  3. Delphi 实现多线程编程的线程类 TThread

    http://blog.csdn.net/henreash/article/details/3183119 Delphi中有一个线程类TThread是用来实现多线程编程的,这个绝大多数Delphi书藉 ...

  4. DELPHI下多线程编程的几个思维误区(QDAC)

    有几个网友私下问我一些有关线程的事情.过节写个东西上来大家交流. 思维误区1,自己新建的THREAD是线程,自己的主程序不是线程. 很多人在多线程编程没有把主线程也当作线程.其实主线程也是线程.看起来 ...

  5. Java多线程编程核心技术---学习分享

    继承Thread类实现多线程 public class MyThread extends Thread { @Override public void run() { super.run(); Sys ...

  6. java多线程编程核心技术学习-1

    实现多线程的两种方式 继承Thread类,重写Thread类中的run方法 public class MyThread extends Thread{ @Override public void ru ...

  7. delphi之多线程编程

    本文的内容取自网络,并重新加以整理,在此留存仅仅是方便自己学习和查阅.所有代码均亲自测试 delphi7下测试有效.图片均为自己制作. 多线程应该是编程工作者的基础技能, 但这个基础我从来没学过,所以 ...

  8. delphi的多线程编程

    多线程的基本概念 win 98/nt/2000/xp 是个多任务操作系统,也就是:一个进程可以划分为多个线程,每个线程轮流占用cpu 运行时间和资源,或者说,把cpu 时间划成片,每个片分给不同的线程 ...

  9. [转]Delphi多线程编程入门(二)——通过调用API实现多线程

    以下是一篇很值得看的关于Delphi多线程编程的文章,内容很全面,建议收藏. 一.入门 ㈠. function CreateThread(    lpThreadAttributes: Pointer ...

随机推荐

  1. [Now] Update an application hosted with Zeit’s Now

    Because now deploys are immutable, you can’t push changes to a running instance - you just push a ne ...

  2. ConcurrentLinkedQueue的实现原理分析

    1.    引言 在并发编程中我们有时候需要使用线程安全的队列.如果我们要实现一个线程安全的队列有两种实现方式一种是使用阻塞算法,另一种是使用非阻塞算法.使用阻塞算法的队列可以用一个锁(入队和出队用同 ...

  3. js进阶 11-7 jquery如何获取和改变元素的位置

    js进阶 11-7  jquery如何获取和改变元素的位置 一.总结 一句话总结:jquery中匿名函数中的index参数是什么意思.jquery对象多集合,故index为所选元素的下标. 1.jqu ...

  4. [React Router v4] Use the React Router v4 Link Component for Navigation Between Routes

    If you’ve created several Routes within your application, you will also want to be able to navigate ...

  5. Codeforces Round #313 (Div. 2) 560C Gerald's Hexagon(脑洞)

    C. Gerald's Hexagon time limit per test 2 seconds memory limit per test 256 megabytes input standard ...

  6. js进阶 9-14 js如何实现下拉列表多选移除

    js进阶 9-14 js如何实现下拉列表多选移除 一.总结 一句话总结: 1.js如何实现下拉列表多选移除? 把这个下拉列表中的option移除,然后加到另外一个下拉列表(文字)中去.remove方法 ...

  7. MAC终端:如何调整字体大小和终端样式

    1.字体 进入终端后comman键和+键的组合可以增大显示 2.样式  

  8. AppStoreID--安装URL--应用更新URL--应用评分URL

    #define AppStoreID @"987353224" //应用安装URL #define AppStoreInstallURLFormat @"https:// ...

  9. 欢迎阅读 Erlang OTP 设计原理文档

    http://erldoc.com/doc/otp-design-principles/index.html 原文: OTP Design Principles 翻译: ShiningRay 有任何问 ...

  10. Docker for Windows 安装

    原文:Docker for Windows 安装 前言: 环境:windows10专业版 64位 正文: 官方下载地址:https://hub.docker.com/editions/communit ...