1. 概述

对于开发人员来说,多线程是必备的知识,但相对来说,也是比较难的知识点。Delphi是一门古老而优秀的编程语言,它对多线程的处理有一些特殊的地方,本文尝试做一些简单的讲解,可以当作Delphi的多线程基础入门知识来阅读。如无特殊说明,所有例子都在XP操作系统中和Delphi7中调试通过。

2. 一个简单的例子

在这一节中,我们将建立一个极为简单的例子,阐述Delphi中多线程的用法。

2.1 实现步骤

第一步:在Delphi7 IDE中新建一个Application,如下图所示。

图1

第二步:打开工程文件,输入{$APPTYPE CONSOLE},以便打开控制台,新建的线程在控制台输出一些文本。操作过程如下所示。

图2

图3

第三步:新建一个单元文件Unit2,如下图所示。

图4

图5

第四步:在新建的Unit2单元中输入如下代码:

图6

在Unit1单元中输入如下代码:

图7

第五步:点击“Save All”,保存相关文件,如下图所示:

图8

图9

第六步:按F9运行程序,如下图所示:

图10

在上图中,可以见到新的线程在运行了,输出了“I am a new thread”。

2.2 线程基础知识

2.2.1 新的线程与主线程的关系

主线程也叫界面线程,就是窗体应用程序启动时,对应进程创建的第一条线程,该线程负责:
          1. 创建窗体、创建窗体上的控件。
          2. 响应键盘消息、鼠标消息等Windows消息。
          3. 负责创建新线程和其他事情。

主线程以外的新线程也叫作工作线程,负责处理具体的事务。主线程创建新线程后,可以让新线程立即运行,也可以让它稍后运行。
          在下面的代码中:

 
  1. myThread := TMyThread.Create(False);

参数False表示,新线程myThread在创建后,将立即运行。如果想不立即运行,而是在另一个适当的时刻运行,则可以使用如下代码:

 
  1. myThread := TMyThread.Create(True);

  2. ……

  3. myThread.Resume; //适当的时刻启动该线程

2.2.2 新的线程在哪里工作

在Delphi中,创建新线程时,通常的做法是从系统线程类TThread进行继承,该类有一个虚方法:

 
  1. procedure Execute; virtual;

在上文的TMyThread类中,重写了这个方法,如下所示:

 
  1. TMyThread = class(TThread)

  2. protected

  3. procedure Execute; override;

  4. public

  5. end;

安排新线程做的工作任务,应该在这个方法中完成。

2.2.3 新的线程何时退出

当方法Execute退出时,新线程就结束了。在下面的Execute方法中,它只是向屏幕打印了一个语句“I am a new thread”,新线程就结束了。

 
  1. procedure TMyThread.Execute;

  2. begin

  3. Writeln('I am a new thread');

  4. end;

这是新线程的正常退出方法。除此之外,Delphi没有提供立即终止新线程的方法,如Kill、Abort等其他语言提供的方法。若要立即杀死线程,则需要调用Win32 API方法TerminateThread(myThread.Handle, 0)。这是一种暴力退出方法,可能导致系统不稳定,因此严重不推荐。
          在TThread中,与线程暂停、退出有关的方法和属性有:

  • property Terminated: Boolean;
            Delphi解释:The thread's Execute method and any methods that Execute calls should check Terminated periodically and exit when it's true. The Terminate method sets the Terminated property to true。
            中文翻译:Execute方法以及它调用的任何方法都应该周期性地查看Terminated的值,一旦发现Terminated为True,就应该退出。Terminate方法将属性Terminated设为True。
            上述说法表明,将Terminated设为True,并不能退出线程,它只是告诉Execute方法,“有人让你们尽快完事,你们快点干”,但Execute可以不理会Terminated的值,仍然自顾自地运行。

  • property Suspended: Boolean;
            Delphi解释:Set Suspended to true to suspend a thread; set it to false to resume it. Suspended threads do not continue execution until they are resumed.
            中文翻译:设置Suspended为True以挂起(暂停)一个线程;设置它为False以唤醒(继续)一个线程。挂起的线程不会继续执行,直到它被唤醒为止。

  • procedure Terminate;
            Delphi解释:Terminate sets the thread’s Terminated property to true, signaling that the thread should be terminated as soon as possible.
            中文翻译:Terminate方法设置线程的Terminated属性为True,该信号表明线程应该尽快结束。
            上述文字表明,Terminate方法只是想Execute方法喊话,“喂,哥们,快点啊,时间不多了,快点干完”,Execute如果懂礼貌的话,它会时不时地注意是否有人让它停止干活,一旦收到停工的消息,它就会尽快收拾停当,如果它不懂礼貌的话,则会把停工的消息当作耳边风。

  • procedure Resume;
            Delphi解释:Call Resume to cause a suspended thread to start running again. Calls to Suspend can be nested; Resume must be called the same number of times Suspend was called before the thread will resume execution.
            中文翻译:调用Resume方法以让一个挂起(暂停)的线程重新开始运行。对Suspend方法的调用可以嵌套;在线程继续运行之前,调用Resume方法的次数必须与调用Suspend方法的次数相同。

  • procedure Suspend;
            Delphi解释:Call Suspend to temporarily halt execution of the thread. To resume execution after a call to Suspend, call Resume. Calls to Suspend can be nested; Resume must be called the same number of times Suspend was called before the thread will resume execution.
            中文翻译:调用Suspend方法临时中止线程的执行。若要在调用Suspend方法后继续执行,请调用Resume方法。对Suspend方法的调用可以嵌套;在线程继续运行之前,调用Resume方法的次数必须与调用Suspend方法的次数相同。

