DELPHI盒子
!实时搜索: 盒子论坛 | 注册用户 | 修改信息 | 退出
检举帖 | 全文检索 | 关闭广告 | 捐赠
技术论坛
 用户名
 密  码
自动登陆(30天有效)
忘了密码
≡技术区≡
DELPHI技术
lazarus/fpc/Free Pascal
移动应用开发
Web应用开发
数据库专区
报表专区
网络通讯
开源项目
论坛精华贴
≡发布区≡
发布代码
发布控件
文档资料
经典工具
≡事务区≡
网站意见
盒子之家
招聘应聘
信息交换
论坛信息
最新加入: tkzcol
今日帖子: 5
在线用户: 13
导航: 论坛 -> DELPHI技术 斑竹:liumazi,sephil  
作者:
男 magiewang (magiewang) ▲▲▲△△ -
普通会员
2021/8/23 17:16:53
标题:
请教关于 TTask 与 TThread 的问题。 浏览:2991
加入我的收藏
楼主: 现在DELPHI的资料太少了,尤其XE后新带来的一个特性资料更少,最
近在改老程,一直都用的多线程TThread,但发现TTask似乎更好,看
了一个文章介绍,TThread是单核时代的产物,多个线程按一定顺序
轮流占用CPU时间片,而TTask是多核时代的产物,多个TASKS占用各自
的CPU,相对独立运行,所以效率更高。

想请教下上面的说法是否完全正确?因为这是一个非官方资料里写的,
另外我如果想把TThread用TTask取代应该如何改?TThread下包含了非
常多的函数代码,在TTask中如何定义他们?

先谢过各位了!
----------------------------------------------
-
作者:
男 bahamut8348 (leonna) ★☆☆☆☆ -
普通会员
2021/8/23 17:39:50
1楼: 使用:
ttask.create(procedure cb()
begin
  user code.
end);

ttask的底层还是用tthread实现的,无非是有线程池做调度而已,至于线程池嘛,不一定是最好的,具体要看你的实际应用场景,并不是所有场景都需要线程池的。
----------------------------------------------
--
作者:
男 dmzn (dmzn) ★☆☆☆☆ -
盒子活跃会员
2021/8/24 9:56:06
2楼: TThread有用的函数只有一个: Execute
我现在的实现方法是: 创建几个线程 和 一个匿名函数列表,将需要在线程里运行的代码放入列表中,然后让随机挑选一个线程来执行.
----------------------------------------------
生活愉快.
作者:
男 pcplayer (pcplayer) ★☆☆☆☆ -
普通会员
2021/8/24 10:52:49
3楼: 【TThread是单核时代的产物,多个线程按一定顺序轮流占用CPU时间片,而TTask是多核时代的产物,多个TASKS占用各自的CPU,相对独立运行,所以效率更高。】 --- 这个是哪里看来的?我不清楚居然还有这样的区别。

我觉得 TTask 就是 TThread 的一个语法糖,背后都一样。

我使用 TTask 我觉得就是为了代码顺序方便。当主线程和线程来回切换的时候,代码可以一行一行往下写,无需考虑线程执行的那部分代码要放到 TThread.Execute 里面单独成为一个代码块,读代码就要来回看。因此我觉得它就是个语法糖。
----------------------------------------------
-
作者:
男 wenyue0811 (wenyue0811) ★☆☆☆☆ -
普通会员
2021/8/24 12:19:44
4楼: 你可以找网上的一本书来看,  好像 论坛中有这本事, 叫 Delphi Cookbook 

    第 5 章 多线程的不同面

    在本章中, 我们将介绍以下主题:

        * 使用 TMonitor 同步共享资源.

        * 使用线程安全队列与主线程进行对话.

        * 使用 TEvent 来同步多个线程.

        * 在 2D 图形上显示测量值, 如示波器.

        * 使用 tasks 来让客户更轻松.

        * 利用 futures 监控事物.

        * 使用 Parallel for 来并行化.



