VCL多线程 TTHREAD

    本文地址:http://tongxinmao.com/Article/Detail/id/188

    先说一下RTL和VCL

      RTL(Run-Time library),运行时库,包括System、SysUtils、Math三个单元,提供的函数与语言、编译器、操作系统及进程有关

      RTL提供类之间继承于 TObject 和 RTL内部的类

      VCL(Visual Component Library),可视化组件库,包括Graphics、classes、Controls等与类和组件相关的单元

     

    VCL不是线程安全的

       因为VCL不是线程安全的,所以对VCL的访问只能在主线程中。这将意味着:所有需要与用户打交道的代码都只能在主线程的环境中执行。这是其结构上明显的不足,并且这种需求看起来只局限在表面上,但它实际上有一些优点

      开发多线程项目的主要需要考虑的一点就是同步多线程使用资源,不要产生冲突,其实想Delphi的VCL组件也是一种资源,但是VCL不是线程安全的,不能让其他的线程使用,只能通过主线程来使用它

     

    1.可能的一个应用场景

      比如在开发图形化界面的项目中,需要连接数据库,可以采用这样的策略:用主线程来绘制组件到图形化界面,而连接数据库的过程在子线程中实现。

      这时候能够保证就算在连接数据库的时候出现问题,子线程可能会去尝试一直连接,但是因为各个线程之间互不相干,各自执行各自的逻辑代码,所以不影响主线程绘制组件,所以窗体并不会卡住

      但是可能要在子线程中读取数据库中的数据来展示数据,这个时候,因为VCL 不是线程安全的,所以不能允许主线程(绘制组件)和子线程(想要去将从数据库中的数据“写”到界面上)同时去操作组件

      所以可能的解决方法(见 3.Synchronize() 方法)就是 使用Synchronize() 方法来调用子线程想要将数据“写到”界面的方法,这样就能保证这个方法实际上是在主线程中执行的(虽然它是子线程的方法,但是通过Synchronize() 方法可以实现将子线程的方法放到主线程中执行),这样就能保证不会出现多个线程使用VCL 组件

     

    2.单线程用户界面的好处

      首先,只有一个线程能够访问用户界面,这减少了编程的复杂性。Win32 要求每个创建窗口的线程都要使用 GetMessage() 建立自己的消息循环。正如你所想的,这样的程序将会非常难于调试,因为消息的来源实在太多了

      其次,由于 VCL只用一个线程来访问它,那些用于把线程同步的代码就可以省略了,从而改善了应用程序的性能

     

    3.Synchronize() 方法

      在 TThread中有一个方法叫Synchronize(),通过它可以让子线程的一些方法在主线程中执行。Synchronize() 的声明如下

    1
    procedure Synchronize(Method: TThreadMethod);

      参数Method 的类型是 TThreadMethod(这是一个无参数的过程),类型的声明如下

    1
    2
    type
        TThreadMethod = procedure of object;

      Method参数用来传递在主线程中执行的方法。以 TTestThread对象为例,如果要在一个编辑框中显示计算的结果。首先要在TTestThread中增加能对编辑控件的Text 属性进行修改的方法,然后,用Synchronize() 来调用此方法

      给这个方法取名 GiveAnswer(),下面列出例子的代码,其中包含了更新主窗体的编辑控件的代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    unit ThrdU;
     
    interface
    uses
        Classes;
     
    type
        TTestThread = class(TThread)
        private
            Answer: Integer;
        protected
            procedure GiveAnswer;
            procedure Execute; override;
        end;
     
    implementation
    uses
        SysUtils, Main;
     
    {TTestThread}
    procedure TTestThread.GiveAnswer;
    begin
        MainForm.Edit1.Text := IntToStr(Answer);
    end;
     
    procedure TTestThread.Execute;
    var
        I: Integer;
    begin
        FreeOnTerminate:= True;
        for I:= 1 to 2000000 do
        begin
            if Terminated then Breadk;
            Inc(Answer, Round(Abs(Sin(Sqrt(I))));
            Synchronize(GiveAnswer);
        end;
    end;

      Synchronize() 的作用是在主线程中执行一个方法。

      当你在程序中第一次创建一个附属线程时,VCL 将会从主线程环境中创建和维护一个隐含的线程窗口。此窗口唯一的目的是把通过Synchronize() 调用的方法排队

      Synchronize() 把由Method 参数传递过来的方法保存在 TThread的 FMethod字段中,然后,给线程窗口发送一个CM_EXECPROC消息,并且把消息的lParam 参数设为self(这里是值线程对象)。当线程窗口的窗口过程收到这个消息后,它就调用 FMethod字段所指定的方法。由于线程窗口是在主线程内创建的,线程窗口的窗口过程也将被主线程执行。因此,FMethod字段所指定的方法就在主线程内执行

      下图形象地说明了 Synchronize() 的内部机制和原理

     

    4.用消息来同步

      可以利用在线程之间使用消息同步以替代 TThread.Synchronize() 方法。可以使用API 函数SendMessage() 或 PostMessage() 来发送消息。例如下面一段用来在一个线程中设置另一个线程中的编辑框文本的代码

    1
    2
    3
    4
    5
    6
    var
        S: String;
    begin
        S:= 'hello from threadland';
        SendMessage(SomeEdit.Handle, WM_SETTEXT, 0Integer(PChar(S)));
    end;

      

    事件(Event)

      事件(Event)与Delphi中的事件有所不同。从本质上讲,Event其实就相当于一个全局的布尔变量。它有两个赋值操作:Set和ReSet,相当于把它设置为 True或False。而检查它的值是通过WaitFor操作进行。对应在Windows平台上,是三个API函数:SetEvent、ResetEvent、WaitForSignalObject(实现WaitFor功能的API还有几个,这是最简单的一个)。

      这三个都是原语,所以Event可以实现一般布尔变量不能实现的在多线程中的应用。Set和Reset的功能前面已经说过了,现在来说一下WaitFor的功能:

      WaitFor的功能是检查Event的状态是不是为Set状态(相当于True),如果是则立即返回,如果不是,则等待它变为Set状态,在等待期间,调用WaitFor的线程处于挂起状态。另外WaitFor有一个参数用于超时设置,如果此参数为0,则不等待,立即返回Event的状态,如果是INFINITE则无线等待这,直到Set状态发生,若是一个有限的数值,则等待相应的毫秒数之后返回Event的状态

      当Event从Reset状态向Set状态转换时,唤醒其他由于WaitFor这个Event而挂起的线程,这就是他为什么叫Event的原因。所谓“事件”就是指“状态的转换”。通过Event可以在线程间传递这种“状态转换”信息。

      当然用一个受保护(见下面的临界区介绍——)的布尔变量也能实现类似的功能,只要用一个循环检查此布尔值的代码来代替WaitFor即可,从功能上说完全没有问题,但是实际使用中就会发现,这样会占用大量的CPU资源,降低系统性能,影响到别的线程的执行速度,所以是不经济的,有时候甚至可能出问题,所以不建议这么做

    临界区(Critical Section)

      临界区则是一项共享数据访问保护的技术。它其实也是相当于一个全局的布尔变量。但对它的操作有所不同,它只有两个操作:Enter和Leave,同样可以把它的两个状态当做True和False,分别表示现在是否处于临界区中。这两个操作也是原语,所以它可以用在多线程应用中保护共享数据,防止访问冲突

      用临界区保护共享数据的方法很简单:在每次要共享数据之前调用Enter设置进入临界区标识,然后再操作数据,最后调用Leave离开临界区。它的保护原理是这样的:当一个线程进入临界区之后,如果此时另一个线程也要访问这个数据,则它会在调用Enter时,发现已经有线程进入临界区,然后此线程就会被挂起,等待当前在临界区的线程调用Leave离开临界区,当另一个线程完成操作时,调用Leave离开后,次线程就会被唤醒,并设置临界区标志,开始操作数据,这样就防止了访问冲突


    互斥控制是为了避免一个线程在使用某一个对象或全局变量与其他线程发生冲突。实现线程互斥的方法有:

    (1)   访问代码委托给VCL主线程执行。在线程中若要调用可视化的方法或访问其属性,可将执行代码委托给VCL主线程执行,否则会发生并发访问冲突。委托的方法是先将使用可视化组件的代码单独编成一个函数,函数原型是void 函数名(void),然后电泳TThread类的成员函数Synchronize(函数名)来调用它,VCL主线程顺序执行所有组建对该组建的访问(包括响应人机界面事件、windows系统事件等),从而不会发生冲突。

    (2)   使用对象锁。有些VCL类提供对象锁,可以使用对象的LOCKUNLOCK方法进行加锁与解锁。当访问这些对象时,可调用LOCK方法锁住对象,然后访问该对象,访问完毕后调用对象的UNLOCK方法释放该对象。

    (3)   使用临界区对象。若要访问一个全局变量,则可设置一个临界区对象(TCritical Section)来实现互斥,该对象有AcquireRelease两个方法。Acquire方法阻塞其他线程,执行临界区代码,而Release方法释放等待进入临界区的线程。例如:设Q为全局变量,Crit1为临界区对象,在访问Q进入临界区时须执行Crit1.Acquire(),访问后退出临界区时须执行Crit1.Release()。


    在这里说一些题外话:很多人问过我,如何才能“立即”终止线程(当前是指用TThread 创建的线程)。结果当然是不行!终止线程的唯一的方法就是让Execute 方法执行完毕,所以一般来说,要让你的线程能尽快终止,必须在Execute 方法中在较短的时间内不断检查Terminated标志,以便能及时地退出。这是设计线程代码的一个很重要的原则!

      当然如果你一定要能“立即”退出线程,那么TThread 类不是一个好的选择,因为如果用API强制终止线程的话,最终会导致TThread 线程对象不能被正确释放,在对象析构时出现 Access Violation。这种情况你只能使用API或者RTL函数来创建线程

      如果线程结束处于启动挂起状态,则线程转入到运行状态,然后调用WaitFor进行等待,其功能就是等待到线程结束后才继续向下执行。关于WaitFor的实现,将放到后面说明。

      线程结束后,关闭线程Handle(正常线程创建的情况下Handle都是存在的),释放操作系统创建的线程对象。

      然后调用TObject.Destory 释放本对象,并释放已经捕获的异常对象,最后调用RemoveThread减少进程的线程数


    在线程函数内部,我们可以使用任意的全局变量,但有些变量我们并不希望同一线程类的其他实例共享它,就可以声明一个线程(thread-local)变量。通过将__thread修饰语加入变量声明就可以声明一个线程变量。例如 int __thread x; 声明一个整型变量。

    __thread修饰语只可用于全局(文件范围)或静态变量。指针和函数变量不能作为线程变量。使用“在写入时复制”语法的类,如AnsiStrings也不能作为线程变量。需要在运行时进行初始化或析构的类型也不能被声明为__thread类型。



    在多线程开发中,如果在多线程中访问主线程创建的对象,并触发了这个对象的事件,将会执行这个事件的处理函数,那么这个处理函数是在主线程中执行还是在触发事件的线程中执行呢?针对这个问题做了一下测试,如果没有通过Windows消息触发事件,则在子线程(触发事件的线程)中执行事件处理函数,如果是由Windows消息触发的事件,则由主线程执行事件处理函数.这是因为Windows消息只由创建控件的线程进行处理,那么由此引起的事件及其处理函数自然就在创建控件的线程中执行了.而普通的事件触发,则全部在子线程中完成的,因此在子线程中执行事件处理函数.由此也解释了对于需要执行大量的任务的子线程,如果需要主线程显示处理进度,则可以在子线程中直接修改进度条控件的当前位置,主线程负责处理界面显示.这是因为子线程在修改进度条控件的当前位置时,会将一个Windows消息投递到消息队列,进度条的创建线程(主线程)在处理这个消息的时候,刷新界面上的进度.那么如果要在子线程中创建一个控件,并处理其由Windows消息触发的事件,消息会有子线程处理,子线程的消息队列会管理在本线程中创建的控件的消息.

    验证1:普通的事件(无Windows消息触发)处理函数由子线程执行.

    unit Unit1;

    interface

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

    type

      TNotify = procedure of object;

      TCtrl = class
      private
        FNotify: TNotify;
      public
        property Notify: TNotify read FNotify write FNotify;
        procedure TriggerNotify;
      end;

      TThrd = class(TThread)
      private
        FCtrl: TCtrl;
      protected
        procedure Execute; override;
      public
        constructor Create(ACtrl: TCtrl); reintroduce;
      end;

      TForm1 = class(TForm)
        Button1: TButton;
        Button2: TButton;
        procedure FormCreate(Sender: TObject);
        procedure FormDestroy(Sender: TObject);
        procedure Button1Click(Sender: TObject);
        procedure Button2Click(Sender: TObject);
      private
        FCtrl: TCtrl;
        procedure Notify;
      public
        { Public declarations }
      end;

    var
      Form1: TForm1;

    implementation

    {$R *.dfm}

    procedure TForm1.Button1Click(Sender: TObject);
    begin
      OutputDebugString(PChar(Format('>>>>>:%d', [GetCurrentThreadId])));
      FCtrl.TriggerNotify;
    end;

    procedure TForm1.Button2Click(Sender: TObject);
    begin
      OutputDebugString(PChar(Format('触发多线程的线程:%d', [GetCurrentThreadId])));
      TThrd.Create(FCtrl);
    end;

    procedure TForm1.FormCreate(Sender: TObject);
    begin
      FCtrl := TCtrl.Create;
      FCtrl.Notify := Self.Notify;
    end;

    { TCtrl }

    procedure TCtrl.TriggerNotify;
    begin
      if Assigned(FNotify) then
        FNotify;
    end;

    procedure TForm1.FormDestroy(Sender: TObject);
    begin
      FCtrl.Free;
    end;

    procedure TForm1.Notify;
    begin
      OutputDebugString(PChar(Format('>>>>>:%d', [GetCurrentThreadId])));
    end;

    { TThrd }

    constructor TThrd.Create(ACtrl: TCtrl);
    begin
      inherited Create(False);
      FCtrl := ACtrl;
    end;

    procedure TThrd.Execute;
    begin
      FCtrl.TriggerNotify;
    end;

    end.

    验证2:进度条处理当前位置变化的代码中是通过Windows消息通知创建控件的线程(主线程)的

    procedure TProgressBar.SetPosition(Value: Integer);
    begin
      if not F32BitMode and ((Value < 0) or (Value > Limit16)) then
        ProgressLimitError;
      if HandleAllocated then SendMessage(Handle, PBM_SETPOS, Value, 0)
      else FPosition := Value;
    end;

     

    验证3:可以在多线程中触发一个主线程创建的控件的事件,在处理函数中输出处理线程的ID,比较发现处理线程正是主线程.

    验证4:在子线程中创建控件并处理由Windows消息触发的事件,事件由子线程执行.结论:子线程触发的控件事件,处理函数由创建控件的那个线程来执行.

    结论:当一个线程第一次被创建时,系统假定线程不会用于任何与用户相关的任务.这样可以减少线程对系统资源的要求.但是,一旦该线程调用一个与图形用户界面有关的函数 如检查它的消息队列或建立一个窗口 ),系统就会为该线程分配一些另外的资源,以便它能够执行与用户界面有关的任务.特别是,系统分配了一个THREADINFO结构,并将这个数据结构与线程联系起来.

      另外管理控件与创建线程的关系是由Windows来完成的,将线程ID和控件的Handle进行映射,当需要向某个Handle发送消息时,会搜索其创建线程,并将消息投递到对应线程的消息队列中.




    在BCB中,使用了__closure关键字扩展了标准C++,从而实现了委托:

    typedef void __fastcall (__closure *FCallBack)(int);


    __closure纯粹是为了兼容vcl而出现的,它是vcl事件callback模型的基础,因为它是类型安全的。
    事实上__closure表示忽略成员函数中第一个参数的类型(此参数当然就是this了),而对其他参数和调用方式(如__stdcall)进行类型检测。
    另外__closure函数指针和普通的函数指针是不同的,__closure函数指针是8个byte,前4个byte保存对象指针,后4个byte为成员函数指针。


    你要明白,__closure和__stdcall虽然都是可以用来修饰函数指针的,但是方向是不同的,
    __stdcall修饰函数的调用方式,__closure是指修饰函数为成员函数。其中的SomeClass::就相当于__closure关键字的作用。


    上一篇:mac地址对应的厂商
    下一篇:ESP8266 AT透传