线程杂谈

 
前言
由于项目的关系,所以和线程的接触颇多,常常遇到问题,常常看 TThread 的代码,又常常想一些办法来解决遇到的问题,所以就有了这篇文章。
 
正文
我们常有工作线程和主线程之分,工作线程负责作一些后台操作,比如接收邮件;主线程负责界面上的一些显示。工作线程的好处在某些时候是不言而喻的,你的主界面可以响应任何操作,而背后的线程却在默默地工作。
VCL 中,工作线程执行在 Execute 方法中,你必须从 TThread 继承一个类并覆盖 Execute 方法,在这个方法中,所有代码都是在另一个线程中执行的,除此之外,你的线程类的其他方法都在主线程执行,包括构造方法,析构方法, Resume 等,很多人常常忽略了这一点。
最简单的一个线程类如下:
TMyThread = class(TThread)
protected
    procedure Execute; override;
end;
Execute 中的代码,有一个技术要点,如果你的代码执行时间很短,像这样, Sleep(1000) ,那没有关系;如果是这样 Sleep(10000) 10 秒,那么你就不能直接这样写了,须把这 10 秒拆分成 10 1 秒,然后判断 Terminated 属性,像下面这样:
procedure TMyThread.Execute;
var
 i: Integer;
begin
 for i := 0 to 9 do
    if not Terminated then
      Sleep(1000)
    else
      Break;
end;
这样写有什么好处呢,想想你要关闭程序,在关闭的时候调用 MyThread.Free ,这个时候线程并没有马上结束,它调用 WaitFor ,等待 Execute 执行完后才能释放。你的程序就必须等 10 秒以后才能关闭,受得了吗。如果像上面那样写,在程序关闭时,调用 Free 之后,它顶多再等一秒就会关闭。为什么?答案得去线程类的 Destroy 中找,它会先调用 Terminate 方法,在这个方法里面它把 Terminated 设为 True (仅此而已,很多人以为是结束线程,其实不是)。请记住这一切是在主线程中操作的,所以和 Execute 是并行执行的。既然 Terminated 属性已为 Ture ,那么在 Execute 中判断之后,当然就 Break 了, Execute 执行完毕,线程类也正常释放。
或者有人说, TThread 可以设 FreeOnTerminate 属性为 True ,线程类就能自动释放。除非你的线程执行的任务很简单,不然,还是不要去理会这个属性,一切由你来操作,才能使线程更灵活强大。
 
接下来的问题是如何使工作线程和主线程很好的通信,很多时候主线程必须得到工作线程的通知,才能做出响应。比如接收邮件,工作线程向服务器收取邮件,收取完毕之后,它得通知主线程收到多少封邮件,主线程才能弹出一个窗口通知用户。
VCL 中,我们可以用两种方法,一种是向主线程中的窗体发送消息,另一种是使用异步事件。第一种方法其实没有第二种来得方便。想想线程类中的 OnTerminate 事件,这个事件由线程函数的堆栈引起,却在主线程执行。
事实上,真正的线程函数是这个:
function ThreadProc(Thread: TThread): Integer;
函数里面有 Thread.Execute ,这就是为什么 Execute 是在其他线程中执行,该方法执行之后,有如下语句:
Thread.DoTerminate;
而线程类的 DoTerminate 方法里面是
if Assigned(FOnTerminate) then Synchronize(CallOnTerminate);
显然 Synchronize 方法使得 CallOnTerminate 在主线程中执行,而 CallOnTerminate 里面的代码其实就是:
if Assigned(FOnTerminate) then FOnTerminate(Self);
只要 Execute 方法一执行完就发生 OnTerminate 事件。不过有一点是必须注意, OnTerminate 事件发生后,线程类不一定会释放,只有在 FreeOnTerminate True 之后,才会 Thread.Free 。看一下 ThreadProc 函数就知道。
依照 Onterminate 事件,我们可以设计自己的异步事件。
Synchronize 方法只能传进一个无参数的方法类型,但我们的事件经常是要带一些参数的,这个稍加思考就可以得到解决,即在线程类中保存参数,触发事件前先设置参数,再调用异步事件,参数复杂的可以用记录或者类来实现。
假设这样,上面的代码每睡一秒,线程即向外面引发一次事件,我们的类可以这样设计:
  TSecondEvent =  procedure  (Second: Integer)  of  object ;
  TMyThread = 
class (TThread)
  
private
    FSecond: Integer;
    FSecondEvent: TSecondEvent;
    
