线程类内部或外部的关键部分更好? [内部示例]

时间:2015-10-11 17:46:12

标签: multithreading delphi critical-section

我有几个线程(Providers),由其他线程(Workers)并发使用。

根据线程,几个线程意味着severan关键部分。

将一个临界区变量置于一个线程类中是不可能的,还是更好地为每个需要同时访问的线程的关键部分保存单独的数组?

还是有更好的做法?

UPD:有点解释。

流程是:

  1. 工作人员将数据池加载到其自身
  2. 在自己的数据池周期中,工作人员尝试访问提供商
  3. 工作人员获得对提供者的访问权限 - >
  4. 工人锁定它 - >
  5. 工作人员从自己的池中向提供者发布数据 - >
  6. 提供商开始做一份工作 - >
  7. 当工作完成时,提供者向工作人员发送事件 - >
  8. 工人获得结果 - >
  9. 工人释放锁定 - >
  10. 转到2或退出来自池中的最后一个数据
  11. 这实际上在真实情况下效果很好,结果非常好。 但问题是当我需要杀死其中一个提供商时。

    如果Provider类中有一个关键部分,那么一旦Worker已经进入CS并等待解锁并且提供者尚未真正终止,但终止下一步就可能获得访问冲突。

    if not Provider.Terminated then
       Provider.CS.Enter;  //This is a place where AV occurs.
    Provider.PostData(Worker.Data);
    Provider.StartJob;
    WaitForSingleObject(Provider.EventJobIsDone, 30000);
    Provider.CS.Leave; //or here 
    

    如果我必须将CS放在外面 - 如何更好地执行此操作并测试提供程序是否已终止?

    多个提供商和许多工人,随时创建/终止并应该一起工作。没有选项,一个Worker处理其所有数据池而其他人正在等待。每个工作人员进行一次迭代然后等待其他工作人员执行相同操作,然后再次执行下一次迭代然后再次等待,而其他工作人员执行相同操作,依此类推,直到所有池都被处理并且工作人员终止(每个人都在其自己的时间,当池时完了)。

    每次迭代的工作者和结果都会影响下次为该工作者选择的提供者。

    再次 - 这在真实条件下工作正常,不会发生卡住。非常快,并且预期会有效。

    示例

    一个粗略的例子。

    你有工作线程,它从数据库加载数据池。有不同的DB,不仅仅是一个。每个项目 - 重磅图片0.1Mb-10Mb - 你以前从来不知道。想象一下,每个工人有100-5000件物品需要处理 - 你需要将它转换为较小的图片或做一些其他过程。你永远不会知道任何下一个工作线程中会有多少项。它可能是10,1000,5000,或多或少。每当一个Worker线程启动时 - 新的Worker线程都不应该超时开始处理数据池中的第一个项目,因为这样会浪费时间。

    您有10个提供商,它们连接到本地网络上的10台计算机。这些机器可以远程完成您需要的所有工作。 (一个粗略的例子,请)

    你应该平衡(真正的策略更复杂,取决于实际数据 - 它类似于:它是JPG,TIFF,PNG,BMP或任何格式,但只是请考虑平衡)所有提供商之间的流量正确。要做到这一点,你应该计算处理数据的数量,并决定每次迭代在哪里放置下一个工人调用 - 到Provider1,2,3 ...或10.此外你需要立即将结果返回给调用Worker,因为一个Worker应该立即报告,给DB写一个标志(只有Worker知道哪个DB)处理数据(!important)等等。

    这就是条件。 简而言之:

    • 10-50名工人,每次100-5000件物品
    • 工人自行启动并完成
    • 你无法预测任何时候会有多少工人
    • 服务全天候工作并准备处理数据
    • 10个提供商,每个都是与网络上的机器的连接
    • 新员工应尽快开始处理其数据
    • 需要在提供商之间平衡流量
    • 通过每个提供商处理的数据量会影响为下一个数据流程选择下一个提供商的决定。
    • 工作人员应始终获得结果并将数据更新到DB
    • 突然您即将终止提供商
    • 一些工作人员已经在等待使用此提供商处理数据

    如何告诉工作人员停止等待以及何时解冻 - 如何避免在工作流程的下一步(在上面的代码中显示)中非常确定的访问冲突。

