多线程,主线程的概念以及在 Delphi 的新语法中的代码写法

什么是多线程

用 Delphi 写代码的初学者,因为 Delphi 的 VCL 框架(或者新的 FireMonkey 框架),导致对线程没有概念。那么,究竟线程是个什么玩意?

接本博客的上一篇文章:关于大图片加载的速度太慢导致界面卡顿的问题_pcplayer的博客-优快云博客

假设用 C 语言来写一个程序,代码大概是这样:

main(
  1. 连接数据库服务器;
  2. 发送 select 语句,获得返回的数据;
  3. 把返回的数据,用合适的界面代码,显示到界面上。
  4. 把数据库返回的数据里面的 BLOB 二进制数据变成图片;
  5. 把图片显示到界面上;
  6. while true do........ 假设用户按 esc 键退出这个 while 循环;
);

 上述程序,就是从 main 的第一行开始,一行一行地执行。假设数据库服务器离得远,网络带宽小,select 获取的数据太多,可能第二行代码的执行时间就需要20秒。那么,它就要等 20 秒后,等第二行执行完毕,才会执行第三行。这就是所谓的【程序】,也就是一行一行往下执行。

上述程序,最终有一个死循环(第六行),如果没有这个死循环,程序执行完第五行就结束了,程序直接退出。那么,第五行显示的东西,用户来不及看见,程序就没有了。为了让程序一直停留在屏幕上,最后就要搞一个死循环。当然,这个死循环要有一个退出机制,比如用户按键盘左上角的 esc 按键。

这个驱动计算机按照我们写的程序,一行一行地往下执行的东西,就叫做【线程】

为什么在 Delphi 里面,我们看到的是在一个 Form 里面点了一个按钮,我们写程序是在按钮的 OnClick 里面写,看不到这样一行一行从头到尾的执行的完整代码?其实 Delphi 的代码也是这样写的,只不过把 main() 变成了:我们在 Delphi 里面新建一个 VCL 工程,IDE 自动创建的工程代码如下:

program Project1;

uses
  Vcl.Forms,
  Unit1 in 'Unit1.pas' {Form1};

{$R *.res}

begin
  Application.Initialize;
  Application.MainFormOnTaskbar := True;
  Application.CreateForm(TForm1, Form1);
  Application.Run;
end.

这个 program Project1 begin ... end. 其实就是类似 C 代码的 mian();

上述代码,从 begin 下面第一行开始,逐行执行,一直执行到 Application.Run。如果这个 Application.Run 执行结束,就执行到 end,程序也就结束了。

Delphi 的 VCL 框架的秘密就在这个 Application.Run 里面,这里面,实际上就是一个死循环,这个死循环里面的代码,一直在处理各种消息,比如鼠标点击了界面上的某个按钮。

因此,这个驱动代码从第一行开始,逐行执行到 Application.Run,并且在 Application.Run 里面跑死循环执行代码的玩意,就是一个线程。这个线程,我们称之为【主线程】。

那么,多线程是个什么玩意?

多线程,是另外一个(或者多个)驱动一堆代码逐行往下执行的东西。是独立于【主线程】之外的东西。我们可以把线程理解为驱动轮子转动的发动机。主线程是一个发动机,多线程就是除开主线程之外的另外的发动机。这样的发动机,你可以同时有很多个。

假设你的程序要同时处理 3 件事情:

1. 显示一个界面,界面上有按钮和输入框,响应用户的输入和按钮的点击操作;

2. 要做一个巨大数字的搜索,比如你有一个文本有一百万行,你要从一百万行里面,找出有【hello world】那一行。这个搜索可能需要5分钟才能做完;

3. 你还要通过网络,从微博服务器上,抓取微博页面上的一张图片,把图片下载下来,保存到硬盘上。这个事情因为微博有热点事件,导致微博服务器响应很慢,可能需要3分钟才能处理完。

假设你把上面的3件事情,都放在一个线程,也就是主线程里面做,可能的情况是,在处理 2 的时候,因为线程要忙 5分钟,这 5 分钟里面,界面上处理用户点击的代码没法执行,因此界面冻结像死机一样;接下来处理第3件事,线程再次忙 3 分钟,界面继续冻结 3 分钟。结果就是 8 分钟里面,不管你点鼠标,还是按键盘,界面没有任何反应。要想让界面不会冻结,怎么办?