procedure  CallSecondEvent;
  
protected
    
procedure  Execute; override;
  
public
    
property  SencondEvent: TSecondEvent  read  FSecondEvent
      
write  FSecondEvent;
  
end ;

{ TMyThread }

procedure  TMyThread.CallSecondEvent;
begin
  
if  Assigned(FSecondEvent)  then
    FSecondEvent(FSecond);
end ;

procedure  TMyThread.Execute;
var
  i: Integer;
begin
  
for  i :=  0  to  9  do
    
if  not  Terminated  then
    
begin
      Sleep(
1000 );
      FSecond := i;
      Synchronize(CallSecondEvent);
    
end
    
else
      Break;
end ;
 
在主窗体中假设我们这样操作线程:
procedure  TForm1.Button1Click(Sender: TObject);
begin
  MyThread := TMyThread.Create(true);
  MyThread.OnTerminate :=  ThreadTerminate;
  MyThread.SencondEvent := SecondEvent;
  MyThread.Resume;
end ;

procedure  TForm1.ThreadTerminate(Sender: TObject);
begin
  ShowMessage(
'ok' );
end ;

procedure  TForm1.SecondEvent(Second: Integer);
begin
  Edit1.Text := IntToStr(Second);
end ;
我们将每隔一秒就得到一次通知并在 Edit 中显示出来。
 
现在我们已经知道如何正确使用 Execute 方法,以及如何在主线程与工作线程之间通信了。但问题还没有结束,有一种情况出乎我的意料之外,即如果线程中有一些资源, Execute 正在使用这些资源,而主线程要释放这个线程,这个线程在释放的过程中会释放掉资源。想想会不会有问题呢,两个线程,一个在使用资源,一个在释放资源,会出现什么情况呢,
用下面代码来说明:
type
  TMyClass = 
class
  
private
    FSecond: Integer;
  
public
    
procedure  SleepOneSecond;
  
end ;

  TMyThread = 
class (TThread)
  
private
    FMyClass: TMyClass;
  
protected
    
procedure  Execute; override;
  
public
    
constructor  MyCreate(CreateSuspended: Boolean);
    
destructor  Destroy; override;
  
end ;

implementation

{ TMyThread }

constructor  TMyThread.MyCreate(CreateSuspended: Boolean);
begin
  
inherited  Create(CreateSuspended);
  FMyClass := TMyClass.Create;
end ;

destructor  TMyThread.Destroy;
begin
  FMyClass.Free;
  FMyClass := 
nil ;
  
inherited ;
end ;

procedure  TMyThread.Execute;
var
  i: Integer;
begin
  
for  i :=  0  to  9  do
    FMyClass.SleepOneSecond;
end ;

{ TMyClass }

procedure  TMyClass.SleepOneSecond;
begin
  FSecond := 
0 ;
  Sleep(
1000 );
end ;

end .
 
用下面的代码来调用上面的类:
procedure  TForm1.Button1Click(Sender: TObject);
begin
  MyThread := TMyThread.MyCreate(true);
  MyThread.OnTerminate :=  ThreadTerminate;
  MyThread.Resume;
end ;

procedure  TForm1.Button2Click(Sender: TObject);
begin
  MyThread.Free;
end ;
先点击 Button1 创建一个线程,再点击 Button2 释放该类,出现什么情况呢,违法访问,是的, MyThread.Free 时, MyClass 被释放掉了
FMyClass.Free;
FMyClass :=  nil ;
而此时 Execute 却还在执行,并且调用 MyClass 的方法,当然就出现违法访问。对于这种情况,有什么办法来防止呢,我想到一种方法,即在线程类中使用一个成员,假设为 FFinished ,在 Execute 方法中有如下的形式:
FFinished := False;
try
//... ...
finally
FFinished := True;
End;
接着在线程类的 Destroy 中有如下形式:
While not FFinished do
 Sleep(100);
MyClass.Free;
 
这样便能保证 MyClass 能被正确释放。
 
线程是一种很有用的技术。但使用不当,常使人头痛。在 CSDN 论坛上看到一些人问,我的窗口在线程中调用为什么出错,主线程怎么向其他线程发送消息等等,其实,我们在抱怨线程难用时,也要想想我们使用的方法对不对,只要遵循一些正确的使用规则,线程其实很简单。
 