2.2.4 新线程如何销毁

有两种方法:
          1. 在创建线程时,设置FreeOnTerminate为True,那么当Execute执行完毕时,系统自动销毁新线程。有如下几个地方,设置FreeOnTerminate为True。

  • 第一个地方——Create方法内部

     
    1. //为TMyThread提供Create方法,在Create方法中设置

    2. constructor TMyThread.Create;

    3. begin

    4. FreeOnTerminate := True; //线程工作完毕后要自行销毁

    5. inherited Create(False); //线程创建后立即启动

    6. end;

    7. //调用TMyThread.Create的地方,需要修改为

    8. procedure TForm1.FormCreate(Sender: TObject);

    9. begin

    10. myThread := TMyThread.Create;

    11. end;

  • 第二个方法——在Execute方法内部,执行工作任务之前

     
    1. procedure TMyThread.Execute;

    2. begin

    3. FreeOnTerminate := True;

    4. Writeln('I am a new thread');

    5. end;

  • 第三个方法——在类TMyThread的外部

     
    1. constructor TMyThread.Create;

    2. Begin

    3. inherited Create(True); //线程创建后不立即启动

    4. end;

    5. //在TForm1内部,创建TMyThread线程后,设置FreeOnTerminate 为True

    6. procedure TForm1.FormCreate(Sender: TObject);

    7. begin

    8. myThread := TMyThread.Create;

    9. myThread.FreeOnTerminate := True;

    10. end;

    11. //在合适的地方,执行如下语句

    12. myThread.Resume;

由此可见,只需要在Execute退出前,设置FreeOnTerminate为True即可。

2. 如果没有设置FreeOnTerminate为True,则需要在Execute执行完毕后,开发人员人为地销毁它,代码如下:

 
  1. //主线程在适当的地方执行下述语句

  2. FreeAndNil(myThread);

3. 可持续工作的线程

3.1 使用循环实现持续工作

上文创建的线程,只做了一件事情(即向控制台输出一行文本),就退出了。显然,这种做法没有挖掘线程的价值。为了改变这种现象,需要在Execute方法写入一个循环。该循环通常是一个永真循环,即死循环;当某些条件为真(比如Terminated为True)时,退出死循环。循环的形式可以是while、for、repeat-until语句,退出循环的形式可以是break或Exit。这里以while和Exit为例,说明用法。
        第一步:在上文的项目中,将unitMyThread单元的代码改为:

 
  1. unit unitMyThread;

  2. interface

  3. uses Windows, Classes, SysUtils;

  4. type

  5. TMyThread = class(TThread)

  6. protected

  7. procedure Execute; override;

  8. procedure DoMyWorkByWhile;

  9. public

  10. constructor Create;

  11. end;

  12. implementation

  13. { TMyThread }

  14. constructor TMyThread.Create;

  15. begin

  16. inherited Create(True);

  17. end;

  18. procedure TMyThread.DoMyWorkByWhile;

  19. var totalCount: Integer;

  20. begin

  21. totalCount := 0;

  22. while(True) do

  23. begin

  24. Inc(totalCount);

  25. Writeln('第' + IntToStr(totalCount) + '次循环 @' + FormatDateTime('yyyy-MM-dd HH:mm:ss', Now));

  26. Sleep(500);

  27. if(Terminated) then

  28. begin

  29. Exit;

  30. end;

  31. end;

  32. //上述循环也可以改为

  33. while(not Terminated) do

  34. begin

  35. Inc(totalCount);

  36. Writeln('第' + IntToStr(totalCount) + '次循环 @' + FormatDateTime('yyyy-MM-dd HH:mm:ss', Now));

  37. Sleep(500);

  38. end;

  39. end;

  40. procedure TMyThread.Execute;

  41. begin

  42. FreeOnTerminate := True;

  43. DoMyWorkByWhile;

  44. end;

  45. end.