2 个答案:

答案 0 :(得分:0)

您的问题归结为以下情况:

  • 内部有CS的提供者线程必须终止。
  • 可能会阻止多个工作线程,等待CS被释放。
  • 提供程序线程在CS发布后立即终止,导致所有挂起的线程访问不存在的线程。

这是一个设计问题,从不访问可以在不事先通知的情况下终止的线程。你可以通过一些沟通解决它。 也许让提供者活得足够长,告诉所有其他工作者线程,是时候说再见了。设置信号量,使用生命结束消息回答所有待处理的请求。在发送请求之前,请确保工作线程查看信号量。

答案 1 :(得分:0)

我对你所写的内容印象深刻,很多系统都不在你的掌控之中。我仍然有点不确定你可以改变多少。

我已经实现了一个全局Controller : TSystemController类,用于控制所有部分。

我理解,终止提供商时,由您决定。 因此,我认为只有通过添加全局ProviderJob List / Queue才能避免AV。

工作人员将推送工作,提供者将弹出工作。 关键部分用于在推/弹期间由许多涉及的线程进行一致性。 我只使用了一个关键部分,因为此队列负责保存整个系统的所有作业。

A ProviderJob : TProviderJob,将ThreadID保存到提供程序线程,以避免引用已终止/已释放的提供程序。除了WorkItem之外,还需要一个由worker提供的DoneEvent,因此Provider可以向等待的worker发出信号。

如果提供者突然被杀,那么工人将等待30秒并超时。

TProviderJob = class
  FProviderThreadID : Cardinal;
  FWorkItem : TWorkItem;
  FWorkDoneEvent : TEvent;
public
  property ProviderThreadID : Cardinal read FProviderThreadID;
  property WorkItem : TWorkItem read FWorkItem;
  property WorkDoneEvent : TEvent read FWorkDoneEvent;
end;

ProviderJob被Worker线程推送如下(没有直接引用Provider对象):

ProviderID := Controller.GetProviderThreadID;
Controller.PushWorkItemToProviderJobList( ProviderID, CurrentWorkItem, WorkDoneEvent );

工作人员将等到提供者完成该工作:

if WaitForSingleObjehect( WorkDoneEvent.Handle, 30000 ) = WAIT_OBJECT_0    then
begin
  if CurrentWorkItem.State = wisProcessedOK then
  begin
    Inc(Controller.FWorkItemsDone);
    Inc(NextWorkItemIdx);
    if NextWorkItemIdx >= WorkItems.Count then
      Terminate;
  end;
end;

各个提供商正在按如下方式处理工作:

procedure TProviderThread.Execute;
var
  WorkItem: TWorkItem;
  WorkDoneEvent: TEvent;
begin
  while not Terminated do
  begin
    Controller.PopNextWorkItemFromProviderJobList( self.ThreadID, WorkItem, WorkDoneEvent );
    if (WorkItem<>nil) and (WorkDoneEvent<>nil) then
    begin
      WorkItem.FState := wisStarted;
      Sleep( Round( Random( 5000 )));
      WorkItem.FState := wisProcessedOK;
      WorkDoneEvent.SetEvent;
    end
    else
      Sleep(500);
  end;
end;

这是一个例子: Screenshot of the test app

以下是完整的解决方案:

object Form1: TForm1
  Left = 0
  Top = 0
  Caption = 'Form1'
  ClientHeight = 248
  ClientWidth = 477
  Color = clBtnFace
  Font.Charset = DEFAULT_CHARSET
  Font.Color = clWindowText
  Font.Height = -11
  Font.Name = 'Tahoma'
  Font.Style = []
  OldCreateOrder = False
  PixelsPerInch = 96
  TextHeight = 13
  object Memo1: TMemo
    Left = 8
    Top = 8
    Width = 369
    Height = 209
    Lines.Strings = (
      'Memo1')
    TabOrder = 0
  end
  object bStart: TButton
    Left = 383
    Top = 72
    Width = 75
    Height = 25
    Caption = 'Start'
    TabOrder = 1
    OnClick = bStartClick
  end
  object bStop: TButton
    Left = 383
    Top = 103
    Width = 75
    Height = 25
    Caption = 'Stop'
    TabOrder = 2
    OnClick = bStopClick
  end
  object bKillProvider: TButton
    Left = 383
    Top = 134
    Width = 75
    Height = 25
    Caption = 'Kill Provider'
    TabOrder = 3
    OnClick = bKillProviderClick
  end
  object Timer1: TTimer
    OnTimer = Timer1Timer
    Left = 400
    Top = 8
  end
