DELPHI盒子
!实时搜索: 盒子论坛 | 注册用户 | 修改信息 | 退出
检举帖 | 全文检索 | 关闭广告 | 捐赠
技术论坛
 用户名
 密  码
自动登陆(30天有效)
忘了密码
≡技术区≡
DELPHI技术
lazarus/fpc/Free Pascal
移动应用开发
Web应用开发
数据库专区
报表专区
网络通讯
开源项目
论坛精华贴
≡发布区≡
发布代码
发布控件
文档资料
经典工具
≡事务区≡
网站意见
盒子之家
招聘应聘
信息交换
论坛信息
最新加入: webb123
今日帖子: 34
在线用户: 20
导航: 论坛 -> DELPHI技术 斑竹:liumazi,sephil  
作者:
男 akai1203 (w-dins) ★☆☆☆☆ -
普通会员
2021/1/14 22:39:43
标题:
fmx下线程阻塞主UI的问题 浏览:1836
加入我的收藏
楼主: 我在FMX的界面上有一个跑马灯的动画,原来用timer每隔3秒去测试服务器的连通性,后来发现timer事件触发时,跑马灯的动画就会卡顿,后来改成tthread线程方式,发现用线程也会卡顿,大家帮忙分析一下是什么原因,怎么解决

代码如下:

procedure THelper.TryToConnectServer;
begin
  TThread.CreateAnonymousThread(
    procedure()

    begin
      while 1 > 0 do
      begin

        try
          try
          TCPClient.ReadTimeout := 1000;
          TCPClient.ConnectTimeout := 1000;
          TCPClient.Port := 8099;
          TCPClient.Host := ConfigController.Config.Server;
          TCPClient.Connect;  //拿掉这行动画就不卡顿了
          FConnectedFailTime := 0;
          except
          FConnectedFailTime := FConnectedFailTime + 1;
          end;
        finally
          TCPClient.Disconnect;
        end;
        sleep(5000);
      end;
    end).Start;
end;
----------------------------------------------
-把学习当信仰
作者:
男 onlykingqc (simon) ★☆☆☆☆ -
普通会员
2021/1/15 10:41:01
1楼: 你都sleep(5000)了
cpu都睡了,能不卡吗?
----------------------------------------------
-
作者:
男 akai1203 (w-dins) ★☆☆☆☆ -
普通会员
2021/1/15 11:25:24
2楼: 不是在主线程sleep
----------------------------------------------
-把学习当信仰
作者:
男 thinknet (thinknet) ★☆☆☆☆ -
盒子活跃会员
2021/1/15 15:58:51
3楼: 将TCPClient放在线程中创建,别用主线程中的
----------------------------------------------
-
作者:
男 pcplayer (pcplayer) ★☆☆☆☆ -
普通会员
2021/1/15 21:46:05
4楼: 先别管跑马灯动画卡不卡。你先说你的 UI 这个时候用鼠标键盘有没有响应?
----------------------------------------------
-
作者:
男 wenyue0811 (wenyue0811) ★☆☆☆☆ -
普通会员
2021/1/16 12:54:47
5楼: FMX 下的线程处理好像相对较为复杂一些...我也说不明白(因为水平有限). 我贴一些从一个文章中翻译来的一些说明. 提供给你做一点参考吧....希望有用.