第二步:在TForm1主窗体上添加两个按钮,分别是btnStart(启动)和btnExit(退出),主窗体单元代码为:

 
  1. unit unitMainForm;

  2. interface

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

  4. type

  5. TForm1 = class(TForm)

  6. btnStart: TButton;

  7. btnExit: TButton;

  8. procedure btnStartClick(Sender: TObject);

  9. procedure btnExitClick(Sender: TObject);

  10. private

  11. { Private declarations }

  12. myThread: TMyThread;

  13. public

  14. { Public declarations }

  15. end;

  16. var

  17. Form1: TForm1;

  18. implementation

  19. {$R *.dfm}

  20. procedure TForm1.btnStartClick(Sender: TObject);

  21. begin

  22. myThread := TMyThread.Create;

  23. Writeln('亲亲的主人,谢谢您给了我以生命 @' + FormatDateTime('yyyy-MM-dd HH:mm:ss', Now));

  24. //这里可以做很多其他工作

  25. myThread.Resume;

  26. end;

  27. procedure TForm1.btnExitClick(Sender: TObject);

  28. begin

  29. if Assigned(myThread) then

  30. begin

  31. myThread.Terminate; //不可以写作myThread.Terminated := True,因为Terminated不是public的

  32. Writeln('');

  33. Writeln('亲,永别了,来生再会 @' + FormatDateTime('yyyy-MM-dd HH:mm:ss', Now));

  34. end;

  35. end;

  36. end.

第三步:点击启动,一会儿后,点击退出,运行结果如下:

图11

3.2 可暂停的线程

第一步:在TForm1主窗体上添加一个按钮btnPause(暂停)和btnResume(继续),双击它增加一个事件处理方法,如下所示:

 
  1. procedure TForm1.btnPauseClick(Sender: TObject);

  2. begin

  3. if Assigned(myThread) then

  4. begin

  5. myThread.Suspend;

  6. Writeln('亲,你把我暂停了 @' + FormatDateTime('yyyy-MM-dd HH:mm:ss', Now));

  7. end;

  8. end;

  9. procedure TForm1.btnResumeClick(Sender: TObject);

  10. begin

  11. if Assigned(myThread) then

  12. begin

  13. myThread.Resume;

  14. Writeln('');

  15. Writeln('亲,你把我唤醒了,我还没睡够呢 @' + FormatDateTime('yyyy-MM-dd HH:mm:ss', Now));

  16. end;

  17. end;

第二步:点击启动,一会儿,点击暂停,一会儿,点击继续,一会儿,点击退出,运行结果如下:

图12

3.3 可空转/继续工作的线程

在3.2节中,我们让线程实现了挂起(暂停)和唤醒(继续)两个操作,对某些场合来说很有用,但仍然无法某些场景的需求,举例如下:
        令狐冲因为跟魔教有勾结,被师傅岳不群罚去洗碗,包一日三餐,早上5:00开始上班,晚上11:00休息,中间不允许休息。令狐冲起床后,就要站在洗碗流水线上,他要做3件事情:1)观察是不是有碗筷要洗;2)如果有碗筷要洗,则刷洗碗筷;3)观察是不是洗完了。
        上述场景,用Suspend和Resume是解决不了的。为什么这么说呢?表面上,正在洗碗的动作,表示的是Resumed状态,这没错,而没有洗碗动作的时候,貌似是Suspended状态,这就错了,其实这也是Resumed状态。原因在于,若一个线程处于Suspended状态,就相当于令狐冲在睡觉,在睡眠中他是无法观察任何外界现象的。因此,实际上,上述三件事情,线程都必须在Resumed状态。
        第一步:在unitMyThread单元的类TMyThread中增加一个public字段:

 
  1. Working: Boolean;

第二步:在TMyThread.Create中初始化:

 
  1. Working := True;

第三步:增加洗碗方法TMyThread.WashDishes,如下所示:

 
  1. procedure TMyThread.WashDishes;

  2. var totalCount: Integer;

  3. begin

  4. totalCount := 0;

  5. while(not Terminated) do

  6. begin

  7. while(Working) do //观察是否洗完了,Working为True说明还没洗完

  8. begin

  9. Inc(totalCount);

  10. Writeln('老板,我洗了第' + IntToStr(totalCount) + '个碗 @' + FormatDateTime('yyyy-MM-dd HH:mm:ss', Now));

  11. Sleep(500); //洗一个碗,歇500毫秒

  12. end;

  13. //洗完了

  14. Sleep(2000); //每隔2秒钟,观察是否有新的一波碗要洗

  15. end;

  16. end;

