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