TThread 类 (单元 System.Classes) 是线程对象概念的包装文件. 您可以使用 TThread 类作为父类来继承自己的类, 然后提
    供 Execute 方法的自定义的实现. 您在 Execute 方法中放置的代码将在单独的线程中执行. 您只需要创建一个新类的实例并
    使用 TThread.Start 方法启动它即可.

    当涉及到并行 (多线程) 编程时, 共享资源总是有问题的. 一个常见的场景就是定义你的 TThread 继承的类,  将所有必要的
    数据作为类的成员 (字段或属性) 提供. 在启动线程之前 (在创建实例之后),  你需要一个机会来为这些字段或者属性提供值.

    正在运行的线程将会使用这些值, 而不必担心并发问题或者同步需求.  显然, 对于原始数据类型 (字符串 / 数字 / 记录等)
    来说, 这很容易做到, 但可能需要更多地关注对象实例或者其他的共享数据.

    事实上, 复制_引用对象_并不意味着您正在向线程提供整个对象的副本.  可能会发生_由共享相同引用的_其他人引起的_最终
    交互. 这使得对象成为了共享资源, 在这种情况下, 您通常需要注意. 您可能需要实现一些同步或者锁定策略, 以确保对象被
    不同的并发线程安全地操作.

    您可以选择 File | New | Delphi | Individual files | Thread Object, 并让 IDE 生成具有模板结构的新的线程文件. 下
    面的屏幕截图显示 IDE 对话框, 其中选择了线程对象条目:

          unit Threads.Simple;

          interface

          uses
          System.Classes, System.SysUtils;
          type

          TSimpleThread = class(TThread)

          private
          protected
          procedure Execute; override;
          end;

          implementation

          { TSimpleThread }

          procedure TSimpleThread.Execute;
          begin
          { Place thread code here }
          end;

          end.


    要创建和启动一个新的 TSimpleThread 实例, 只需要调用它的无参数 Create 构造函数即可. 这将创建一个新的实例并立即
    开始执行. 如果需要设置实例 (例如, 要将值设置为某些属性), 则可能需要执行以下的任一项:

        1. 为您的 TSimpleThread 类提供自定义的构造函数, 将数据作为参数传递和存储, 然后在线程执行期间使用.

        2. 使用 Create 构造函数的另一个版本来创建处于挂起状态的线程, 具有 CreateSuspended 布尔参数, 设置
          你的实例, 然后调用 Start 方法来实际让线程开始执行.

          请记住, 线程的构造函数将在调用线程的上下文 (即可能是 main / UI 线程) 中执行, 因此它是操作共享
          对象或者数据的安全场所.

 每个线程实例都有一个布尔字段, 即 Terminated, 可以通过公共的 Terminate 方法的调用来设置. 你可能会想调用 Terminate,
    这会有一些即时的效果, 比如停止线程或者杀死线程, 但事实并非如此. 调用 Terminate 将简单地将 Terminated 设置为 True,
    并因此调用 TerminatedSet 保护过程.

        小心, Terminate 方法将在调用线程 (即 main / UI 线程) 的上下文中执行, 因此将会发生后续的
        TThread.TerminatedSet 方法执行.

    线程代码有责任定期的检查是否有人 (来自外部的) 要求提前终止线程. 例如, 这可以在多步骤操作的步骤之间进行, 或者如
    果线程有一个控制循环, 则在循环的每一次迭代结束时进行.  处理这种情况的一个简单的方法是退出 Execute 方法, 从而导
    致其终止. 下面的代码块举例说明了处理线程代码中 Terminated 的两种常见模式:

          // 多步骤操作, 在步骤之间进行检查
          procedure TSimpleThread.Execute;
          begin
          DoSomething1;   // 处理一些代码
          if Terminated then Exit;

          DoSomething2;   // 处理一些代码
          if Terminated then Exit;

          DoSomething3;
          end;

          // 循环控件, 每次迭代前检查
          procedure TSimpleThread.Execute;
          begin
          while not Terminated do
          DoSomething;
          end;


    在前面的代码中, 这个模式显然对线程的实现给予了很大的控制 (和责任), 从而提供了适当的处理线程特定内部的机会, 即清
    理_终止时_的_中间数据.

    如果线程被卡在某个无限的循环中的话 (或者永远不检查 Terminated 值的话), 就没有简单的方法可以优雅地从外部来阻止它
    了. 这意味着线程将会被隔离. 与外部的唯一的接触点是 Terminated 属性. 正如您在非响应进程中所做的那样,  有一些方法
    (通过 API 的调用) 来终止线程, 但这可能会导致资源损耗.

    TThread 提供的另一个重要特性是 OnTerminate 事件, 一旦 Execute 方法完成了, OnTerminate 就会被触发.  鉴于线程通常
    用于实现异步操作 (shoot-and-forget 或 shoot-and-report), 一旦线程结束你可能需要执行一些操作.  提供  OnTerminate
    事件处理程序将可以完成这个任务.

    为了您的方便, OnTerminate 事件处理程序的执行与应用程序的主线程是同步的 (因此, 它是与 UI 交互的安全场所). 正确的
    处理线程实例的生命周期是强制性的, 以避免内存泄漏或者发生不一致的行为.

    您需要注意, 具有自己状态的线程 (创建 / 运行 / 终止 / 销毁), 并且在使用对_线程对象_的引用时_需要考虑到这一点.

    SimpleThreadProject 演示应该有助于理解线程生命周期中发生的事件链.

        考虑到您正在使用的实际的内存模型, 应该对线程 (以及通常每个 Delphi 对象) 进行一些考虑. 对于桌面平台,
        我们可以手动处理对象生命周期的管理, 因此你要负责每个创建的对象的生命周期.

        对于移动平台 (和 Linux 平台), 编译器 (基于LLVM) 启用了自动引用计数 (ARC).

        理解内存管理的一个很好的参考是 Delphi 的内存管理: 用于经典的和 ARC 编译器的, 来自 Dalija Prasnikar 和
        Neven Prasnikar Jr.


    让我们讨论示例的一些方面 (完整的源代码是随书提供的) .

    一个 FThread: TSimpleThread 字段已经添加到我们的主要窗体中. 这将作为对所有窗体事件处理程序的辅助线程的引用. 要
    启动线程, 我们使用以下代码:

          FThread := TSimpleThread.Create(True);
          FThread.FreeOnTerminate := True;
          FThread.OnTerminate := TerminateThreadHandler;
          FThread.OnProgress  := procedure
          begin
          StatusLabel.Text := 'Running, counter = ' + FThread.Counter.ToString;
          end;
          FThread.Start;


    如前所述, 我们正在创建处于挂起状态的线程, 这样我们就可以在启动实际实例之前来设置一些属性.

    TThread.FreeOnTerminate 属性允许您在完成线程的执行后设置这个线程自动销毁. 这显然是一个非常方便的功能, 在 shoot-
    and-forget 的情况下, 但也对 shoot-and-report 的情况有效, 因为它允许开发人员找到一个适当的时间去销毁线程. 当销毁
    OnTerminate 处理程序中的线程可能会导致问题, 因为在对象本身的事件处理程序中, 销毁对象通常是一种糟糕的做法.

    TSimpleThread.OnProgress 属性是为了让 TSimpleThread 在其操作期间通知一些进度更新而添加的.  它是通过匿名方法实现
    的事件. 这是一个非常方便的现代化的语言功能 - 如果你还不熟悉它, 你真的需要学习它.

        *: 为了提高你的 delphi 语言的技能, 我强烈建议寻找马可坎图的所有书籍和在线材料.


    如果您是匿名方法的新手,  只需将其视为可以作为值来传递的代码.  TSimpleThread 将会有机会在需要时执行它.  我们将
    TSimpleThread 实现的一部分_委托给_调用线程, 它能够在 main / UI 线程的范围内编写一段代码, 这些代码将从 TSimpleThread
    的代码中调用.

        注意不要混淆_代码定义 / 范围 / 调用 / 执行上下文的概念. 一段代码必须在某个地方定义 (例如在 OOP 中,
        它在类方法中), 并由 "某人" (线程) 来执行, 因为对它的调用. 在我们的例子中, 我们有一个类 (TSimpleThread)
        包含了一个线程对象 (executor) .


    我们还有一个窗体 (TMainForm 的实例, 它存在于 main / UI 线程中), 它提供了一些代码的定义 (通过匿名方法). 匿名方法
    是在窗体中定义的 (因此它可以访问 UI 元素, 存在于 main / UI 线程中), 它通过 OnProgress 属性赋值传递给 TSimpleThread,
    最后, 由于 TSimpleThread 实现中的某些代码 (即放置在 Execute 方法中的代码 - 它可能是任意的代码) 对它进行了调用,
    故而它被执行.

    换句话说, 它是在 TMainForm 类中定义的, 并由 TSimpleThread 调用了. 调用一方才有机会来确定执行.  如果直接调用, 调
    用方和匿名方法将会共享执行 (TSimpleThread 工作线程). 否则 (通过同步机制),  调用方可以将匿名方法执行放在另一个线
    程 (即 main / UI 线程) 的上下文中. 并行编程首先可能很难得到.

    最后, 线程通过其 Start 方法的启动调用. 线程 (系统) 对象得到了实际的分配. 执行将开始, 并且将包括提供的线程实现,
    即其执行方法.

    在下面的代码中, 有一个控件循环, 在每次迭代时检查 Terminated 是否仍然为 False:

          begin
          (...)
          FCounter := 0;
          while not Terminated do
          DoSomething; (...)
          end;


    下面的代码显示条件是否存在 (这意味着线程尚没有收到终止请求) , 然后执行 DoSomething:

          procedure TSimpleThread.DoSomething;
          begin
          (...)
          Sleep(1000);
          Inc(FCounter);
          if Assigned(OnProgress) then
          Synchronize(FOnProgress);
          end;


    再次, Sleep 函数被用来模拟正在进行的一些密集的处理. 每次执行 DoSomething 时都会更新一个计数器. 发生 OnProgress
    事件 (如果提供了任何的处理程序), 其处理程序通过调用 Synchronize 触发. 这意味着执行程序是 main / UI 线程的. 这被
    证明是方便的, 因为它是非常常见的.

    典型的 OnProgress 事件处理程序与 UI 有关.  通过在主 UI 线程的上下文中执行, 所有的可视化组件都将以线程安全的方式
    访问.

    下面的代码显示了另一个事件处理程序, 即 TerminateThreadHandler 程序:

          procedure TMainForm.TerminateThreadHandler(Sender: TObject);
          begin
          StatusLabel.Text := 'Completed, counter = ' + FThread.Counter.ToString;
          FThread := nil;
          end;


    从前面的代码中可以看到, 它除了用计数器的最终值更新 StatusLabel 之外什么也没做. 它还将 FThread 变量设置为 nil.这
    与实际销毁对象不同 - 我们只是清理了对线程实例的引用, 因为我们知道线程将被销毁 (多亏了 FreeOnTerminate 机制), 我
    们不想再意外地引用它了.

        这本书提供了实际的演示, 也有一些 CodeSite 调用来记录所有的事件. CodeSite 还保留了它正在记录的线程的
        信息, 因此它将帮助您正确地跟踪正在发生的事情, 以及哪个线程负责不同的日志行.


    最后, 但并非最不重要的是, 由于我们的线程中有一个循环, 直到线程从同一 TThread 类的外部终止为止, 所以我们需要一个
    按钮来停止正在运行的线程. 以下的代码显示其 OnClick 事件处理程序:

          procedure TMainForm.TerminateButtonClick(Sender: TObject);
          begin
          if Assigned(FThread) and FThread.Started then
          begin
          FThread.Terminate;
          StatusLabel.Text := 'Terminated, waiting completion... 已终止,等待完成...';
          end;
          end;


    在前面的代码示例中, 我们可以看到创建一个线程, 配置一些事件处理程序 (OnTerminate 和 OnProgress), 通过 FreeOnTerminate
    属性设置自动销毁, 然后启动. 线程将进入一个循环, 定期的执行一些代码, 并在每次迭代时通知进度 (执行提供的 OnProgress
    事件处理程序). 一旦请求终止, 一些代码 (即 OnTerminate 事件处理程序) 将会收集线程的最终结果. 然后它将自毁, 我们
    将准备重新开始 (当然还有一个新的实例) .

    这个小示例的目的是突出您在进行并行编程时需要考虑的所有方面 - 例如:

        1. 密码是什么.
        2. 有组织的?它是如何被执行的?
        3. 从哪个线程执行?
        4. 正在使用哪种同步技术?何时使用?


    CodeSite 输出应该可以帮助您检查实际发生的情况. 以下截图显示程序的典型输出:

    以下是对上一张截图的一些考虑:

        1. main / UI 线程的 ID:6424;    您可以看到线程的创建是如何在主线程以及 AfterConstruction 和对 Start
          方法的调用中进行的.

        2. 一旦线程开始执行, 您就可以看到日志条目: ID:4008 的新线程.

          请注意, TSimpleThreadDoSomething 方法正在辅助线程 (4008) 中执行, 而 OnProgress 事件处理程序由于使
          用 Synchronize 而在主线程 (6424) 的上下文中执行.

        3. 当用户单击 Terminate 按钮时, 终止请求就会发生; 执行 Terminate 方法 (以及随后的 TerminatedSet 方法
          的调用) 是 main / UI 线程, 您可以在日志 (Thread: 6424) 中看到.

        4. 一旦 Terminated 属性设置为 True, 循环就停止迭代 (当然是完成了当前迭代), 这就解释了为什么您可以在
          终止设置之后读取 OnProgress 事件处理程序的最后一次执行.

        5. TerminateThreadHandler 发生在 main / UI 线程中. 最后一条_日志行_用于 TSimpleThread 析构函数,因为
          线程本身会销毁 (因为 FreeOnTerminate 属性为 True); 请注意, 执行线程不是 main / UI 线程.

          一个常见的错误是认为 Tthread 继承类的每个方法都将在辅助线程的上下文中执行. 换句话说, 错误在于认
          为代码定义与代码执行有关, 而事实并非如此. 您必须遵循调用链条并且关注同步函数, 以便了解哪个线程
          正在执行哪一段代码.


    我建议, 如果您还不熟悉多线程编程, 请仔细检查这个示例, 直到每个方面都变得清晰. 多线程编程通常被认为是非常复杂的,
    但可以通过一些努力和勤奋来掌握.

        如果你真的对并行编程感兴趣, 我强烈建议你在 Primoz Gabrijelcic 这样的专家的帮助下适当地深入研究这件事.



    随着诸如匿名方法等现代语言特性的出现, Delphi 开发人员有了一个新的机会. 在 TThread 基类中实现了委托机制, 而不是
    为要在单独线程中_执行的_每个任务继承一个新类. 这个概念是一样的 — 我用它将 OnProgress 事件添加到  TSimpleThread
    类中.

        实际上, 这种行为已经在另一个类中实现了 — 匿名线程 (System.Classes 单元), 为了方便起见, 在 TThread
        中添加了一个 shortcut 类函数. TAnonymousThread 继承于 TThread, 基本上保留了要执行的匿名方法的引用,
        并在其 Execute 方法中执行它.


    TThread.CreateAnonymousThread 类函数 (自 Delphi XE 以来可用) 为您提供了一种方便的方法来定义将在 TThread.Execute
    中执行的匿名方法.

    这意味着 TThread 类现在可以有不同的 (委托) 行为, 而不需要每次从 TThread 继承特定的类.  这非常的有用, 并且大大简
    化了多线程软件的编写 (特别是当要执行的任务不复杂时) .

    下面的代码演示了当我们有 SimpleThreadProject 示例时, 如何在使用_匿名线程_来模拟相同的行为:

    procedure TMainForm.StartButtonClick(Sender: TObject);
    begin
        if Assigned(FThread) then Exit;
        (...)
        FTerminated := False;
        FThread := TThread.CreateAnonymousThread( procedure
          var
          LCounter: Integer;
          begin
          (...)
          LCounter := 0;
          while not FTerminated do
          begin
          (...)
          Sleep(1000);
          Inc(LCounter);
          TThread.Synchronize(nil,  procedure
          begin
          (...)
          StatusLabel.Text := 'Running, counter = ' + LCounter.ToString;
          end  // TThread.Synchronize...
          );
          end;
          FFinalCounter := LCounter; // Sync needed (...)
          end  // TThread.CreateAnonymousThread...
          );
        FThread.OnTerminate := TerminateThreadHandler;
        (...)
        FThread.Start;
        StatusLabel.Text := 'Started';
    end;


    从前面的代码中可以看到, 这种方法意味着不需要定义特定的类类型 (TSimpleThread) 来完成任务. 由于缺少类型定义, 我
    们也失去了为线程实例执行定义变量或者私有数据的位置.

    另一方面, 使用匿名方法, 我们可以访问定义匿名方法本身的范围. 因此, 我们可以利用窗体的字段和属性来访问 UI 组件,
    或者存储线程中计算的值 (即 FFinalCounter 字段). 我们还需要一些 TThread.Terminated 的替换, 它不是公开可见的, 因
    此它不能以与我们在 TSimpleThread 代码中所做的相同的方式来使用. 一个简单的布尔变量 (FTerminated) 就足够了.

        有时候, 继承 TThread 类仍然是正确的事情. 拥有类型定义也就意味着, 更多的选项来实现适当的信息隐藏
        和代码的隔离. 另一方面, Keep It Simple Stupid (KISS) 方法有时会启动, 匿名方法可以大大的简化常见
        的情况.


    下面的截图显示了 AnonymousThreadProject 演示的典型的运行的 CodeSite 日志:

    前面的截图显示了这个版本程序的执行流程. 这显然是一个更紧凑的解决方案. 较少的样板代码意味着整体可读性提高, 因为
    任务定义在一个地方, 您可以专注于它, 而不是只需要代码来实现多线程编程的内部. 它产生了巨大的差异, 特别是当任务定
    义很小的时候.

        为了简单起见, 我故意避免同步将 LCounter 的最终值传递给 FFinalCounter 变量.

        理论上, 需要一些同步, 因为我们有一个不同的 (背景) 线程来操作存活在 main / UI 线程中的对象(窗体).
        此外, 我制作了这个演示, 其没有多个线程, 这大大简化了一般场景.


    既然我们对多线程编程的经典概念有信心, 我们就可以向前迈出另一步, 了解 Delphi 的并行编程库. 它是几年前推出的, 是
    RTL 的一部分 (在所有支持的平台上都有) .

 在上一节中, 我们看到了利用诸如匿名方法等现代语言特性是多么的方便. 这种特性乍一看可能会让开发人员认为它们对现有
    代码没有实际意义. 但 TThread 案例显然证明, 使用匿名方法提供了一个简洁的 / 实用的 / 有效的 / 简单易用的替代方案
    来解决同样的问题.

    利用 Delphi XE7 (2014 年 9 月), 在 Delphi RTL 中添加了一个新的库 (从 VCL 和 FMX 都可以获得) - 并行编程库 (PPL),
    主要集中在 System.Threading 单元中.

    现代计算机 (包括移动设备) 具有多核心的 CPU. 现代软件应用对多线程代码的需求是越来越大. 现代语言特征为多线程编程
    的旧问题提供了一种现代的方法.

        我们将介绍一些关于并行编程库的基本信息, 以及它如何对您的多线程方法产生重大的影响.

    首先, 我们在抽象方面更加进一步 — 我们将关注要执行的任务, 而不再关注执行代码的线程. 如前所述, 特别是在处理 UI时,
    您需要在后台执行一些代码, 并可能与 main / UI 线程进行交互以提交结果. 任务是要在后台执行的代码部分.  你可以在任
    务中包含, 你将要在 TThread 继承类的 Execute 方法中放置的任何指令.

    并行编程库提供了一些数据结构 (类) 来实现这个新的模型, 例如 TTask / TParallel<并行> / TFuture<未来> 类.

    关于 PPL, 您需要理解的另一个关键点是线程池的概念<thread pooling>. 想象一下在短时间内定义和执行一百个任务. 这不
    是一个不可能的数字, 特别是当我们刚刚指出完美的场景所涉及到的小任务, 需要定义和计划执行.  理论上使用 TThread 方
    法, 我们最终会创建 / 执行 100 个线程, 针对 main / UI 线程时不时的同步然后销毁这些线程.  所有这些只是为了在应用
    程序的后台执行一些小的任务.

    一个更加有效的方法, 来处理这种情况涉及到一个非常众所周知的概念, pooling<池>. 与其把资源 (时间, CPU, 内存) 花在
    重复的创建和销毁线程上, 仅仅是为了在短时间内来使用它们, 我们可以考虑维护一个线程池并使用它们来执行任务.

    在我们的示例中, 我们将定义 100 个任务 (在您可以认为是一个队列中), 并使用一些执行程序 (线程) 来消耗任务队列. 基
    本上, 我们正在重用线程实例, 而不是不断地设置它们并将它们销毁. 在数据库世界中, 这已被证明对性能有很大的积极影响,
    因为通过重用连接, 我们跳过了所有延迟时间和连接设置时间 (在高通量场景中大大提高了性能) .

        并行编程库被 RTL / FMX 和 VCL 的其他部分使用. 一般来说, 容易实现并行编程的能力会提高许多领域 (非 UI)
        的性能.


    这种方法增加了适当大小的池的能力, 也根据实际可用的硬件 (CPU 核心) 来进行. 此外,  这还可以防止 CPU 在创建太多线
    程时出现吃不饱. 显然如果队列任务是可以接受的, 那么这些都是有意义的, 换句话说, 您不需要同时定义的任务也同时执行.
    有时确定这一点远远不简单, 但是在一般情况下, 我们正在处理 (UI 交互), 它通常是一个简单的假设.

    从现在开始, 您不再启动线程; 相反, 您要求并行编程库 (PPL) 在辅助线程上运行任务 (无论哪个线程都属于池) .

    让我们仔细看看 TTask 类, PPL 中的超级有用类. 在下一节中, 我们将学习如何使用它的功能.


    在这一点上, 任务定义通过匿名方法应该听起来很自然. 下面的片段显示了通过 PPL 定义一个新任务是多么容易:

          uses
          System.Threading;

          TTask.Run(procedure
          begin
          // Do something
          end
          );

    我们应该回忆一下 TThread.CreateAnonymousThread 类函数, 但要抽象一点. 一旦此任务被分配到 PPL 池的线程中, 匿名方
    法将会被执行.

    没什么魔力 —— 底层元素仍然存在 (线程 / 同步需求等等). 换句话说, 这就是典型的在经典问题上添加语法糖的例子.

    我们的示例可以使用 PPL 元素进行翻译, 如 PPLProject 演示 (包括完整的源代码) 所示. 以下摘录来自 StartButton 的
    OnClick 事件处理程序:

    procedure TMainForm.StartButtonClick(Sender: TObject);
    begin
        if Assigned(FTask) then Exit;

        FTask := TTask.Run( procedure
          var
          LCounter: Integer;
          begin
          (...)
          LCounter := 0;
          while FTask.Status = TTaskStatus.Running do
          begin
          (...)
          Sleep(1000);
          Inc(LCounter);
          // OnProgress equivalent  // 等同于  OnProgress
          TThread.Synchronize(nil,  procedure
          begin
          (...)
          StatusLabel.Text := 'Running, counter = ' + LCounter.ToString;
          end
          );
          end;

          // OnTerminate equivalent
          TThread.Synchronize(nil,  procedure
          begin
          FFinalCounter := LCounter;
          StatusLabel.Text := 'Completed, counter = ' + FFinalCounter.ToString;
          FTask := nil;
          end
          );
          end
          );

        (...)
        StatusLabel.Text := 'Started';
    end;


    在前面的代码中, 请注意 TTask.Run 返回了对_定义任务_的引用 (类型 ITask). 例如, 您可以查询任务状态 (通过同名属性)
    或者对其采取一些操作 (即取消任务 / 等待完成等) .

    这个例子相当于原始程序 (SimpleThreadProject 的演示), 但它是使用新工具 (PPL) 对原始方法的清晰的翻译. 您可以简化
    这个演示, 使其更加面向任务, 更少的线程结构化. 此外,  PPL 在 shoot-and-forget and shoot-and-report 的情况下表现
    最好 (在代码可读性方面). 控件循环情况不太适合与 PPL 方法匹配.

    如前所述, PPL 在多线程编程问题上的作用有点像语法糖. 许多主题与前面看到的相同, 包括同步需求. 在下一节中, 我们将
    学习如何使用 PPL 寻址同步<address synchronization>.


  一旦您开始使用任务 (或我们看到的线程), 您可能将面临同步的需要. 在多个核心之间同时传播代码执行的能力_并不能消除
    对某种同步的偶尔需要. 您可以并行执行某些代码, 但随后, 您可能需要等待某些任务完成后再继续执行. 或者, 正如已经说
    过的, 您可能需要找到一些机制来正确的访问一些共享资源, 如果不加区分地访问, 这些资源将被破坏.


    假设我们有一个任务引用 (ITask), 我们希望 (同步) 等待它的完成, 然后再继续进行.

    正是因为这个目的而提供了 ITask.Wait 方法. 它还接受所需要超时的参数, 使操作很容易进行微调. 下面的代码显示了 ITask
    .wait 方法:

          var
          LTask: ITask;
          begin
          LTask := TTask.Run( procedure
          begin
          Sleep(5000);
          // Do something...
          end
          );
          LTask.Wait();
          // Completed
          end;

    在前面的代码中, 假设我们在 main / UI 线程中运行此代码, 这里的预期行为是, 任务将在后台运行, main / UI 线程将同步
    的等待任务的完成. 显然, 如果任务不是即时的话, 这将会阻塞 主 / UI 线程, 因此这不是实现响应的良好实践.

    下面的代码显示了如何通过使用 Wait 方法的 Timeout 参数来避免_阻塞任务的整个执行时间的 主 / UI 线程, 因此我们可以
    将等待时间分割成多个段, 在以下两个段之间有机会去执行一些代码:

          var
          LTask: ITask;
          LCompleted: Boolean;

          begin
          LTask := TTask.Run( procedure
          begin
          Sleep(5000);
          // Do something...
          end
          );
          LCompleted := LTask.Wait(10);

          while not LCompleted do
          begin
          Log('Waiting, task status: ' + TRttiEnumerationType.GetName<TTaskStatus>(LTask.Status));
          LCompleted := LTask.Wait(500);
          end;
          Log('Completed');
          end;


    在前面的代码中, 我们等待的时间非常短 (10ms), 然后, 如果任务还没有完成, 我们就输入一个具有 500ms 作为超时的等待调
    用循环. 如果任务 (在后台运行) 在 Wait 调用打开时完成, 调用将立即返回 true, 否则, 它将保持 Wait 直到超时 (500ms)
    后结束 (并且返回 False).


    第二个实现版本在等待过程中给了您一些控制, 但它仍然非常同步 (因此会阻塞). 主线程 / UI 线程将无法保持 UI 的活力和
    响应, 因为它正忙于等待任务的完成. 这显然与我们期望实现的目标相去甚远.

    我们引入了一些任务, 以避免在做一些冗长的操作时阻塞 UI.  阻塞<Blocking> 等待_后台完成_UI_的操作_绝对是我们目标的
    相反, 也就是说, 我们希望 UI 线程在后台执行冗长的操作时是可以自由流动的.

        这里最有经验的 Delphi 开发人员可能会争论在 while 循环中添加一个 Application.ProcessMessage 指令.
        特别是在基于消息队列的系统上, 这将会让 UI 处理消息 (包括与绘制相关的消息), 给人的印象是应用程序
        没有被冻结. 然而, 这只是一种安慰剂, 而不是一种实现反应的实际解决方案.


    下面的代码显示了 PPLWaitProject 演示中提供的第三个等待实现:

          var
          LTask: ITask;
          begin
          Log('Button clicked');

          LTask := TTask.Run( procedure
          begin
          Log('Task start');
          Sleep(5000);
          Log('Task end');
          end
          );

          TTask.Run( procedure
          var
          LCompleted: Boolean;
          begin
          LCompleted := LTask.Wait(10);
          while not LCompleted do
          begin
          Log('Waiting, task status: '  + TRttiEnumerationType.GetName<TTaskStatus>(LTask.Status));
          LCompleted := LTask.Wait(500);
          end;
          Log('Completed');
          end
          );
          end;


    从前面的代码中, 您可以发现在第二个版本中使用了相同的代码, 但这一次它被包在 TTask.run 的调用中. 我们正在将同步等
    待移动到另一个任务中, 因此我们将有一个任务执行一些长操作, 另一个任务等待第一个任务完成.

    这段代码的执行将立即返回, 这意味着调用 (主线程 / UI 线程)  不会被工作任务的启动 (和执行) 或者等待任务的启动 (和
    执行) 所阻止.

        在演示中, 我使用了 CodeSite 进行日志记录, 您可以通过查看日志查看器和检查 Thread 列立即注意到差异.
        在上一个示例中, 您将注意到所涉及的三个不同的线程 ID 主线程 / UI 线程 / 执行工作任务的线程和执行
        等待任务的线程.


    作为最后的说明, 请考虑等待任务不应该是您的第一选择. 大多数情况下, 您可以简单地将一段代码 (如果需要的话, 可以适当
    地同步) 作为任务定义的最后一部分, 以便发出任务已经完成的信号. 下面的代码实现了一个典型的_shoot-and-report 任务:

    TTask.Run( procedure
          begin
          Sleep(5000);
          // Do something ...
          TThread.Synchronize(nil, procedure
          begin
          ShowMessage('Completed');
          end
          );
          end
          );


    正如您在前面的代码中所看到的, 我们希望在任务完成后运行的代码被写入任务本身的尾部. 通过这种方式, 我们将代码的执行
    委托给任务本身, 在适当的时刻, 并有机会选择是否与主线程同步.

    然而, 等待策略可能非常方便, 特别是在涉及多个 (可能嵌套的) 任务的情况下. 在下一节中, 我们将看到如何一次处理多个任
    务.



    考虑一个场景, 您已经定义并启动了多个任务. 例如, 假设您正在从 Web 上下载多个文件, 以便处理它们并为用户构建一些
    UI 元素.

    假设您为每个文件启动了一个任务, 并将引用存储在数组中 (TArray<ITask> 类型). 您可以决定等待所有下载文件的完成,
    然后继续处理它们, 或者一旦其中一个文件可用, 就开始处理这个文件.

    处理这两种情况有两种方法: 

        1. TTask.WaitForAll 方法:     它将接受 ITask 引用数组作为参数, 并将同步等待直到所有它们全部完成.
          第二个可选参数可以用于设置等待超时,  所有考虑都在上一节中关于同步
          等待或者_异步站立<asynchronously standing> 的考虑.

        2. TTask.WaitForAny 方法:     它有相同的参数, 但一旦其中一个文件完成, 它就会返回.


        前面两种方法再次大大简化了多任务代码的编写, 让您专注于应用程序代码, 而不是处理任务同步的细节. 

          运行多个任务并有机会检查操作的全局状态的另一个选项是: 使用 TParallel.Join 函数. 它是一个
          异步调用, 但它返回了一个 ITask 的引用, 您可以使用它来检查其 Status 属性.


    现在我们已经获得了对任务及其同步机制的一些信心, 我们可以引入另一个特性, 用这些构建块构建. 如果我们有能力定义一
    个任务, 让它在后台运行, 并可能等待它的完成, 那么如何使用这些功能, 来并行计算_我们已经知道在未来某个时刻_我们需
    要的一些值呢?

    这里的关键是我们知道自个需要这个值, 我们知道如何计算它. 我们只需要一个简化, 如果我们能提前计算, 那就好了; 否则,
    我们需要代码等待, 直到值实际可用. 下一节将向我们展示如何通过 PPL 的功能轻松实现这一点.



    最后但并非最不重要的是, 我们可以在 PPL 中找到好东西, 就是 future.  它可以被定义为我们已经知道我们将来需要的值.
    关键点是获得 (计算 / 构建 / 检索等) 一个在计算资源 (CPU / 时间 / 网络等) 方面昂贵的值, 并且我们不想用这个负担
    影响 主线程 / UI 线程.

    下面的代码显示了 future 的实现:

    var
        LFuture: IFuture<Integer>;
    begin
        LFuture := TTask.Future<Integer>( function: Integer
          begin
          Sleep(5000);
          // Do something...
          Result := 42;
          end
          );
        ShowMessage(LFuture.Value.ToString);
    end;


    前面的代码示例显示了如何定义 future. 基本上, 我们提供了一个生成器函数, 它能够返回我们的值, 并将此函数包在任务
    中.

        future 的实现依赖于另一个现代语言特性 — 泛型. 我们试图处理某个类型的值 (例如: X), 因此我们需要提供
        一个返回的值 (X 类型) 的函数. 同样, 我们需要将值 (一旦可用) 保存在 X 类型的变量中, 以此类推.  我建
        议你至少让自己熟悉泛型的基本概念 (参考马可·坎图的书) .


    如果您将前面的代码放入按钮的 OnClick 事件处理程序中, 您将注意到应用程序将冻结 (5秒). 这是因为我们定义了 future
    (这是眼前的), 然后我们要求 future 的值. 此时, future 将尝试检索这个值, 并且在这个值可用之前不会返回这个值.

    为了有意义, 我们需要在它的值之前来定义 future 的方式. 这样, 一旦定义了 future, future 就将会在后台运行 (在一个
    专用的任务中), 并将在我们要求之前就建立我们将来需要的值.

    记住, 一个 future 将试图建立它的值, 一旦它被定义了 (一个任务是为目的开始的).  当它的值在某些表达式或者指令中使
    用时, 如果它已经可用了, 则将没有等待时间. 否则, 执行流程将被搁置, 直到这个值可用为止.

    这是一个很大的简化, 因为在这一点上, 你可以忽略掉所有的同步问题, 只需要使用值  (返回到顺序执行模型 - 对人类来说
    是如此的自然), 并有两个保证:

        1. 如果可以在我们试图使用它的点之前_预先构建这个值, 则不会发生等待.

        2. 如果值仍在生成中, 等待将会自动的介入.


    在查看单个 future 场景时很难获得优势, 但是如果您有多个 future 的话 (定义在单个或者多个点中),  您将会看到代码将
    会自动的并行化. 定义三个 future 将导致三个任务被设置, 以便同时构建三个值.

    想象一下, 例如, 一个业务应用程序启动后, 加载一些远程资源在完成 future 时可能会有很大的优势. 即使是在单一的功能
    上, 您也可以编写代码, 以便能够在用户进行一些数据输入或浏览一些初步数据时可以提前满足某些需求, 并在后台运行相关
    的代码.

    在响应方面, 请记住, 您仍然可能陷入阻塞等待<blocking waits>, 所以考虑将 future 打包成任务, 以便从 主线程 / UI线
    程的角度保持整个事情的平滑.

    在本节中, 我们了解了为什么响应是重要的, 以及用于实现响应的技术. 我们注意到最常见的缺陷, 例如在 UI 线程中执行长
    任务时冻结用户的界面 (UI), 以及缺少 UI 线程和辅助线程之间的适当同步. 在处理现代应用程序开发中的常见任务时,  不
    要错过内置并行编程库的优点.
