【问题标题】:multithreading in another unit in DelphiDelphi的另一个单元中的多线程
【发布时间】:2021-07-13 16:54:19
【问题描述】:

我在一个名为 Execution 的单元中有一个数字运算应用程序,它是从主窗体调用的。通常 Execution.pas 中的代码需要连续运行 10-15 次,我正在寻找一种应用多线程的有效方法,以便执行单元上的计算并行运行。简化版代码如下:

其中有一个 Button1 的主窗体:

unit MainForm;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, Execution;

type
  TMainForm1 = class(TForm)
    Button1: TButton;
    procedure Button1Click(Sender: TObject);
  end;

var
  MainForm1: TMainForm1;

implementation

{$R *.dfm}

procedure TMainForm1.Button1Click(Sender: TObject);
var
  ExecutionThread: TThread;
  run_ID: integer;
begin
    for run_ID := 0 to 2 do
    begin
        ExecutionThread := TThread.CreateAnonymousThread(
        procedure
        begin
            Execution.CalculateSum;
        end);
        ExecutionThread.FreeOnTerminate := true;
        ExecutionThread.Start;
    end;
end;

end.

Execution.pas 单位:

unit Execution;

interface
uses System.SysUtils, Vcl.Dialogs;

  procedure CalculateSum;
  procedure IncrementSum(var Sum: integer);

implementation

const
  NoOfTimes = 100;
var
  Sum: integer;

procedure CalculateSum;
var
  i: integer;
begin
    Sum := 0;
    for i := 0 to Pred(NoofTimes) do
    begin
        IncrementSum(Sum);
    end;
    ShowMessage('Sum = ' + IntToStr(Sum));
end;

procedure IncrementSum(var Sum: integer);
begin
    Inc(Sum);
    Sleep(10);
end;

end.

如果我执行简化的代码,我会收到三个总和接近 300 的消息,但不是预期的 300(285、287、289),因为所有线程同时更改相同的全局变量 Sum 并且有时增量会重叠。

有没有一种简单的方法可以为每个线程分配不同的内存,以便我得到三条总和为 100 的消息?还是我必须自己硬编码,在每个线程中分配不同的执行变量?

请注意,我拥有的原始应用程序包含数千个变量和记录,因此我正在寻找一种快速的方法来执行此操作而无需显着更改代码(当然,如果有的话)。

提前谢谢你。

【问题讨论】:

  • 是的,您有责任确保线程不会同时对共享资源进行操作。有很多方法可以做到这一点,为每个线程硬编码单独的全局变量是一种方法,尽管不是一种非常优雅的方法。理想情况下,您会将所需的操作封装到一个类中,该类管理并包含所需的状态,并且您可以创建任意数量的实例。
  • 遗憾的是,如果您的应用程序写得像您的示例一样糟糕,那么就没有简单的方法了。我已经为一个大型的、主要是非 OO 遗留应用程序(100% 全局变量和数百个单元的意大利面条)完成了此操作,由此产生的 TThread 后代需要封装 300 多个以前的全局变量和数组,并且需要重写大约 10,000 行以前的全局代码到新类中。花了几个月的时间。这就是为什么开发人员通常会对写得不好的代码有强烈的反应——因为它会弄得一团糟,让你别无选择,只能做大量的工作来清理。
  • 因此,最简单的方法是为 Execution.pas 的所有计算创建一个类,对吗?我希望有一种更简单的方法来解决我的问题(老实说,希望不高)
  • 您已将代码放置在单元中,但单元不是成为工作对象的类。从 TThread 创建一个类,将方法和变量放入该类,创建该类的实例,调用 execute。如果要传入参数,请覆盖 create。并且永远不要更新 GUI 或从线程内部调用 ShowMessage。如果您想要结果,请在终止线程之前阅读它,或者在 GUI 中添加终止时的回调,以便它可以显示消息或获取结果。
  • 不要从后台线程调用 ShowMessage(或任何与 UI 交互的东西)!

标签: multithreading delphi


【解决方案1】:

不要在没有同步的情况下从多个线程写入同一个变量,否则可能会损坏该变量的数据。

所以不要在你的线程中调用IncrementSum(Sum)Synchronize(IncrementSum(Sum))

这会强制 IncrementSum 过程在主线程范围内执行,从而防止多个工作线程同时修改该变量,从而导致潜在的数据损坏。

此外,您可能希望避免在每个工作线程的每个循环周期中更新此全局变量。如果您有 10 个工作线程都在等待全局线程更新此全局变量,那么它们最终可能会花费大部分时间等待主线程处理它们的 Synchronize 调用。