第四步:修改TMyThread.Execute方法,如下所示:

 
  1. procedure TMyThread.Execute;

  2. begin

  3. FreeOnTerminate := True;

  4. //DoMyWorkByWhile;

  5. WashDishes;

  6. end;

第五步:在TForm1主窗体上增加按钮btnStartWash(洗碗)和btnSleepAwhile(歇会儿),增加两个事件方法,如下所示:

 
  1. procedure TForm1.btnStartWashClick(Sender: TObject);

  2. begin

  3. if Assigned(myThread) then

  4. begin

  5. Writeln('老板,从哪儿弄来这么多碗,生意不错啊,我要开洗了 @' + FormatDateTime('yyyy-MM-dd HH:mm:ss', Now));

  6. myThread.Working := True;

  7. end;

  8. end;

  9. procedure TForm1.btnSleepAwhileClick(Sender: TObject);

  10. begin

  11. if Assigned(myThread) then

  12. begin

  13. myThread.Working := False;

  14. Writeln('老板,我洗完了,歇会儿啊 @' + FormatDateTime('yyyy-MM-dd HH:mm:ss', Now));

  15. end;

  16. end;

  17.         第六步:点击启动→洗碗→歇会儿,结果如下:

图13

4 与主线程通信

在上文中,工作线程只是自顾自地干活(即向控制台输出文本),它并没有将工作进度实时报告给主线程。
线程通信有很多实现方式,有些复杂,有些简单。这里主要提供两种思路和实现。

4.1 主线程循环查询

在主线程中增加一个时钟Timer1和OnTimer事件的处理方法Timer1Timer(Sender: TObject),在这个方法中,主线程循环查询工作线程的内部状态。
        第一步:在TMyThread中增加一个public字段:

 
  1. DishNumber: Integer; //表示总共洗了多少个碗

第二步:在TMyThread.Create方法中增加一个语句:

DishNumber := 0;

第三步:在TMyThread.WashDishes方法中的Writeln上方增加一条语句:

DishNumber := totalCount;

第四步:在TForm1主窗体上增加一个标签lblDishNumber(洗碗个数)和文本框edtDishNumber。
        第五步:在Timer1Timer方法中增加语句:

edtDishNumber.Text := IntToStr(myThread.DishNumber);

第六步:运行程序,点击启动→洗碗→歇会儿,结果如下:

图14

4.2 工作线程回调

第一步:在TMyThread中增加一个public字段:

Callback: TNotifyEvent;

第二步:在TMyThread.WashDishes中的Writeln上方增加一条语句:

 
  1. Callback(Self);

第三步:在TForm1中增加一个方法TForm1.callbackByMyThread(Sender: TObject),如下所示:

 
  1. procedure TForm1.callbackByMyThread(Sender: TObject);

  2. begin

  3. if Assigned(myThread) then

  4. begin

  5. edtDishNumber.Text := IntToStr(myThread.DishNumber);

  6. end;

  7. end;

第四步:在TForm1.btnStartClick(Sender: TObject)中的Writeln上方增加一个语句:

 
  1. myThread.Callback := callbackByMyThread; //设置回调方法

第五步:在TForm1.btnStartClick(Sender: TObject)中的Writeln上方增加一个语句:

 
  1. Timer1.Enabled := False; //禁止时钟

第六步:运行程序,点击启动→洗碗→歇会儿,结果如下:

图15

4.3 两种通信方法的缺点

如下所示:

优缺点 主线程循环查询 工作线程回调
优点 可以方便访问窗体控件,无需线程切换 实时性有保证
缺点 实时性难以保证 需要切换线程,在访问窗体控件时需要线程同步

在实时性方面,举一个比喻来说明问题。

岳不群派遣令狐冲带小师妹岳灵珊攻占黑木崖,但岳不群又不放心,为保险起见,他决定采取打电话循环查询的方式,他这么做:

 
  1. 第一个夜晚:岳不群打电话:“冲儿,攻占黑木崖了吗?”

  2. 令狐冲回道:“师傅,没呢,还在路上。”

  3. 第二个夜晚:岳不群打电话:“冲儿,攻占黑木崖了吗?”

  4. 令狐冲回道:“师傅,没呢,还在路上。”

  5. ……

  6. 第1000个夜晚:岳不群打电话:“冲儿,攻占黑木崖了吗?”

  7. 令狐冲回道:“师傅,没呢,还在路上。”

  8. 第1000 + 1个夜晚:岳不群打电话:“冲儿,攻占黑木崖了吗?”

  9. 令狐冲回道:“师傅,攻占了,但是师妹被东方不败抢走了。”

  10. 岳不群:“完了,完了,给我立即追击,要是找不到姗儿,你就别回来了。”

