【问题标题】:multithreading in DelphiDelphi中的多线程
【发布时间】:2021-09-29 19:00:13
【问题描述】:

我有一个带有 TExecution 类的数字运算应用程序,该类包含在一个单独的单元 Execution.pas 中并执行所有计算。类实例是从程序的主要形式创建的。很多时候 Execution.pas 中的代码需要连续运行 10-15 次,我想在不同的线程中创建几个 TExecution 实例并并行运行它们。简化版代码如下:

其中有一个 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, System.Threading, 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: array of TThread;
  NoThreads: integer;
  Execution: array of TExecution;
  thread_ID: integer;
begin
    NoThreads := 5;
    SetLength(Execution,NoThreads);
    SetLength(ExecutionThread,NoThreads);
    //----------------------------------------------------------------------------------
   for thread_ID := 0 to Pred(NoThreads) do
    begin
        ExecutionThread[thread_ID] := TThread.CreateAnonymousThread(
        procedure
        begin
            try
                Execution[thread_ID] := TExecution.Create;
                Execution[thread_ID].CalculateSum;
            finally
                if Assigned(Execution[thread_ID]) then
                begin
                    Execution[thread_ID] := nil;
                    Execution[thread_ID].Free;
                end;
            end;
        end);
        ExecutionThread[thread_ID].FreeOnTerminate := true;
        ExecutionThread[thread_ID].Start;
    end;

end;

end.

Execution.pas 单位:

unit Execution;

interface
uses
System.SysUtils, Vcl.Dialogs, System.Classes, WinApi.Windows;

 type
   TExecution = Class
      const
        NoOfTimes = 1000000;
      var
        Sum: integer;
      private
        procedure IncrementSum(var Sum: integer);
      published
        procedure CalculateSum;
   End;

implementation

procedure TExecution.CalculateSum;
var
  i: integer;
begin
    Sum := 0;
    for i := 0 to Pred(NoofTimes) do
    begin
        IncrementSum(Sum);
    end;
end;

procedure TExecution.IncrementSum(var Sum: integer);
begin
    Inc(Sum);
end;

end.

每当我通过单击 Button1 运行上述代码时,TExecution 实例都会运行,但是当我关闭程序时,我在函数 SysFreeMem 中的 GetMem.inc 中遇到访问冲突。显然,代码弄乱了内存,我猜是因为并行内存分配,但我无法找到原因并解决它。 我注意到,使用一个线程(NoThreads := 1),或串行执行代码(使用单个新线程和 5 个 TExecution 实例,或者直接从 MainForm 创建 TExecution 实例时),我没有得到类似的记忆问题。我的代码有什么问题? 非常感谢!

【问题讨论】:

  • 为什么不马上TExecution = Class( TThread )
  • 只是因为我没有考虑过(而且我从没想过这是可能的)。如何创建线程,调用计算程序,然后销毁类?语法是一样的,但是在新线程中创建新类?
  • 那是class inheritance:您将从TExecute 实例化对象,这是TThread 的扩展,只需使用您的附加变量/过程。欢迎来到 OOP。

标签: multithreading delphi


【解决方案1】:

问题来自ExecutionThreadExecution,它们是局部变量。当所有线程都启动时,过程Button1Click 退出,这两个变量在线程终止之前很久就被释放了。

ExecutionThreadExecution 这两个变量移动到TMainForm1 字段中,您的问题就会消失。当然:如果你在线程终止之前关闭程序,你又会遇到麻烦。

另外,反转两行:

Execution[thread_ID] := nil;
Execution[thread_ID].Free;

你必须在 niling 之前释放。

顺便说一句:您应该在 TExecution 中收到关于 published 的编译器警告。

编辑: 在对此答案的评论之后,这是同一进程的代码,但使用显式工作线程和通用 TList 来维护正在运行的线程列表。

主窗体的来源:

unit ThreadExecutionDemoMain;

interface

uses
    Winapi.Windows, Winapi.Messages,
    System.SysUtils, System.Variants, System.Classes,
    System.Generics.Collections,
    Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls,
    ThreadExecutionDemoExecution,
    ThreadExecutionDemoWorkerThread;

type
    TMainForm = class(TForm)
        StartButton: TButton;
        DisplayMemo: TMemo;
        procedure StartButtonClick(Sender: TObject);
    private
        ThreadList : TList<TWorkerThread>;
        procedure WrokerThreadTerminate(Sender : TObject);
    public
        constructor Create(AOwner : TComponent); override;
        destructor  Destroy; override;
    end;