解决这个问题的办法,就是采用另外的线程,去执行第 2 和第 3 件事情的代码。这个另外的线程,就是平常我们说的 【多线程】。而那个负责处理界面和用户输入(键盘,鼠标)的线程,负责驱动处理界面的代码的线程,我们叫做【主线程】。其实,它们都是【线程】,是并行的,没有主次之分,也没有上下级关系之分。

Delphi 里面多线程代码的写法

针对上述问题,在 Delphi 里面,多线程代码该如何写?

那个主线程,Delphi 的 IDE 帮我们创建代码框架的时候,就已经创

建好了。我们按照正常的方式写我们的界面代码就好了。比如 New 一个 Form,在 Form 上拖一些输入框 TEdit,拖一些按钮 TButton 等等过来。等等。

那么,要帮我们处理上述假设的场景的 2 和 3 的代码,要让多线程去驱动,该怎么写?

传统的 Delphi 的多线程的写法:

TMyThread_1 = class(TThread)
private
  FPic: TBitmap;

  procedure ShowSearchText;
  procedure ShowPic;
public
  procedure Execute; override;
end;


procedure TMyThread_1.Execute;
begin
  2. 搜索大文本数据的代码放这里。这里执行 5 分钟才出结果也没关系。不会导致主线程停下来被卡住导致界面冻结。
  
  //5分钟以后,上述代码执行完成,就接着执行下面的代码,把搜索结果显示到界面上。
  Synchronize(ShowSearchText); //这个 ShowSearchText 函数一定要放到 Synchronize 里面。

  3. 从微薄服务器抓取图片,耗时 3 分钟;抓到后把图片写入磁盘(如果图片太大,磁盘慢,也需要一点时间); 把图片数据写入 FPic。

  Synchronize(ShowPic); //3分钟后,显示下载的图片到界面。
end;


procedure TMyThread_1.ShowSearchText;
begin
  //显示搜索结果到界面上
  Form1.Label1.Catption := '这里是我的搜索结果';
end;

procedure TMyThread_1.ShowPic;
begin
  //把从微博服务器上抓下来的图片显示到界面上。
  Form1.Image1.Bitmap.Assign(FPic);
end;

上述代码,就是在一个线程里面(Execute 方法里面的代码),逐行往下执行耗时的任务。

上述代码里面,需要注意的是:

1. 凡是【显示到界面】的操作,必须放到 Synchronize 函数里面。这是因为,凡是涉及到界面的操作,必须由主线程执行。这个 Synchronize 函数的意思是,它的括号里面的代码,转交给主线程执行,而当前这个线程本身,暂停,等待主线程执行完毕,再继续往下走。你可以理解为这个多出来的线程这个发动机,驱动一堆齿轮转动,转动到一定的角度,它暂停下来,把一堆齿轮(代码)交给主线程这个发动机去驱动旋转,等主线程旋转完成,当前这个线程继续驱动齿轮往下转动。这一点非常重要。如果不将对界面操作的代码放入这个 Synchronize 函数,可能程序仍然会执行并且没有什么错误,但是,有潜在出现错误的风险。

2. 上述线程代码,执行的就是 Execute 里面的代码,从 begin 开始到 end 结束。执行到 end 线程也就结束了,线程这个发动机就熄火了,停止运转。

3. 上述代码,是定义了一个线程类,同时定义了这个线程类的 Exectue 方法里面都要执行什么代码。但要让这个线程转动起来,需要创建它的实例。我们可以在主线程里面创建它的实力。

procedure TForm1.Button1Click(Sender: TObject);
var
  AThread: TMyThread_1;
begin
  AThread := TMyThread_1.Create;
  AThread.Execute; //让线程跑起来。
end;

 上述代码在主线程的按钮点击的时候被执行,一旦执行 AThread.Execute 则线程里面的代码开始跑,它跑 8 分钟也没关系,不会堵塞主线程。这个时候主线程没活干,一直在跑它自己的死循环,处理消息(时不时看看有没有用户点击按钮或者输入文字等等),这样界面就不会被冻结,程序不会看起来像是死机一样。