在这1001个夜晚,岳不群茶饭不思、心神不灵,干啥啥不成,睡啥啥不香,还不如亲自带着自己的小师妹宁中则攻占黑木崖呢。令狐冲也过得不爽,每天晚上都要接一次电话,简直没法跟小师妹说悄悄话了和做其他事情了。特别是,要是早上9:00攻占了黑木崖,还不能立即告诉师傅好消息,只能等到晚上师傅打电话过来。

由于师妹被东方不败抢走了,据说带到扶桑岛去了,令狐冲决定趁胜追击,但是路途遥远,路上信号又不好,关键是手机用了将近三年,电池不行啊,撑不住每天一个电话,于是,第1002个夜晚,令狐冲突发奇想,给师傅打了个电话,说道:“师傅,东方不败虏着师妹逃到扶桑去了,我要去追她,但手机电池不行了,路上充电不方便,所以,以后您就别打电话问我了,一旦有情况,我给您打电话。”
        岳不群:“好的,冲儿,如此甚好,能省不少电话费。”

一路上,令狐冲为了省电,将手机关机。岳不群不用打电话问令狐冲了,每天带着宁中则用心训练其他徒弟,以便随时增援令狐冲。

 
  1. 第1100个夜晚,岳不群收到令狐冲来电:“师傅,我到了山东半岛入海口,已经雇佣好了民船。”

  2. 岳不群:“冲儿,别坐民船了,到青岛机场坐飞机过去。”

  3. 令狐冲:“师傅,我没钱了。”

  4. 岳不群:“叫你师娘给你微信转账10万块。”

第1101个夜晚,令狐冲在扶桑下了飞机,恰巧看到东方不败也带着小师妹下了飞机,令狐冲赶紧给师傅打电话:“师傅,我看到东方不败和小师妹了,我立即去收拾他。”
        岳不群:“好,小心点,一定要把姗儿救回来。”

从两个例子看出,回调的方法,其实时性要好于循环查询。

但是,回调方法有一个缺点,就是不能直接在回调方法中访问窗体控件,其原因为:窗体控件都是由主线程创建的(其他线程也可以创建窗体,但这里假设只有主窗体创建了窗体),Delphi运行时库为了安全性,不允许主线程以外的线程访问和修改窗体控件,否则会引发一些隐患,很难跟踪调试。
        不过,由于Delphi7的运行时库并不是很严谨,因此有时候工作线程也能修改窗体控件,Delphi7不报错,例如上述TForm1.callbackByMyThread方法就在线程myThread中允许,它修改了edtDishNumber.Text。尽管如此,但Delphi7对这种操作的可靠性、稳定性不予以任何保证。
        那怎么办呢?采用线程同步的方法。线程同步有很多方式,这里采用较为简单的一种,即线程切换。具体做法是:
        第一步:在TForm1中增加一个无参数方法,如下所示:

 
  1. procedure TForm1.dealAfterCallbackByMyThread;

  2. begin

  3. edtDishNumber.Text := IntToStr(myThread.DishNumber);

  4. end;

第二步:将TForm1.callbackByMyThread修改为:

 
  1. procedure TForm1.callbackByMyThread(Sender: TObject);

  2. begin

  3. if Assigned(myThread) then

  4. begin

  5. TThread.Synchronize(nil, dealAfterCallbackByMyThread);

  6. end;

  7. end;

在上述代码中,调用类TThread的静态方法,就可以从线程myThread切换到主线程。具体地说,方法TForm1.callbackByMyThread运行在线程myThread上,线程myThread通过调用方法TThread.Synchronize,将方法放到主线程上,并立即切换到主线程,主线程且立即执行dealAfterCallbackByMyThread。

第三步:运行程序,点击启动→洗碗→歇会儿,结果如下:

图16

4.4 回调方法可能存在的固有缺陷

细心的童鞋们,肯定发现了一个问题,那就是上图中的时间有问题,不再是按照500毫秒的间隔更新,而是隔了好几秒,实时性更差了。这是为什么呢?
        这个现象也让我百思不得其解,我用了以下方法去寻找原因:
        1)死锁:找了半天,也没有找到死锁的地方。
        2)Synchronize用法不对:找了很多地方,发现大家都是这么用的。
        3)到国外网站查看:没有搜索到相同问题的描述
        4)将程序拷贝到Win10,运行良好,如下所示:

图17

我冥思苦想,手脚晃荡,无意中,我发现了一个问题,那就是在XP系统中,当我鼠标在TForm1窗体上晃动时,程序马上有输出。
        这是什么原因呢?我分析如下,不知对错,请大家指正:
        1)方法TForm1.callbackByMyThread运行在线程myThread上,在该方法内部,线程myThread通过调用方法TThread.Synchronize,先是给主线程发一个空的PostMessage消息,激活主线程。
        2)myThread然后给主线程发送一个SendMessage,消息告诉主线程调用dealAfterCallbackByMyThread。
        3)然后myThread挂起线程myThread,等待主线程回复。
        4)主线程在适当的时候执行dealAfterCallbackByMyThread。
        5)主线程执行dealAfterCallbackByMyThread完毕后,告诉线程myThread,线程myThread继续运行。