var
  MainForm: TMainForm;

implementation

{$R *.dfm}

constructor TMainForm.Create(AOwner: TComponent);
begin
    ThreadList := TList<TWorkerThread>.Create;
    inherited Create(AOwner);
end;

destructor TMainForm.Destroy;
begin
    FreeAndNil(ThreadList);
    inherited Destroy;;
end;

procedure TMainForm.StartButtonClick(Sender: TObject);
var
    NoThreads    : Integer;
    ID           : Integer;
    WorkerThread : TWorkerThread;
begin
    NoThreads := 5;
    for ID := 0 to Pred(NoThreads) do begin
        WorkerThread := TWorkerThread.Create(TRUE);
        WorkerThread.ID          := ID;
        WorkerThread.OnTerminate := WrokerThreadTerminate;
        WorkerThread.FreeOnTerminate := TRUE;
        ThreadList.Add(WorkerThread);
        DisplayMemo.Lines.Add(Format('Starting thread %d', [WorkerThread.ID]));
        WorkerThread.Start;
    end;
    DisplayMemo.Lines.Add(Format('There are %d running threads', [ThreadList.Count]));
end;

procedure TMainForm.WrokerThreadTerminate(Sender: TObject);
var
    WorkerThread : TWorkerThread;
begin
    WorkerThread := TWorkerThread(Sender);
    ThreadList.Remove(WorkerThread);
    // This event handler is executed in the context of the main thread
    // we can access the user interface directly
    DisplayMemo.Lines.Add(Format('Thread %d done. Sum=%d',
                                 [WorkerThread.ID, WorkerThread.Sum]));
    if ThreadList.Count = 0 then
        DisplayMemo.Lines.Add('No more running threads');
end;

end.

执行单元的来源:

unit ThreadExecutionDemoExecution;

interface

type
    TExecution = class
    const
        NoOfTimes = 1000000;
    private
        FSum: Integer;
        procedure IncrementSum(var ASum: Integer);
    public
        procedure CalculateSum;
        property Sum: Integer    read  FSum
                                 write FSum;
    end;


implementation

{ TExecution }

procedure TExecution.CalculateSum;
var
    I: Integer;
begin
    FSum := 0;
    for I := 0 to Pred(NoOfTimes) do
        IncrementSum(FSum);
end;

procedure TExecution.IncrementSum(var ASum: Integer);
begin
    Inc(ASum);
end;

end.

工作线程的来源:

unit ThreadExecutionDemoWorkerThread;

interface

uses
    System.SysUtils, System.Classes,
    ThreadExecutionDemoExecution;

type
    TWorkerThread = class(TThread)
    private
        FExecution : TExecution;
        FID        : Integer;
        FSum       : Integer;
    protected
        procedure Execute; override;
    public
        property ID        : Integer    read  FID
                                        write FID;
        property Sum       : Integer    read  FSum
                                        write FSum;
    end;


implementation

{ TWorkerThread }

procedure TWorkerThread.Execute;
begin
    FExecution := TExecution.Create;
    try
        FExecution.CalculateSum;
        FSum := FExecution.Sum;
    finally
        FreeAndNil(FExecution);
    end;
end;

end.

【讨论】:

  • 谢谢@fpiette。我试过了,但我得到了完全相同的行为。我尝试将变量添加到 TMainForm1 类(作为私有或公共变量)或作为 MainForm 单元中的变量。在所有情况下,行为都不会改变。
  • 确保在离开程序之前终止所有线程。如果您不想等待线程并仍然确定它们已完成,请将迭代次数更改为较小的值。
  • FreeAndNil 先保存地址,然后设置为nil,然后释放保存的地址。这不是你要做的:你首先 nil 并释放地址,然后使用 nil 地址调用 Free,这不会释放内存。
  • 我将您的代码与我描述的更改一起使用,它使用 Delphi 10.4.2 在这里工作。也许您显示的代码不是您使用的真实代码,或者您使用的是具有不同行为的旧 Delphi。也许尝试使用显式线程(从 TThread 继承的类)而不是匿名线程。
  • 你最好用我写的代码重启。
猜你喜欢
  • 1970-01-01
  • 2013-04-18
  • 1970-01-01
  • 2016-02-06
  • 1970-01-01
  • 1970-01-01
  • 2021-01-15
  • 1970-01-01
  • 2010-10-20
相关资源
最近更新 更多