例如,也许您应该考虑每 10 个左右周期更新一次这个全局变量。你去和更新全局变量之间的周期数取决于每个周期的完成速度。更快的循环完成更多的循环在更新全局变量之前等待更多的循环,从而减少Synchronize调用的数量,从而减少主线程的负载。

【讨论】:

  • 但是 OP 不希望单独的线程对同一个变量进行操作。他们需要线程对自己的变量进行操作,问题是如何做到这一点。
  • OP 还想要代码重构最少的解决方案。使用Synchronize 调用可以实现这一点,尽管它不是最好的解决方案。其他解决方案是放弃使用匿名线程,而是派生出自己的 TThread 类,该类将包含自己的用于计数周期的字段。但 OP 也表示,他的真实项目有许多全局变量,目前所有这些变量都由他的工作线程访问。这意味着他的 TThread 类需要有多个字段,并且他必须在代码中进行多次更改才能使用这些字段而不是全局变量。
  • 我需要的是 J.. 指出的。一种在不同线程中具有不同变量的简单方法。在我的情况下,同步不起作用。我相信 J.. 在上面的 cmets 中回答有效,即声明单独的变量或使用类。我已经开始考虑班级选项了。非常感谢你们!赞赏
  • 是的。带有代码和数据的类 - 代码方法、'globals'、(yuk)、ints/strings 来保存错误/成功数据。创建实例,最好将它们排队到线程池中。忘记应用程序级或主窗体全局变量:正如其他人所评论的那样,这是一个绝望的设计:(您不需要线程类来保存任何状态数据 - 只需从 TObject 下降。您可以轻松地测试该类而无需任何通过简单地在主窗体消息处理程序中实例化一个线程并调用“工作”方法来增加线程。
【解决方案2】:

有没有一种简单的方法可以为每个线程分配不同的内存,所以 我收到三条总和为 100 的消息?

有一种非常简单的方法可以为每个线程分配不同的内存并使用相同的声明:使用 threadvar 关键字。

这里是修改的代码:

unit Execution;

interface

uses
  System.SysUtils, Vcl.Dialogs;

procedure CalculateSum;
procedure IncrementSum(var Sum: integer);

implementation

const
  NoOfTimes = 100;
threadvar                // <==== ONLY CHANGE IS HERE
  Sum: integer;

procedure CalculateSum;
var
    i: integer;
begin
    Sum := 0;
    for i := 0 to Pred(NoofTimes) do
    begin
        IncrementSum(Sum);
    end;
    ShowMessage('Sum = ' + IntToStr(Sum));
end;

procedure IncrementSum(var Sum: integer);
begin
    Inc(Sum);
    Sleep(10);
end;

end.

【讨论】:

  • “简单”,但是全局变量令人头疼……threadvar 有点像醋酸铅。起初它尝起来很甜,但过了一段时间你开始感觉到有毒。小心使用。
  • 是的,这不是解决这个问题的正确方法。你想要一个比线程变量更多的局部范围的变量。
  • @DavidHeffernan OP 不想重写太多代码。他要求一种简单的方法来为他的 当前代码中的每个线程分配不同的内存,而threadvar 是最简单的方法。他说:“因此,最简单的方法是为 Execution.pas 的所有计算创建一个类,对吗?我希望有一种更简单的方法来解决我的情况(但说实话,没有寄予厚望) ”。他知道他没有做过最好的设计,以及下次如何才能做得更好。我给了他当前代码的快速简便的解决方案。
  • @fpiette 如果 OP 想要走捷径,那是他们的决定,但这里的答案确实应该显示最佳实践。坦率地说,使用多线程走捷径总是会失败。通常这是一种虚假的经济。
  • @J... 我认为你是对的。当我感觉 100% 准备好时,我会做一些背景阅读并发布问题。非常感谢您的支持!
【解决方案3】:

你有没有想过在System.SyncObjs中使用TCriticalSection

一次只有一个线程可以进入TCriticalSection,所以如果一个线程进入,修改共享变量,然后离开,那么每个线程都会等待其他线程。

如果您将大部分时间花在 TCriticalSection 中,这将无助于扩展,因为您的线程将主要等待对象,但它确实确保只有一个线程正在修改受 TCriticalSection 保护的数据一次。

因此,如果您的代码不断更新共享变量,这将阻止它们发生冲突,但不会真正提供单独线程的好处。但是,如果线程在不修改变量的情况下进行大量计算,那么这允许该代码并行执行。

【讨论】:

  • 我的问题不是我想要不同的线程访问共享变量。我正在寻找在不同线程中声明和使用同一单元的全局变量的不同实例的方法。无论如何,谢谢您的回复
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2021-09-29
  • 1970-01-01
  • 2014-05-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多