----------------------------------------------


美国国务卿蓬佩奥回答大学生提问时说,“我曾担任美国中央情报局(CIA)的局长。我们撒谎、我们欺骗、我们偷窃。我们还有一门课程专门来教这些。这才是美国不断探索进取的荣耀
作者:
男 pcplayer (pcplayer) ★☆☆☆☆ -
普通会员
2021/1/16 21:24:28
6楼: 楼上你贴的那些,是使用线程的基本规则。不管是 VCL 还是 FMX。

其实使用线程也简单,注意几点就行:
1. 留意你的代码,哪些是主线程在执行,哪些是线程在执行;
2. 留意几个线程都要访问的变量,避免同时访问,办法:加锁
3. 线程执行的代码,中途如果某几行会影响到 UI 界面比如会更改界面显示,则加上对主线程的同步。
----------------------------------------------
-
作者:
男 akai1203 (w-dins) ★☆☆☆☆ -
普通会员
2021/1/16 22:18:17
7楼: 感谢各位回复,为了可以流畅地运行跑马灯文字效果 ,我把之前放在ttimer里的代码移到了线程中.我发现无论在线程中无论是使用tthread.Queue 还是 tthread.Synchronize 也都是会造成跑马灯卡顿,但是比起用ttimer,要好那么一点,但还是能看到明显的卡顿.所以我就直接在线程中更新,我把主界面的label控件传到线程中,这时,在每次运行时 会 "duplication not allowed"这样的错误,但是基本不卡顿了,哪位给个解决方案或思路 ?

  TThread.CreateAnonymousThread(
    procedure()
    begin
      while 1 > 0 do
      begin
        cs.Enter;
        try
          DoLoadCalls;
        //  tthread.Queue(tthread.CurrentThread, DoLoadCalls);
        //  tthread.Synchronize(tthread.CurrentThread, DoLoadCalls);
          sleep(1000);
        finally
          cs.Leave;
        end;
      end;
    end).Start;