Delphi 新版的新语法的写法

上述代码,要先自己定义一个线程类,把代码写在类里面,然后创建类的对象实例。代码挺多,打字很累。Delphi 的新版本,不记得从哪个版本开始(肯定是 Delphi XE 以后,Delphi 7 是没有的),有了匿名函数和匿名线程,以及新的 TTask 类。

上述代码就可以写成更简单的模样:

TTask.Run(
  procedure
  var
    FPic: TBitmap;
  begin
     //这里开始的代码,是被主线程之外的另外一个线程在执行。逐行往下执行。

     DoSomeThings;//非常耗时的操作放在这里,比如要花 8 分钟的事情;
     
     //以下代码是同步到主线程去做,显示到界面的 Label1 上面。
     TThread.Synchronize(nil,
       procedure
       begin
         Form1.Label1.Caption := '我的搜索结果'; //操作界面控件带来界面变化的代码,放进主线程。
       end
     );

    DoGetPictureFrom_WeiBo(FPic); //耗时 3 分钟的操作,从微博下载图片;
    
    //操作界面控件带来界面变化的代码,放进主线程。
    TThread.Synchronize(nil,
       procedure
       begin
         Form1.Image1.Bitmap.Assign(FPic); //显示下载的图片。
       end
     );

  end

)

上述代码比传统的写法,简单一些。读起来也更容易一些。

上述代码,从头到尾执行完毕可能需要 5 分钟 + 3分钟。但这个过程中,不会阻塞界面的操作,界面不会死机冻结,当然,你点了按钮后它开始执行,你可能要等几分钟才能在界面上看到结果,但这个等待过程中,你的主线程的程序可以继续跑,比如你可以在界面上,放一个进度条让它来回跑。同时,在那个多线程的程序没跑完之前,用户也可以在界面上做其它操作。那个多线程的程序跑完一个阶段,到了调用显示的代码,界面上会立刻出来执行结果。