上述过程的一个关键地方就是“主线程在适当的时候”,什么时候才算是适当的呢?互联网上很多文章说,主线程在空闲的时候会执行dealAfterCallbackByMyThread。这就麻烦了,主线程有时候很忙的,日理万机,不知道什么时候能闲下来,没空理会myThread,这就会导致myThread发生阻塞,从而没法好好干活。

经过我的研究发现,“主线程在空闲的时候”是不准确的,至少在XP系统中是这样。上面我提到过,在XP系统中,程序实时输出内容到控制台,但只要鼠标在TForm1窗体上晃动,程序就会马上更新输出,因此,这说明恰恰是“主线程在忙的时候(处理鼠标晃动消息)”才会理会线程myThread的消息。照着这个思路,我再说一下上述过程:

1)方法TForm1.callbackByMyThread运行在线程myThread上,在该方法内部,线程myThread通过调用方法TThread.Synchronize,先是给主线程发一个空的PostMessage消息(WM_NULL),试图激活主线程。但是,由于PostMessage发的是空消息,所以主线程没有处理,即使此时主线程很空闲,它也懒得处理空消息。也可以简单理解为,主线程实际上没有激活。
        2)myThread然后给主线程发送一个SendMessage,消息告诉主线程调用dealAfterCallbackByMyThread。此时,主线程收到了SendMessage发来的消息,但是因为主线程没有理会myThread通过PostMessage发送的消息,自然也就不会理会SendMessage的消息。凡事总有一个先来后到,前面的还没处理呢,后面的咋处理呢?
        3)然后myThread挂起线程myThread,等待主线程回复。主线程这个时候正闲着呢,才懒得回复呢。
        4)上帝移动了一下鼠标,主线程收到了上帝的命令,立即开始干活,先是处理上帝的命令(注意,Windows消息有优先级别,来自用户也就是上帝的消息,通常有较高的优先级),完毕后,发现角落里还藏着myThread发来的消息,于是顺便处理,马上执行dealAfterCallbackByMyThread。
        5)主线程执行dealAfterCallbackByMyThread完毕后,告诉线程myThread,事情处理完了,线程myThread继续运行。

上述说辞似乎解决了问题,用的是时钟方法。但是,上帝总不能时时刻刻去移动鼠标啊,那还不如让岳不群每天打一个电话呢。
        上帝总是万能的,于是上帝在TForm1.Create方法中,又启动了时钟,且为了及时处理myThread的消息,时钟的间隔得比myThread.Execute中的休息间隔500毫秒还短。为了保证较好的实时性,将时钟间隔设为100毫秒。于是,每隔100毫秒,主线程收到了一条必须马上处理的时钟消息,处理完后往角落里看两眼,若是有myThread的消息,就马上处理,若是没有,则继续睡100毫秒小憩。
        更改代码后,在XP中重新运行,结果如下:

图18

可见,上述结果是正常的。
        对于这个问题,我不知道这算不算Delphi7的缺陷或是XP系统的缺陷,也不知道我上面的解释是否正确,请大家指正。

4.5 回调方法就没有用了吗?

在4.4节中,我们在使用回调方法进行线程通信时,出现了问题,为了解决问题,又使用了基于时钟的查询方法。这是否说明回调方法不但毫无用处呢,还更费事呢?
        也不能这么说,我举一个例子,说明一下。还是以岳不群、令狐冲为例。

令狐冲带着小师妹去攻打黑木崖,临行前,岳不群嘱咐道:“冲儿,你独孤九剑已经达到炉火纯青的地步,远超为师了,你就放心地去收拾东方不败吧,有啥事你打个电话,没啥事我就跟你师娘唱唱歌、吟吟诗。”令狐冲答应照办。
        (也就是说,两人商量以回调的方式来通信。)

走到半路上,令狐冲和小师妹突然遇到东方不败派来的四大护法,四大护法练就了一个天罡北斗阵,令狐冲苦战得脱,幸好小师妹毫发无损。
        (也就是说,工作线程完成了一个任务。)

作战毕,令狐冲给岳不群打了个电话,但岳不群忙着呢,没接着,于是令狐冲只好发了微信,然后就抱着小师妹在路边歇着,等着师傅下一个指示。
        (也就是是,工作线程给主线程发了一个消息,然后把自己挂起来,等待主线程的回复。)

但是,岳不群的微信好友太多,消息太多,没有把令狐冲的消息当回事,也就没有处理令狐冲的消息。
        (也就是说,主线程没有理会工作线程发来的空消息和其他消息。)