function TDisplayHelper.DoLoadCalls: Boolean;
 
begin 
   // FNumber.Text := 'test' //这里的FNumber是主界面的label控件,如果不注释第一次会出错,之后就不会了 
end;
----------------------------------------------
-把学习当信仰
作者:
男 pcplayer (pcplayer) ★☆☆☆☆ -
普通会员
2021/1/18 16:46:41
8楼: 楼上,我在你的楼上已经说过了,更新 UI 的代码不要放到线程里面去。

如果一定要在线程里面更新UI,则把更新UI的代码放入同步主线程里面。

至于你的代码,你先不要管 TCP 读写的阻塞,你写一个 Sleep(5000) 停5秒来代替 TCP 的阻塞,然后看看你的动画会不会停。如果会停,那肯定是你的代码有问题,你把代码贴出来。
----------------------------------------------
-
作者:
男 akai1203 (w-dins) ★☆☆☆☆ -
普通会员
2021/1/21 18:36:09
9楼: 感谢楼上,我是在FMX平台下使用,感觉是这个TThread有问题,后来换用FMXUI里的TAsync来弄,就不卡了
----------------------------------------------
-把学习当信仰
作者:
男 pcplayer (pcplayer) ★☆☆☆☆ -
普通会员
2021/1/22 12:28:45
10楼: 楼上,你这样仅仅是让问题消杀,但并没有解决问题:为啥会卡,为啥又不会卡了。

不要怀疑 TThread 有问题。要怀疑自己哪里没做对。
----------------------------------------------
-
作者:
男 pcplayer (pcplayer) ★☆☆☆☆ -
普通会员
2021/1/22 18:10:36
11楼: 楼主,关于 FireMonkey 的动画和线程,我写了个例子程序。看我的博客文章。有程序源码下载。

https://blog.csdn.net/pcplayer/article/details/112993454
----------------------------------------------
-
信息
登陆以后才能回复
Copyright © 2CCC.Com 盒子论坛 v3.0.1 版权所有 页面执行152.3438毫秒 RSS