后记
上面有一处代码有些奇怪: FMyClass.Free; FMyClass :=  nil ; 如果你只写 FMyClass.Free ,线程类还不会出现异常,即调用 FMyClass.SleepOneSecond 不会出错。我在主线程中试了下面的代码
MyClass := TMyClass.Create;
 MyClass.SleepOneSecond;
 MyClass.Free;
 MyClass.SleepOneSecond;
同样也不会出错,但关闭程序时就出错了,如果是这样:
MyClass := TMyClass.Create;
 MyClass.SleepOneSecond;
 MyClass.Free;
 MyThread := TMyThread.MyCreate(true);
 MyThread.OnTerminate := ThreadTerminate;
 MyThread.Resume;
 MyClass.SleepOneSecond;
马上就出错。所以这个和线程类无线,应该是 Delphi 对于堆栈空间的释放规则,我想 MyClass.Free 之后,该对象在堆栈上空间还是保留着,只是允许其他资源使用这个空间,所以接着调用下面这一句 MyClass.SleepOneSecond 就不会出错,当程序退出时可能对堆栈作一些清理导致出错。而如果 MyClass.Free 之后即创建 MyThread ,大概 MyClass 的空间已经被 MyThread 使用,所以再调用 MyClass.SleepOneSecond 就出错了。

线程杂谈2 - [原创] <script language=JavaScript> document.title="线程杂谈2 - [原创] - "+document.title </script>

作者 :linzhenqun( )
时间 :2006-2-6
-----------------------------------------------------------------------------------------------------
 
前言
上次写了一篇关于线程的文章,其中有介绍当工作线程有资源存在时,主线程如何释放工作线程的方法,后来仔细读了《 Win32 多线程程序设计》,对于线程的机制有了更进一步的理解,才知道那并非最佳的方法,这里将给出一个更好的方法,这也是我写这篇文章的最大原因。另外,对于同步机制也将作一些探讨。如果想全面了解多线程的,推荐看候捷翻译的《 Win32 多线程程序设计》。
 
干净地终止一个线程
回到上一篇的问题描述: 如果线程中有一些资源, Execute 正在使用这些资源,而主线程要释放这个线程,这个线程在释放的过程中会释放掉资源。想想会不会有问题呢,两个线程,一个在使用资源,一个在释放资源,会出现什么情况呢。 具体的情况请看“线程杂谈”那篇文章。总之是出现了违法访问,而我给出的解决方法是使用一个 FFinished 成员,最后释放的时候循环判断 FFinished ,这样可以保证 Execute 执行完毕之前,线程的成员类不会被释放。利用线程类的 Terminated 属性和 FFinished 成员 , 便可在主线程干净结束工作线程。
但这并非最好的解决方法,循环判断 FFinished 成员即如下:
While not FFinished do
 Sleep(100);
这并不能即时释放线程,最慢的情况还要等上 0.1 秒才能释放。而如果我们使用下面这种形式:
While not FFinished do
 ;
则是一种典型的 Busy loop ,主线程不停的使用 While 循环,非常浪费 CPU 的资源,这样的效率比直接等待慢一倍以上。
那么有没有一种方法,让主线程通知工作线程可以结束了,然后主线程等待工作线程结束,工作线程结束后才释放资源呢?让主线程通知工作线程结束可以用线程类的 Terminated 属性,这一点在上一篇文章已有介绍。而主线程等待 Execute 结束,事实上线程类的 Destroy 方法已经有这样的机制:
Terminate;
...
WaitFor;
其中的 WaitFor 方法一直等待直到 Execute 方法执行完毕(准确的说应该是 ThreadProc 函数执行完毕)。这些代码在一般的线程类中已经够用了,但有些线程类有自己的类成员,并会覆盖 Destroy 方法,在其中释放类成员,既然这样,我们可以把上面的代码包成一个方法,并在自己的 Destroy 中首先调用该方法。这样即可即时干净地释放线程类,比使用循环更好。
下面给出例子:
unit  Unit1;

interface

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

type
  TMyClass = class
  private
    FTest: Integer;
  public
    procedure test;
  end;

  TMyThread = class(TThread)
  private
    FMyClass: TMyClass;
  protected
     (*  等待 Execute 方法执行完毕  *)
    procedure WaitExecute;
    procedure Execute; override;
  public
    constructor Create(CreateSuspended: Boolean);
    destructor Destroy; override;
  end;

  TForm1 = class(TForm)
    Button1: TButton;
    Button2: TButton;
    procedure Button1Click(Sender: TObject);
    procedure Button2Click(Sender: TObject);
  private
     { Private declarations }
    procedure OnTerminate(Sender: TObject);
  public
     { Public declarations }
    MyThread: TMyThread;
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