幸好,岳不群有一个抽烟的习惯,他一般是10分钟抽一根,一根烟抽10分钟,然后干别的事情。在抽烟的过程中,岳不群啥事不干,就是把所有的未读微信消息看个遍,很快就看到了令狐冲的消息,于是顺便回复了一句:“冲儿,干得漂亮,天罡北斗阵还是蛮霸道的,上次差点让为师和师娘回不来了。”
        (抽烟的事件相当于时钟消息。)

在上述岳不群和令狐冲的例子中,如果只有基于抽烟的循环查询,那么岳不群每抽一根烟,都要打电话问一下令狐冲,“冲儿,现在走到哪里了?情况如何?”可以想象,岳不群这个烟抽得肯定不爽。
        但是,若在抽烟的同时,看看有没有令狐冲的回调消息,如果有令狐冲的消息,就处理一下,没有的话,就看看左冷禅等群友最近在哪里发财,然后问问能不能带上自己。可以想象,这种情调下的抽烟,那才叫吞云吐雾般的享受。

另外,再说明一下,如果只有基于时钟消息的循环查询,那么若工作线程没有更新,则每次查询时都得到同样的工作状态,这是正常现象。但是,如果规定,对于工作线程的每次状态,主线程只能用一次,那么基于时钟消息的循环查询,在工作线程没有更新状态的时候,就完全无法遵守规定。
为了好理解,这里举例说明。

令狐冲是打牌高手,岳不群要给岳灵珊和令狐冲准备嫁妆,但没钱,于是派令狐冲去澳门赌坊弄点钱。两人约定,令狐冲每天生活费要1000元,出发前岳不群只给令狐冲一天的生活费,令狐冲每局从牌桌上能赢得至少0元,赢来的钱马上存到银行卡里,岳不群知道令狐冲的银行卡号,他每隔30分钟从令狐冲的卡里划1000元到自己的卡里。令狐冲由于要集中精力打牌,因此不管输赢,都不会打电话告诉岳不群。

大家想想,岳不群有没有可能把令狐冲的生活费划走?
        有些人觉得不会,有些觉得会。注意上面的约定,并未包括“岳不群在划账之前要检测一下令狐冲银行卡里面的钱够不够”。为什么不包括呢?因为令狐冲有自己的隐私,不愿意让岳不群检查自己的钱包(也就是说,对象的封装性,决定了对象必须封装一些内部状态,不让外部读取。)因此,如果令狐冲手气不好,有好几局没赢到钱,岳不群肯定会把他的生活费划走的。
        如果两人加一条约定,就可以解决上述问题。

令狐冲:“师傅,每次赢了超过1000块钱,我就给您发条微信,告诉您赢了多少,您有空的时候就处理一下。”(注意:最开始的时候,令狐冲有1000块钱,在赌博开始前,他是不会给岳不群发消息的。)
        岳不群:“此计甚好。不过,我也有自己的事情,半个小时处理一下(相当于时钟消息),若正好在看微信(相当于在窗体上移动鼠标),我就马上处理。”

可见,令狐冲每次有更新的时候,就把更新的状态(新赢到的钱)通过回调(微信消息)告诉岳不群,岳不群在主线程中通过微信把新赢到的钱转走,但对于令狐冲那1000块钱生活费,因为它不是新的变化,岳不群即使看到了也不会去处理。

再举一个例子,电脑通过COM口连接了一个扫码枪,扫码枪收到一个条形码(比如“69123432432”)后,就存在内部,然后通过回调告诉电脑,电脑在回调里处理这个码。电脑同时运行一个时钟,假设周期是1秒。如果没有回调方法,则电脑只能在时钟事件里去处理条形码“69123432432”,算出当前总价,处理完后,下一个1秒钟,电脑又看到了条形码“69123432432”,此时就有了疑问,这是上一个处理过的商品,还是顾客又扫了同一个商品?这时就很难区分了。
        对于这种情况,有些童鞋会说,电脑可以在用完“69123432432”之后,电脑让扫码枪删除内部保存的“69123432432”。在下一个1秒钟,如果顾客没有扫描商品,则码是空的,如果顾客扫描了商品,即使仍然是同一个“69123432432”,电脑也能够安全地使用它。
        但是,不要忘了,电脑和扫码枪是两个独立的线程,电脑线程让扫码枪删除“69123432432”,但扫码枪线程可能同时新收到顾客扫描的“69123432432”,那么,此时就会产生资源争用问题。
        如果在扫码枪的回调方法里处理条形码“69123432432”,问题就好办了。由于扫码枪仅仅在收到新的条形码后才会进行回调,因此能确保每个条形码能被处理一次,且仅被处理一次。

