multithreading AnTProc समाप्त होने के बाद TTask.Run(AnonProc) में बंद नहीं हुआ




delphi (2)

डेल्फी में बेनामी विधियां बंद हो जाती हैं, जो अज्ञात विधि समाप्त होने तक संदर्भ में स्थानीय परिवेश को "आस-पास" रखती है। यदि इंटरफ़ेस चर का उपयोग कर रहे हैं, तो वे अज्ञात विधि समाप्त होने से पहले नहीं उनके संदर्भित उदाहरण को कम कर देंगे। अब तक सब ठीक है।

अज्ञात विधि के साथ TTask.Run (AProc: TProc) का उपयोग करते समय मैं उम्मीद करता हूं कि जब संबंधित कार्यकर्ता थ्रेड ने "एपोक" निष्पादित करना समाप्त कर दिया है तो बंद होने पर बंद हो जाएगा। हालांकि ऐसा प्रतीत नहीं होता है। प्रोग्राम समाप्ति पर, जब थ्रेड पूल (यह टीटीस्क जेनरेटेड थ्रेड से संबंधित है) जारी हो जाता है, तो आप अंत में देख सकते हैं कि इन स्थानीय संदर्भित उदाहरण जारी किए जाते हैं - यानी बंद होने पर स्पष्ट रूप से रिलीज़ हो जाता है।

सवाल यह है कि यह एक सुविधा या बग है? या मैं यहाँ कुछ देखरेख करता हूँ?

नीचे, TTask.Run (...) के बाद। प्रतीक्षा करें कि मैं एलएफयू के विनाशक को बुलाए जाने की उम्मीद करूंगा - जो नहीं होता है।

procedure Test3;
var
  LFoo: IFoo;
begin
  LFoo := TFoo.Create;

  TTask.Run(
    procedure
    begin
      Something(LFoo);
    end).Wait; // Wait for task to finish

   //After TTask.Run has finished, it should let go LFoo out of scope - which it does not apprently. 
end;

निम्नलिखित एक पूर्ण परीक्षण केस है, जो दिखाता है कि "सरल" अज्ञात विधि अपेक्षित (टेस्ट 2) के रूप में काम करती है, लेकिन जब टीटीस्क में खिलाया जाता है। यह नहीं करता है (टेस्ट 3)

program InterfaceBug;

{$APPTYPE CONSOLE}
{$R *.res}

uses
  System.Classes,
  System.SysUtils,
  System.Threading;

type

  //Simple Interface/Class
  IFoo = interface(IInterface)
    ['{7B78D718-4BA1-44F2-86CB-DDD05EF2FC56}']
    procedure Bar;
  end;

  TFoo = class(TInterfacedObject, IFoo)
  public
    constructor Create;
    destructor Destroy; override;
    procedure Bar;
  end;

procedure TFoo.Bar;
begin
  Writeln('Foo.Bar');
end;

constructor TFoo.Create;
begin
  inherited;
  Writeln('Foo.Create');
end;

destructor TFoo.Destroy;
begin
  Writeln('Foo.Destroy');
  inherited;
end;

procedure Something(const AFoo: IFoo);
begin
  Writeln('Something');
  AFoo.Bar;
end;

procedure Test1;
var
  LFoo: IFoo;
begin
  Writeln('Test1...');
  LFoo := TFoo.Create;
  Something(LFoo);
  Writeln('Test1 done.');
  //LFoo goes out od scope, and the destructor gets called
end;

procedure Test2;
var
  LFoo: IFoo;
  LProc: TProc;
begin
  Writeln('Test2...');
  LFoo := TFoo.Create;
  LProc := procedure
    begin
      Something(LFoo);
    end;
  LProc();
  Writeln('Test2 done.');
   //LFoo goes out od scope, and the destructor gets called
end;

procedure Test3;
var
  LFoo: IFoo;
begin
  Writeln('Test3...');
  LFoo := TFoo.Create;
  TTask.Run(
    procedure
    begin
      Something(LFoo);
    end).Wait; // Wait for task to finish
  //LFoo := nil;  This would call TFoo's destructor,
  //but it should get called automatically with LFoo going out of scope - which apparently does not happen!
  Writeln('Test3 done.');
end;