{ TMyThread }

constructor  TMyThread.Create(CreateSuspended: Boolean);
begin
  inherited;
  FMyClass := TMyClass.Create;
end ;

destructor  TMyThread.Destroy;
begin
  WaitExecute;
  FMyClass.Free;
  FMyClass := nil;
  inherited;
end ;

procedure  TMyThread.Execute;
begin
  while not Terminated do
    FMyClass.test;
end ;

procedure  TMyThread.WaitExecute;
begin
  Terminate;
  WaitFor;
end ;

{ TMyClass }

procedure  TMyClass.test;
begin
   // Sleep 模拟业务操作
  Sleep( 100 );
  FTest :=  0 ;
end ;

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

procedure  TForm1.Button2Click(Sender: TObject);
begin
  MyThread.Free;
end ;

procedure  TForm1.OnTerminate(Sender: TObject);
begin
  ShowMessage( 'OK' );
end ;

end .
先点击 Button1 ,然后点击 Button2 ,线程类可以被正确地释放, 试着把 Destroy 方法中的 WaitExecute 方法注释掉再点击按钮,接着关闭程序看看,这时就出现违法访问了。
 
同步机制
当有多个线程同时读写一个资源时,使用同步机制可以保护数据的完整性,也可以防止数据被损坏。线程同步有很多种方式,但最常用的还是临界区( Critical Sections ),这里只讨论临界区。不过之前,先说明一下同步( Synchronous )和异步( Asynchronous )的概念。当程序 1 调用程序 2 时,程序 1 停止不动,直到程序 2 完成回到程序 1 来,程序 1 才继续下去,这就是同步;如果程序 1 调用程序 2 后,自己继续执行下去,那么两者之间就是异步。很明显多线程是一种异步执行方式, Windows 系统的 SendMessage PostMessage 分别是同步和异步方式。
一个 TRTLCriticalSection 类型的变量代表了一个临界区,引用《 Windows 高级编程》中的话,临界区就好像一个只能进一人的洗手间,后面的人只能排队等到进去的人出来才能进去。让我讲得详细一点,假设你有一个数据结构 A 会被线程 B C 读写,则你可以声明一个 TRTLCriticalSection 变量 CS ,接着调用 InitializeCriticalSection(CS) ,这时你拥有了一个临界区,在 B C 线程对 A 进行读写的地方,都套上如下的代码:
EnterCriticalSection(CS);
try
// A 进行读写
finally
 LeaveCriticalSection(CS);
end;
你便能保证同一时间只有一个线程对 A 进行读写。
它的过程是这样的:
线程 B C 一直在运行,某一时刻, B C 同时要对 A 进行操作,但事实上是不可能同时的,总有一个线程会快一点,假设是 B 吧, B 线程的第一句便是 EnterCriticalSection(CS) ,使得线程 B 进入了临界区,接着 B A 进行操作。而操作系统总是这样频繁的在不同线程之间切换以制造“多任务”的假象,当 B A 的操作进行到一半时,系统将执行权切换给线程 C ,我们知道 C 也要对 A 进行操作,它的第一句也是 EnterCriticalSection(CS) ,可是进这个“洗手间”时发现里面已经有“人”了, C 没办法只能一直停在 Enter 的地方等候,过很短的时间,执行权交给了线程 B B 继续对 A 进行操作,如此反复, B 终于操作完了,最后调用 LeaveCriticalSection(CS) 离开“洗手间”,这时执行权再交到 C 时, C 发现 B 已经离开临界区,它终于可以进入临界区对于 A 进行操作了,而此时别的线程要对 A 进行操作,也只能等 C 离开“洗手间”。
 
对于临界区有些地方值得注意:
1.       一个 TRTLCriticalSection 变量代表一个临界区,如果你使用两个这样的变量,则它们是毫不相干的,就像两个“洗手间”一样,对于同一个资源不能起到同步的作用。
2.       不要针对多个资源只声明一个 Critical Section ,这样使每一时刻只能对一个资源进行读写,会对程序的效率有很大的影响。为每个需要同步保护的资源声明一个临界区变量。
3.       需要对资源进行同步保护,则所有对资源操作的地方都要有 Enter Leave 函数,且要有 Try..finally 结构,保证最后能 Leave
4.       在临界区里面不要进行非常费时的操作,更重要的防止死锁的发生。
 