P343


    使用 Tasks 来让客户更轻松

    由于 Delphi XE7 开发人员可以使用并行编程库 (PPL). 什么是 PPL 呢? 其是 Delphi RTL
    的一部分, 它提供了多线程 (或者并行) 编程功能.

    PPL 可以用于 Delphi 支持的所有平台, 并且为运行任务, 加入任务, 等待任务组来处理等
    提供了许多的高级功能. PPL 不仅是创建线程的一种不同方式, 也是管理线程的一种不同的
    方式. 为什么?

    因为要管理所有的这些功能 (任务_tasks, futures, 并行_parallel for, 连接_joining等),
    其有一个 线程池_thread pool 可以自动的进行自我调优 (是基于 CPU 上的负载), 所以你
    不必为此而关心创建或者管理线程.

    好消息是 PPL 的使用非常的简单, 不需要对应用程序进行重大的更改. 你可以通过_在应用
    程序或者应用程序中包含 System.Threading 来使用这个库. 此单元由几个功能组成, 这些
    功能可以包含在新项目中和现有的项目中.

    所以问题是: "如何以及何时使用 PPL 呢?", "嗯_在所有需要使用线程的情况下, 你应该考
    虑一下 Task. 这并不意味着你将不再创建线程, 但是_在许多的情况下, 你最终将使用某种
    任务_task 而不是使用普通线程.

    在这个方法中, 我们将开发一个可重用的异步库, 以满足一个经常性的需求: 一是启动后台
    操作, 二是在主线程中获得后台进程是否成功结束的通知.

    使用普通的 PPL, 一个非常重复的代码类似于以下:

        procedure TMainForm.btnITaskClick(Sender: TObject);
        var LTask: ITask;
        begin
          LTask := TTask.Run( procedure
          var LResult: Integer;
          begin
          Sleep(1000);       // 在这里做一些有用的事情…

          LResult := Random(100);    // 某种 "结果_result"

          // 在主线程中执行 Queue_队列
          TThread.Queue(nil, procedure
          begin
          // TaskEnd 在 用户界面 的线程中被调用.
          TaskEnd(LResult);
          end);
          end);

        end;


       现在非常简单, 但是当你有许多的任务正在运行, 并且必须要处理异常的时候, 事情就会变
    得更加复杂了. 因此, 增加一些可用性层次是有用的. 这就是 Async.Run<T>.

    对 Async.Run<T> 方法的完整的调用是由三个匿名方法组成:

        * 后台任务:     Background task, 这是一个返回了某种数据的函数. 它使用
          PPL task 在后台线程中运行.

        * 成功回调:     success callback, 这是一个获取后台任务结果的过程.  它
          在主用户界面的线程中运行.

        * 错误回调:     error callback, 这是一个获取后台任务引发的异常  (如果
          有的话) 的过程. 它在主用户界面的线程中运行.


    这个小型的库_library 可以通过以下方式来使用:

        Async.Run<String>(function: String
          begin
          // 这是后台任务匿名方法. 在后台线程中运行, 其结果将传递
          // 给成功回调. 在本例中, 结果是一个字符串.
          end,

          procedure(const Value: String)
          begin
          // 这是成功回调. 在用户界面线程中运行, 并且获取后台任务
          // 匿名方法的结果.
          end,

          procedure(const Ex: Exception)
          begin
          // 这是错误回调. 在用户界面线程中运行, 仅当后台任务匿名
          // 方法引发异常时才会调用.
          end
          );


    在本例中, 后台函数返回的数据是一个字符串, 但由于其是 Async.Run<T> 的一个通用的方
    法, 所以你可以将类型更改为你想要的任何的类型.



    如何开始...
        //////////
        在这个配方中, 我们将为它创建异步库和一个测试台程序. 我们的目标是使用一些用例
        来练习库, 看看它是如何工作的.



    如何工作的...
        //////////
        打开项目 AsyncTaskSample.dproj 项目, 让我们来讨论 AsyncTask.pas 单元.

        以下是对本单元的一些评论:

          unit AsyncTask;

          interface

          uses System.SysUtils, System.Threading; // The PPL unit

          type
          // 后台任务
          TAsyncBackgroundTask<T> = reference to function: T;

          // 成功回调
          TAsyncSuccessCallback<T> = reference to procedure(const TaskResult: T);

          // 错误回调
          TAsyncErrorCallback = reference to procedure(const E: Exception);

          // 如果用户不提供默认的错误回调的话.
          TAsyncDefaultErrorCallback = reference to procedure(const E: Exception; const ExptAddress: Pointer);

          // 主要类
          Async = class sealed
          public
          class function Run<T>(Task:    TAsyncBackgroundTask<T>;
          Success: TAsyncSuccessCallback<T>;
          Error:   TAsyncErrorCallback = nil): ITask;
          end;

          var
          // 默认的 错误回调. 它是一个公共变量, 因此程序员可以重写默认行为
          DefaultTaskErrorHandler: TAsyncDefaultErrorCallback = nil;

          implementation

          uses  System.Classes;

          class function Async.Run<T>(Task:    TAsyncBackgroundTask<T>;
          Success: TAsyncSuccessCallback<T>;
          Error:   TAsyncErrorCallback): ITask;
          var
          LRes: T;
          begin
          // 后台任务从这里开始
          Result := TTask.Run(procedure
          var Ex: Pointer; ExceptionAddress: Pointer;
          begin
          Ex := nil;
          try
          LRes := Task(); //运行实际的任务

          if Assigned(Success) then
          begin
          // 调用用于_传递结果_的 "成功回调" 函数
          TThread.Queue(nil, procedure
          begin
          Success(LRes);
          end);
          end;
          except
          // 让我们延长异常对象的生命周期
          Ex := AcquireExceptionObject;
          ExceptionAddress := ExceptAddr;

          // 在主线程的 queue_队列上调用 错误回调.
          TThread.Queue(nil,  procedure
          var LCurrException: Exception;
          begin
          LCurrException := Exception(Ex);
          try
          if Assigned(Error) then
          Error(LCurrException) //调用错误回调
          else
          DefaultTaskErrorHandler(LCurrException, ExceptionAddress);
          finally
          // 释放异常对象. 这是必要的, 因为我们
          // 将异常对象的自然生命延长到了异常块
          // 之外了.
          FreeAndNil(LCurrException);
          end;
          end); // TThread.Queue(nil,
          end; //except
          end); //task.run
          end;

          initialization

          //这是默认的错误回调
          DefaultTaskErrorHandler := procedure(const E: Exception; const ExceptionAddress: Pointer)
          begin
          ShowException(E, ExceptionAddress);
          end;
          end.


        现在我们知道了 Async.Run<T> 是如何实现的, 让我们看看如何使用它. 打开主窗体并
        且检查每一个按钮下的代码. 让我们从按钮 btnSimple 开始:

          procedure TMainForm.btnSimpleClick(Sender: TObject);
          begin
          Async.Run<Integer>(
          function: Integer // 在后台长时间的运行.
          begin
          Sleep(2000);
          Result := Random(100);
          end,

          procedure(const Value: Integer) // 在 UI 中显示结果.
          begin
          // 将结果写到窗体中的备忘录里.
          Log('RESULT: ' + Value.ToString);
          end);

          end;


        长操作在匿名方法中是作为返回整数的函数执行的. 当函数结束时, 它的返回值被传递
        给了另一个匿名方法, 这是一个过程, 并且在用户界面的线程中运行, 以便它可以与用
        户交互. 如果运行这个程序并且单击此按钮, 则可以验证在长操作 (实际上是一个 Sleep
        (2000) 2 秒钟的调用) 运行时用户界面是否没有冻结.


        第二个按钮名为 btnWithException, 显示如何处理后台线程中可能引发的异常:

          procedure TMainForm.btnWithExceptionClick(Sender: TObject);
          begin
          Async.Run<String>(
          function: String
          begin
          raise Exception.Create('This is an error message');
          end,

          procedure(const Value: String)
          begin
          // 从没有调用过.
          end,

          procedure(const Ex: Exception)
          begin
          Log('Exception: ' + sLineBreak + Ex.Message);
          end);
          end;


        很简单, 不是吗? 如果后台出现了问题, 则将相关的异常对象传递给错误回调. 请注意,
        错误块不是标准的 Delphi 块, 而是一个获取异常对象的匿名方法, 例如 raise; 不允
        许调用以重新激活当前异常.

        名为 btnExceptionDef 的默认异常按钮显示了库处理在后台引发的异常的能力,  即使
        程序员没有直接处理异常或者忘记处理异常了:

          procedure TMainForm.btnExceptionDefClick(Sender: TObject);
          begin
          Async.Run<String>(
          function: String
          begin
          raise Exception.Create('Handled by the default Exception handler');
          end,

          procedure(const Value: String)
          begin
          // never called
          end);
          end;


        单击此按钮, 你将看到 Delphi 的标准异常消息. 在某些情况下, 这就足够了, 但如果
        需要一些自定义的处理, 只需要传递特定回调或者重写默认的行为, 将另一个默认处理
        程序分配给全局变量 DefaultTaskErrorHandler.

        最后一个按钮做了一些实际有用的事情: 它从 REST 服务器中获取当前时间. 这个按钮
        称之为 btnRESTRequest, 下面是其代码:

          procedure TMainForm.btnRESTRequestClick(Sender: TObject);
          begin
          Async.Run<String>(
          function: String
          var
          LHTTP: THTTPClient;
          LResp: IHTTPResponse;
          begin
          LHTTP := THTTPClient.Create;

          try
          LResp := LHTTP.Get('http://www.timeapi.org/utc/now&#39;);
          if LResp.StatusCode = 200 then
          begin
          Result := LResp.ContentAsString(TEncoding.UTF8)
          end
          else
          begin
          raise Exception.CreateFmt('Cannot get time. HTTP %d - %s',
          [LResp.StatusCode, LResp.StatusText]);
          end;
          finally
          LHTTP.Free;
          end;
          end,

          procedure(const DateAndTime: String)
          begin
          Log('Current Date Time: ' + DateAndTime);
          end,

          procedure(const Ex: Exception)
          begin
          Log('Exception: ' + sLineBreak + Ex.Message);
          end);
          end;


        在这一点上, 代码应该是清晰的. 在后台任务中, 执行实际的 REST 调用, 如果服务器
        以 200 OK HTTP 状态应答的话, 就把响应的正文传递给成功回调, 否则_就会引发异常,
        说明发生了错误.



    还有更多...
        //////////
        PPL 大大简化了多线程编程. 然而, 最大的优势是线程池, 它完成了创建, 销毁和重用
        后台线程的复杂工作. 因此, 请不要将 PPL 视为创建线程的不同方式, 它_是一种强大
        的机制, 可以正确的处理多个线程, 而不会使 CPU 饱和. 请记住, 当你要求 PPL 启动
        任务时, 任务可能不会立即的启动. 这是因为线程池可能会决定, 将你的任务放入等待
        队列中并且尽快的启动, 但不是现在就启动.

        重要的是, 这实际上不是一个问题, 而是一个特性, 否则, 你将很容易的使 CPU  资源
        饱和. 根据我自己的经验, 在复杂的情况下, 你不能在需要时简单地启动线程, 但你必
        须通知线程管理器你需要一个线程, 然后它可以尽快启动它. 在 PPL 出现之前, Delphi
        RTL 中没有这个线程管理器, 因此我非常欣赏 PPL 线程池.

        以下是一些有用的链接, 可以帮助你了解 PPL 的概念和类:

          { http://docwiki.embarcadero.com/RADStudio/en/Using_the_Parallel_Programming_Lib ;}

          { http://docwiki.embarcadero.com/RADStudio/en/Using_TTask_from_the_Parallel_Pro ;}

          { http://www.danieleteti.it/using-dynamic-arrays-and-parallelprogramming-library-part-1/ ;}


    //


你可以参考一下:
----------------------------------------------


美国国务卿蓬佩奥回答大学生提问时说,“我曾担任美国中央情报局(CIA)的局长。我们撒谎、我们欺骗、我们偷窃。我们还有一门课程专门来教这些。这才是美国不断探索进取的荣耀
作者:
男 magiewang (magiewang) ▲▲▲△△ -
普通会员
2021/8/24 17:36:14
5楼: 非常感谢 wenyue0811及楼上各位。
现在Delphi好书太少了,已经下载了这本书,谢谢。

因为没有对TTask吃透,所以到现在除了简单的代码用用,还没敢正式用。
----------------------------------------------
-
作者:
男 wr960204 (武稀松) ★☆☆☆☆ -
盒子活跃会员
2021/8/26 10:37:12
6楼: Thread是轮询占用CPU,Task是工作在Thread基础上的,不指定工作的CPU核心操作系统会自动调度Thread工作在不同的处理器核心上。Thread也可以明确指定工作在哪些处理器核心上。
Task最终也是工作在Thread之上的,Task比Thread的好处就是封装得更友好,使用更方便而已
----------------------------------------------
武稀松http://www.raysoftware.cn
作者:
男 keymark (嬲) ▲▲▲△△ -
普通会员
2021/8/26 11:23:26
7楼: mark 
4楼
----------------------------------------------
[alias]  co = clone --recurse-submodules  up = submodule update --init --recursiveupd = pullinfo = statusrest = reset --hard懒鬼提速https://www.cctry.com/>http://qalculate.github.io/downloads.htmlhttps://www.cctry.com/
作者:
男 roadrunner (roadrunner) ★☆☆☆☆ -
盒子活跃会员
2021/8/26 14:31:05
8楼: 最简单直观的理解,TTASK创建执行后,要进入一个排队,队伍的宽度等于线程池的线程数量,默认就是CPU的虚拟核心数量,这样的好处就是无限多的TTASK只会被有限多的线程执行,防止无节制创建新线程,降低线程调度消耗。

而TThread则是立即创建,立即执行,所有被执行的TThread都共同竞争CPU资源,在TThread数量很多的情况下,创建和调度的消耗甚至会超过工作负荷很多,TTASK就是为了解决这个缺点而诞生。

而TTASK的优点换来的代价就是,它们被执行的机会不是公平的,而是根据被创建的次序排队执行,这也使得一些编写不好的TTASK,它们故意挂起自己的时候,会卡住后面排队的TTASK。所以,编写TTASK尽量只执行纯计算任务,原则就是要么尽量不要卡住别人,要么不要介意被别人卡住。

当你的并发代码,担心高延迟,不愿意被别人卡住时,用TThread, 不要用TTASK
----------------------------------------------
-
作者:
男 wiseinfo (wisienfo) ★☆☆☆☆ -
普通会员
2021/8/26 16:40:41
9楼: 中國人創造的QWorker顯著減少學習成本提高生產力
----------------------------------------------
-
作者:
男 luckyrandom (luckyrandom) ★☆☆☆☆ -
普通会员
2021/8/27 12:38:56
10楼: 测试了,TThread能使用多核心CPU
我创建50个线程,4个CPU核心使用率都是满负载

AMD A8-7650K Radeon R7, 10 Compute
Win 10 X64,App 32位
----------------------------------------------
SQL SERVER DBA QQ:315054403 曾经的Delphier  缘在上海
作者:
男 magiewang (magiewang) ▲▲▲△△ -
普通会员
2021/8/28 17:57:05
11楼: 多谢各位的解答,感谢!
看来没必要重写TThread代码了,原本以为TTask会提高
运行效率,虽然测试时发现差不多。
----------------------------------------------
-
信息
登陆以后才能回复
Copyright © 2CCC.Com 盒子论坛 v3.0.1 版权所有 页面执行201.1719毫秒 RSS