内联函数(Inlining) D7中的inline关键字作为保留字并不会对编译器产生实际作用,在2009中此关键字起到内嵌到代码中起到实际作用。语法如下: function foo: Integer; inline; 内部函数/过程也可以使用,但在D2009测试版中,类方的内部函数使用inline后不认Self指针;类的子过程/子函数,也可以使用inline关键字,但没有实际效果,且虚方/继承方(virtual/override)不能使用。 重载运算符(Operator Overloading) 可以重载部分运算符,如+、-、类型转换等,在D2006只支持到record,但从2007开始支持到Class,以下示例修改自官网: TMyClass = class // Addition of two operands of type TMyClass class operator Add(a, b: TMyClass): TMyClass; // Subtraction of type TMyClass class operator Subtract(a, b: TMyClass): TMyclass; // Implicit conversion of an Integer to type TMyClass class operator Implicit(a: Integer): TMyClass; // Implicit conversion of TMyClass to Integer class operator Implicit(a: TMyClass): Integer; // Explicit conversion of a Double to TMyClass class operator Explicit(a: Double): TMyClass; end; class operator TMyClass.Add(a, b: TMyClass): TMyClass; begin //... end; var x, y: TMyClass begin x := 12; // Implicit conversion from an Integer y := x + x; // Calls TMyClass.Add(a, b: TMyClass): TMyClass end; 类助手(Class Helpers) Helper是对原Class的扩展,是我们在不修改原类的基础上增加类方,并加入原类的空间中。在Delphi中,对对象的调用实际上采用了两个步骤,首先是把对象地址放入eax寄存器中,然后call类方,所以如果不使用继承类增加数据的话,用父类调用继承类的方是没问题的,所以其实这样的方在D7中也可以使用,但却很麻烦。所以Class Helper起到的就是这个作用,在Class Helper中可以增加的就是与实例无关的内容,所以任何需要增加实例Size的活VMT的功能不能声明,例如变量、虚方等,但只占用类空间的没关系,如class var。在应用上我们可以通过这种方方便的给VCL一类控件加上某个属性。 TFoo = class helper for TControl private function GetA: Integer; public class var X: Integer; procedure MSG(var Message: TMessage); message WM_MYMESSAGE; procedure ProcFoo; property A: Integer read GetA; end; // ... procedure TForm1.Foofoo; begin ProcFoo; // TControl -> TWinControl -> TScrollingWinControl-> TCustomForm -> TForm -> TFrom1: Call TFoo.ProcFoo end; strict关键字(Keyword “strict”) 众所周知,在Delphi中,类的private和protected域中的变量可以被同一单元中可以自由的被访问(Delphi的类没有“友元”的概念,但同一个unit中可以说自动友元化了),而并非是真正的私有或只能被继承类访问。而strict关键字的作用就是使该内容变成严格OO意义上的private/protected作用域,这点没有什么多说的。语法: strict private // Blah... strict protected // Blah... 结构方(Records with Methods) 也没什么特别的,就是和class差不多,就一个不用创建和销毁、不能继承、没有作用域之类的类,很容易掌握,所以这里就不多介绍了。但是很有意思的是带参数的constructor可以通过编译,可能是为了初始化的方便吧。 抽象类和固实类(Abstract and Sealed Classes) 这两个概念在OO中也并不陌生,抽象类是不应该创建实例的(但D2006起的编译器就不给检查,连个Warning都没有,这还有啥用啊 -.- ),而固实类是不能被继承的。语法: TAnAbstractClass = class abstract // or (TParentClass) // Blah... end; TASealedClass = class sealed(TAnAbstractClass) // or empty // Blah... end; 类常量、类变量、类属性与静态类方(Class const/var/property and Static Class Methods) 老的Delphi中只提供了类方,而没有提供类变量、类常量和类属性,这真的是很不方便。这里先区分一下我所使用的类(Class)和对象(Object)即类的实例(Instance of Class)。当在Delphi中声明一个类的时候,这个类是有实际地址的,该地址记录了许多类的相关信息,比如实例的Size啊、虚方信息啊一堆东西,而创建一个对象的时候则把类实例化,在堆(Heap)中分配一块地址,包括内部数据和VMT之类的东西。在调用实例的时候,首先要知道对象地址,然后才能访问内部变量和调用方时使用Self指针即实例地址;而在调用类方的时候,eax中的并不是实例的地址而是类的地址,然后再call方,这时的Self指针并非实例地址而是类地址。所以对于每一个类和继承类来说,包括它和它的继承类的所有实例,类变量、常量都是同一个,这样就存在了一个唯一的可供使用的变量或常量,方便同步并且不需要使用较多的内存(可以参考C#中的类,不过C#中不允许从实例直接访问类变量、常量、方)。而静态类方则是在使用这个类方的时候不传入class地址,也就是说没有Self指针,这样的类方的访问开销要小于普通的类方;这自然也就意味着,该类方不能被继承(不能virtual/override)。另外,类属性的get/set方必须使用静态类方。 TFooClass = class private class procedure SetFoo(const Value: Integer); static; // A Static Class Method protected class var FX : Integer; // class var public const FC: Integer = 10; // class const class procedure VirtualProc; virtual; class property X: Integer read FX write FX; // class property class property Foo: Integer read FC write SetFoo; end; 类内部类型与嵌套类(Class Types and Nested Classes) 可以说现在的Class的域几乎相当于原来的整个unit,以前不能放里面的元素现在都可以放里面了,这个也没什么好多说的,试验一下就很容易明白了。 终方(Final Methods) 这个也是建立在虚方的基础上的,在override后使用final关键字,则表示该虚方不能再被子类继承下去了。 TAClass = class public procedure Foo; virtual; end; TFinalMethodClass = class(TAClass) public procedure Test; override; final; // A Final Method end; For-in循环(For-in Loop) 这个应该是受.Net影响吧,支持遍历一个数组或提供了GetEnumerator函数的类。GetEnumerator要求返回一个类的实例,该类包含有Current属性和MoveNext方。 procedure Foo(List: TStrings); i : Integer; lst : array[0..100]of Integer; s : string; begin for i in lst do ; for s in List do ; // Support of TStrings.GetEnumerator end;
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值