end



unit Unit1;

interface

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

type
  TForm1 = class(TForm)
    Timer1: TTimer;
    Memo1: TMemo;
    bStart: TButton;
    bStop: TButton;
    bKillProvider: TButton;
    procedure Timer1Timer(Sender: TObject);
    procedure bKillProviderClick(Sender: TObject);
    procedure bStopClick(Sender: TObject);
    procedure bStartClick(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  Form1: TForm1;



implementation

{$R *.dfm}

uses
  System.SyncObjs,
  Generics.Collections;

type
  TWorkItemStateE = ( wisNotStarted, wisStarted, wisProcessedOK );

  TWorkItem = class
  private
    FState : TWorkItemStateE;
    FID : Integer;
  public
    property ID : Integer read FID;
    property State: TWorkItemStateE read FState;
  end;

  TWorkItemList = class(TObjectList<TWorkItem>);

  TWorkerThread = class(TThread)
  protected
    procedure Execute; override;
  end;
  TWorkerThreadList = class(TObjectList<TWorkerThread>);

  TProviderThread = class(TThread)
  protected
    procedure Execute; override;
  end;
  TProviderThreadList = TObjectList<TProviderThread>;

  TProviderJob = class
    FProviderThreadID : Cardinal;
    FWorkItem : TWorkItem;
    FWorkDoneEvent : TEvent;
  public
    property ProviderThreadID : Cardinal read FProviderThreadID;
    property WorkItem : TWorkItem read FWorkItem;
    property WorkDoneEvent : TEvent read FWorkDoneEvent;
  end;
  TProviderJobList = TObjectList<TProviderJob>;

  TSystemContoller = class
  private
    FProviders: TProviderThreadList;
    FWorkers : TWorkerThreadList;
    FGlobalProviderJobList : TProviderJobList;
    FCS_GlobalProviderJobList: TCriticalSection;

    FStarted : Boolean;
    FRemovedProviders: Integer;
    FKilledProviders: Integer;
    FRemovedWorkers: Integer;
    FWorkItemsDone: Integer;
    FStartedDateTime: TDateTime;

    procedure ThreadTerminated(Sender: TObject);
    procedure AddProvider;
    procedure AddWorker;
  public
    constructor Create;
    destructor Destroy; override;

    function GetProvider : TProviderThread;
    function GetProviderThreadID: Cardinal;
    procedure PopNextWorkItemFromProviderJobList(
                         const AProviderThreadID: Cardinal;
                         out NextWorkItem: TWorkItem;
                         out NextWorkDoneEvent: TEvent);
    procedure PushWorkItemToProviderJobList(
                         AProviderThreadID: Cardinal;
                         AWorkItem: TWorkItem;
                         AWorkDoneEvent: TEvent);

    procedure Start;
    procedure Stop;
    procedure KillProvider;

    function GetStatusReport : String;
  end;

var
  Controller : TSystemContoller;




{ TWorkThread }

procedure TWorkerThread.Execute;
procedure LoadWorkItems(  AWorkItems : TWorkItemList );
var
  WorkItemCount: Integer;
  n : Integer;
    WorkItem: TWorkItem;
begin
  // Load work items:
  WorkItemCount := 1+Round(Random(10));
  for n := 1 to WorkItemCount do
  begin
    WorkItem := TWorkItem.Create;
    WorkItem.FID := n;
    AWorkItems.Add(WorkItem);
  end;
end;

var
  WorkItems : TWorkItemList;
  ProviderID: Cardinal;
  NextWorkItemIdx: Integer;
  CurrentWorkItem: TWorkItem;
  WorkDoneEvent : TEvent;
begin
  WorkItems := TWorkItemList.Create;
  WorkDoneEvent := TEvent.Create(nil, False, False, '' );
  try
    // load:
    LoadWorkItems( WorkItems );

    // process work items:
    NextWorkItemIdx := 0;
    while not Terminated do
    begin
      CurrentWorkItem := WorkItems[ NextWorkItemIdx ];
      ProviderID := Controller.GetProviderThreadID;
      Controller.PushWorkItemToProviderJobList( ProviderID, CurrentWorkItem, WorkDoneEvent );

      if WaitForSingleObject( WorkDoneEvent.Handle, 30000 ) = WAIT_OBJECT_0 then
      begin
        if CurrentWorkItem.State = wisProcessedOK then
        begin
          Inc(Controller.FWorkItemsDone);
          Inc(NextWorkItemIdx);
          if NextWorkItemIdx >= WorkItems.Count then
            Terminate;
        end;
      end;
      Sleep(1000);

    end;
  finally
    WorkDoneEvent.Free;
    WorkItems.Free;
  end;
end;


{ TProviderThread }

procedure TProviderThread.Execute;
var
  WorkItem: TWorkItem;
  WorkDoneEvent: TEvent;
begin
  while not Terminated do
  begin
    Controller.PopNextWorkItemFromProviderJobList( self.ThreadID, WorkItem, WorkDoneEvent );
    if (WorkItem<>nil) and (WorkDoneEvent<>nil) then
    begin
      WorkItem.FState := wisStarted;
      Sleep( Round( Random( 5000 )));
      WorkItem.FState := wisProcessedOK;
      WorkDoneEvent.SetEvent;
    end
    else
      Sleep(500);
  end;
end;

{ TSystemContoller }

constructor TSystemContoller.Create;
begin
  inherited;
  FStartedDateTime:= now;

  FCS_GlobalProviderJobList := TCriticalSection.Create;
  FGlobalProviderJobList := TProviderJobList.Create;

  FProviders:= TProviderThreadList.Create;
  FProviders.OwnsObjects := False;

  FWorkers := TWorkerThreadList.Create;
  FWorkers.OwnsObjects := False;
end;

destructor TSystemContoller.Destroy;
begin
  FCS_GlobalProviderJobList.Free;
  FGlobalProviderJobList.Free;
  FWorkers.Free;
  FProviders.Free;
  inherited;
end;

procedure TSystemContoller.Start;
var
  n: Integer;
begin
  if not FStarted then
  begin
    FStarted := True;
    for n := 1 to 5 do
      AddProvider;
    for n := 1 to 10 do
      AddWorker;
  end;
end;

procedure TSystemContoller.Stop;
var
  n: Integer;
begin
  for n := FProviders.Count-1 to 0 do
    FProviders[n].Terminate;

  for n := FWorkers.Count-1 to 0 do
    FWorkers[n].Terminate;

  FStarted := False;
end;

procedure TSystemContoller.KillProvider;
var
  Provider: TProviderThread;
begin
  Provider := GetProvider;
  if Provider<>nil then
  begin
    if not Provider.Terminated then
    begin
      GetProvider.Terminate;
      Inc( FKilledProviders );
    end;
  end;
end;

procedure TSystemContoller.AddProvider;
var
  Provider: TProviderThread;
begin
  Provider := TProviderThread.Create(True);
  Provider.OnTerminate := ThreadTerminated;
  Provider.FreeOnTerminate := True;
  FProviders.Add( Provider );
  Provider.Start;
end;

procedure TSystemContoller.AddWorker;
var
  Worker: TWorkerThread;
begin
  Worker := TWorkerThread.Create(True);
  Worker.OnTerminate := ThreadTerminated;
  Worker.FreeOnTerminate := True;
  FWorkers.Add( Worker );
  Worker.Start;
end;

procedure TSystemContoller.ThreadTerminated(Sender : TObject );
begin
  if Sender is TProviderThread then
  begin
    FProviders.Remove(TProviderThread(Sender));
    Inc(FRemovedProviders);
    if FStarted then
      AddProvider;
  end
  else
  if Sender is TWorkerThread then
  begin
    FWorkers.Remove(TWorkerThread(Sender));
    Inc(FRemovedWorkers);
    if FStarted then
      AddWorker;
  end;
end;

procedure TSystemContoller.PushWorkItemToProviderJobList(
                                     AProviderThreadID: Cardinal;
                                     AWorkItem: TWorkItem;
                                     AWorkDoneEvent: TEvent);
var
  ProviderJob: TProviderJob;
begin
  FCS_GlobalProviderJobList.Enter;
  try
    ProviderJob := TProviderJob.Create;
    ProviderJob.FProviderThreadID := AProviderThreadID;
    ProviderJob.FWorkItem := AWorkItem;
    ProviderJob.FWorkDoneEvent := AWorkDoneEvent;
    FGlobalProviderJobList.Add( ProviderJob );
  finally
    FCS_GlobalProviderJobList.Leave;
  end;
end;

procedure TSystemContoller.PopNextWorkItemFromProviderJobList(
                                   const AProviderThreadID: Cardinal;
                                   out NextWorkItem: TWorkItem;
                                   out NextWorkDoneEvent: TEvent);
var
  n : Integer;
begin
  FCS_GlobalProviderJobList.Enter;
  try
    NextWorkItem := nil;
    NextWorkDoneEvent := nil;
    for n := 0 to FGlobalProviderJobList.Count-1 do
    begin
      if FGlobalProviderJobList[n].ProviderThreadID = AProviderThreadID then
      begin
        NextWorkItem := FGlobalProviderJobList[n].WorkItem;
        NextWorkDoneEvent := FGlobalProviderJobList[n].WorkDoneEvent;
        FGlobalProviderJobList.Delete(n);
        Exit;
      end;
    end;
  finally
    FCS_GlobalProviderJobList.Leave;
  end;
end;

function TSystemContoller.GetProvider: TProviderThread;
var
  ProviderIdx: Integer;
begin
  ProviderIdx := Trunc(Random( FProviders.Count ));
  if InRange(ProviderIdx, 0, FProviders.Count-1 ) then
    Result := FProviders[ ProviderIdx ]
  else
    Result := nil;
end;

function TSystemContoller.GetProviderThreadID: Cardinal;
var
  Provider: TProviderThread;
begin
  Provider := GetProvider;
  if Provider<>nil then
    Result := Provider.ThreadID;
end;

function TSystemContoller.GetStatusReport: String;
const
  cState : array[Boolean] of string = ( 'Stopped', 'Started' );
begin
  Result := 'Start Date and Time: ' + DateTimeToStr(FStartedDateTime) + #13#10+
            'Date and Time: ' + DateTimeToStr(now) + #13#10+
            'System State: ' + cState[FStarted] + #13#10+ #13#10 +
            'Queued Work Items: ' + IntToStr( self.FGlobalProviderJobList.Count )+ #13#10 +
            'Work Items Done: ' + IntToStr(FWorkItemsDone)+ #13#10 + #13#10 +
            'Current Providers: ' + IntToStr( self.FProviders.Count ) + #13#10+
            'Removed Providers: ' + IntToStr( FRemovedProviders ) + #13#10 +
            'Random Provider Kills: ' + IntToStr(FKilledProviders)+ #13#10 + #13#10 +
            'Current Workers: ' + IntToStr( self.FWorkers.Count ) + #13#10 +
            'Removed Workers: ' + IntToStr( FRemovedWorkers );
end;



procedure TForm1.bKillProviderClick(Sender: TObject);
begin
  Controller.KillProvider;
end;

procedure TForm1.bStartClick(Sender: TObject);
begin
  Controller.Start;
end;

procedure TForm1.bStopClick(Sender: TObject);
begin
  Controller.Stop;
end;

procedure TForm1.Timer1Timer(Sender: TObject);
begin
  Memo1.Text :=  Controller.GetStatusReport;

  if Random(100) < 30 then
    Controller.KillProvider;
end;

initialization
  Controller := TSystemContoller.Create;
  Controller.Start;
finalization
  Controller.Stop;
  Controller.Free;
end.