死锁
线程同步虽好,但也带来一些问题,最典型的就是死锁,死锁通常发生在线程之间相互等待的情况下。拿临界区来说,假设有两个临界区,两个线程分别进入一个临界区,接着各自又要进入另一个临界区,这时就会落入“你等我,我等你”的轮回。下面我制造这种场景给你给看看:
unit  Unit1;

interface

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

const
  WM_MYMESSAGE = WM_USER +  001 ;

type
  TMyThread1 = class(TThread)
  protected
    procedure Execute; override;
  end;

  TMyThread2 = class(TThread)
  protected
    procedure Execute; override;
  end;

  TForm1 = class(TForm)
    Button1: TButton;
    procedure Button1Click(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    procedure FormClose(Sender: TObject; var Action: TCloseAction);
  private
     { Private declarations }
  public
     { Public declarations }
    Thd1: TMyThread1;
    Thd2: TMyThread2;
  end;

var
  Form1: TForm1;
  CS1, CS2: TRTLCriticalSection;
implementation

{$R *.dfm}

procedure  DeadLock(var CSA, CSB: TRTLCriticalSection);
begin
  EnterCriticalSection(CSA);
   Sleep( 100 );
  EnterCriticalSection(CSB);
  try
    Sleep( 1000 );
  finally
    LeaveCriticalSection(CSA);
    LeaveCriticalSection(CSB);
  end;
end ;

{ TMyThread }

procedure  TMyThread1.Execute;
begin
  DeadLock(CS1, CS2);
end ;

{ TMyThread2 }

procedure  TMyThread2.Execute;
begin
  DeadLock(CS2, CS1);
end ;

{ TForm1 }

procedure  TForm1.Button1Click(Sender: TObject);
begin
  Thd1.Resume;
  Thd2.Resume;
end ;

procedure  TForm1.FormCreate(Sender: TObject);
begin
  InitializeCriticalSection(CS1);
  InitializeCriticalSection(CS2);
  Thd1 := TMyThread1.Create(True);
  Thd2 := TMyThread2.Create(True);
end ;

procedure  TForm1.FormClose(Sender: TObject; var Action: TCloseAction);
begin
  DeleteCriticalSection(CS1);
  DeleteCriticalSection(CS2);
  Thd1.Free;
  Thd2.Free;
end ;

end .
运行上面程序,点击按钮 1 ,你的程序没有什么异样,但关闭程序看看,程序关闭不了,因为两个线程落到相互等待的局面,而线程的 Free 又在等 Execute 结束,所以程序没有办法关闭了。
让我说说为什么会发生死锁,点击 Button1 时,两个线程同时启动, TMyThread1 执行了 DeadLock(CS1, CS2) ,而 TMyThread2 执行了 DeadLock(CS2, CS1) ,注意到两个参数倒过来了。
它的过程是这样,首先线程 1 先执行 DeadLock ,第一个进入 EnterCriticalSection(CSA) ,注意这里的 CSA CS1 ,接下来 Sleep(100) ,在线程 1 睡觉到一般时,线程 2 得到执行权也执行 DeadLock ,它也进入 EnterCriticalSection(CSA) ,这里的 CSA 却是 CS2 ,接着线程 2 Sleep(100) ,也是睡到一半,换到线程 1 ,线程 1 睡醒后往下执行,它要 Enter CSB ,对于线程 1 来说, CSB CS2 ,但 CS2 已经被线程 2 Enter 了,所以线程 1 挂起等待线程 2 离开 CS2 临界区;接着线程 2 醒来要进入 EnterCriticalSection(CSB) ,这里的 CSB CS1 ,但 CS1 给线程 1 进入了,所以线程 2 也挂起等待线程 1 离开 CS1 临界区。就这样,两个线程相互等待,没完没了。
这就是死锁发生的情况之一,当多个临界区一起工作时,死锁就有可能发生,这是要特别注意的,在死锁可能发生的地方,尽量只用一个临界区。
 
后记
关于线程的杂谈就到这里了,这两篇线程的文章更关注于其应用。
如果要学习 Delphi 中的 TThread 对于线程机制的封装,推荐给你一篇技术文章: http://www.delphibbs.com/keylife/iblog_show.asp?xid=19903 ,非常精彩,我在其中也学到很多。
至于要系统学习多线程原理的,首推《 Win32 多线程程序设计》
另外,《 Windows 高级编程》对于线程的篇章也非常好。 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值