begin
  try
    Test1; //works
    Writeln;
    Test2; //works
    Writeln;
    Test3; //fails
    Writeln('--------');
    Writeln('Expected: Three calls of Foo.Create and three corresponding ones of Foo.Destroy');
    Writeln;
    Writeln('Actual: The the third Foo.Destroy is missing and is executed when the program terminates, i.e. when the default ThreadPool gets destroyed.');
    ReadLn;
  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;

end.

यह ज्ञात मुद्दा है: TThreadPool कार्यकर्ता थ्रेड पिछले निष्पादित कार्य के संदर्भ में है

TThreadPool.TQueueWorkerThread.Execute में एक अस्थायी चर अंतिम निष्पादित कार्य-वस्तु (कार्य) का संदर्भ रखता है, जिसे निष्पादन विधि समाप्त होने पर ही रिलीज़ किया जाता है।

पूल में होने के कारण धागे को तब तक जीवित रखा जाता है जब तक पूल नष्ट नहीं हो जाता है, जो कि डिफ़ॉल्ट पूल के लिए इकाई के अंतिमकरण के दौरान होता है। इस प्रकार अंतिम निष्पादित कार्य प्रोग्राम समाप्त होने तक जारी नहीं किए जाते हैं।


मैंने इस बग का कुछ और विश्लेषण किया है कि वास्तविक कारण ITask लिए ITask में ITask क्यों आयोजित किया जा रहा था। TThreadPool.TQueueWorkerThread.Execute . ज्ञात समस्या में उल्लिखित TThreadPool.TQueueWorkerThread.Execute

कोड की निम्नलिखित निर्दोष दिखने वाली रेखा समस्या है:

Item := ThreadPool.FQueue.Dequeue;

वह मामला क्या है? चूंकि TQueue<T>.Dequeue को इनलाइन के रूप में चिह्नित किया गया है और अब आपको यह जानना है कि कंपाइलर प्रबंधित प्रकार लौटने वाले इनलाइन फ़ंक्शंस के लिए तथाकथित वापसी मूल्य अनुकूलन लागू नहीं करता है।

इसका मतलब यह है कि संकलक द्वारा इस कोड में वास्तव में अनुवाद करने से पहले लाइन (मैंने इसे बहुत सरल बना दिया है)। tmp एक कंपाइलर जेनरेटेड वैरिएबल है - यह विधि के प्रस्ताव में स्टैक पर स्थान सुरक्षित रखता है:

tmp := ThreadPool.FQueue.Dequeue;
Item := tmp;

इस चर को विधि के end में अंतिम रूप दिया जाता है। आप वहां एक ब्रेकपॉइंट डाल सकते हैं और एक TTask.Destroy में डाल सकते हैं और फिर आप देखते हैं कि एप्लिकेशन के अंत तक पहुंचने के बाद एप्लिकेशन समाप्त होता है, यह अंतिम TTask इंस्टेंस को नष्ट कर देगा क्योंकि इसे TTask रखने वाले अस्थायी चर को साफ़ किया जा रहा है।

मैंने इस मुद्दे को स्थानीय रूप से ठीक करने के लिए कुछ छोटे हैक का इस्तेमाल किया। मैंने TThreadPool.TQueueWorkerThread.Execute में अस्थायी परिवर्तनीय TThreadPool.TQueueWorkerThread.Execute को खत्म करने के लिए इस स्थानीय प्रक्रिया को जोड़ा। TThreadPool.TQueueWorkerThread.Execute विधि:

procedure InternalDequeue(var Item: IThreadPoolWorkItem);
begin
  Item := ThreadPool.FQueue.Dequeue;
end;

और फिर विधि के अंदर कोड बदल दिया:

InternalDequeue(Item);

यह अभी भी Dequeue को एक अस्थायी चर उत्पन्न करने का कारण Dequeue है लेकिन अब यह केवल InternalDequeue Dequeue विधि के अंदर रहता है और इसे बाहर निकलने के बाद साफ़ किया जा रहा है।

संपादित करें (09.11.2017): यह संकलक में 10.2 में तय किया गया है। यह अब अस्थायी रूप से अस्थायी चर के असाइनमेंट के बाद अंततः ब्लॉक को सम्मिलित करता है ताकि अस्थायी चर किसी अतिरिक्त संदर्भ को इसके अलावा किसी भी संदर्भ का कारण न दे।





delphi