5 结论

本文简单地讨论了Delphi中的多线程编程,内容基础、详实,但是,限于作者水平不高,可能有诸多错误的地方,请大家批评指正。

6 做点广告

Delphi多线程编程基础入门相关推荐

  1. C++ 高性能计算之多线程简单基础入门教程

    C/C++ 高性能计算之多线程简单基础入门教程 比起别人的盲目罗列函数接口,鹦鹉学舌式的解释每一个输入参数和输出参数,一味求全而无重点,我的文章更侧重于入门知识的讲解,宁缺毋滥,只有一些最简单的入门用 ...

  2. python快乐编程—基础入门-从萌新到大神必读书籍 《Python快乐编程基础入门》...

    2019年,全球信息化进程持续加快,IT行业繁荣发展.作为新时代IT人,不仅需要强大的理论知识,更需要过硬的技术.Python作为最受欢迎的编程语言之一,作为人工智能时代的首选语言,因其受众多.用途广 ...

  3. WPF编程基础入门 ——— 第二章 XAML

    XAML 简述 XAML(eXtensible Application Markup Language,可扩展应用程序标记语言)是微软公司创建的一种新的描述性语言,用于搭建应用程序用户界面.XAML实 ...

  4. 「Linux」Linux Shell 编程基础入门

    Linux Shell 编程基础入门 1. 变量 1.1 变量定义 1.2 使用变量 1.3 引号 1.4 将命令的结果赋值给变量 1.5 位置参数 1.6 特殊变量及其含义 2. 字符串 2.1 字 ...

  5. WPF编程基础入门 ——— 第三章 布局(五)布局面板WrapPanel

    WPF布局--布局面板WrapPanel WPF--WrapPanel布局控件 WrapPanel实例--十个按钮 WPF--WrapPanel布局控件 WrapPanel(自动折行面板),允许任意多 ...

  6. Delphi 多线程编程(1)

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

  7. java多线程编程从入门到卓越(超详细总结)

    导读:java多线程编程不太熟?或是听说过?或是想复习一下?找不到好的文章?别担心我给你们又安利一波,文章内容很全,并且考虑到很多开发中遇到的问题和解决方案.循环渐进,通俗易懂,文章较长,建议收藏再看 ...

  8. 多线程编程——基础语法篇

    多线程编程 文章目录 多线程编程 一.Thread 1.1 Thread用法一 1.2.Thread用法二 (Runnable) 1.3.Thread用法三 1.4.Thread用法四 1.5.Thr ...

  9. C++多线程编程(入门实例)

    多线程在编程中有相当重要的地位,我们在实际开发时或者找工作面试时总能遇到多线程的问题,对多线程的理解程度从一个侧面反映了程序员的编程水平. 其实C++语言本身并没有提供多线程机制(当然目前C++ 11 ...

最新文章

  1. Android --- Unable to resolve dependency for ‘:app@debug/compileClasspath‘: Could not resolve com.a
  2. JavaScript强化教程——jQuery选择器
  3. ACM Doing Homework again
  4. 自绘列表框控件显示略缩图----再稍微改进点点。。
  5. 第12步 用户模块前端(客户)
  6. 误删表数据,如何恢复过来
  7. 干货 | 懂点儿经济学有什么用?
  8. 实现二叉树各种遍历算法
  9. 计算机故障升温降温法,电脑故障排除1000例
  10. 学习C语言的必备书籍-从入门到精通
  11. 初级、中级和高级开发人员之间有什么区别?
  12. Java二维数组的错误写法分析
  13. amCharts使用方式
  14. 第10章 大数据与云数据库管理
  15. 研究B站视频编号含义 - av | ep | md ...
  16. Oracle TRUNCATE语法
  17. win10应用商店linux,Ubuntu 20.04 LTS已可通过Windows 10应用商店获取
  18. 1896-2016年奥运会数据分析(SQL)
  19. Java Web项目中缺少Java EE 6 Libraries怎么添加
  20. SAP资产折旧-工作量法

热门文章

  1. smartctl 使用
  2. 树莓派摄像头安装及配置
  3. php默认ssl版本号,centos 6.5系统PHP环境下的CURL库的SSL Version默认为NSS,怎么变更为OpenSSL?...
  4. 电池电量管理软件Batteries for Mac
  5. 脚本引流,什么是脚本引流?脚本引流会是骗局么?
  6. 2022抖音私信名片系统源码+链接跳转引流技术
  7. 机器视觉检测设备能否取代质检员工作
  8. 30年初心坚守 神州信息用数字技术实现普惠金融
  9. 最高上涨30分!又有17所985大学公布计算机考研校线
  10. EPlan2.6版本解决卡顿未响应问题(安装防卡顿布丁)