2016-11-17 9 views
22

Anonimowe metody w Delphi tworzą zamknięcie, które zachowuje "otaczające" zmienne lokalne w kontekście, aż do zakończenia anonimowej metody. Jeśli używasz zmiennych interfejsów, to zmniejszą one ich instancję, do której odwołujesz się, jeszcze przed zakończeniem anonimowej metody. Jak na razie dobrze.Zamknięcie w TTask.Run (AnonProc) nie zostało wydane po zakończeniu AnonProc

Podczas korzystania z metody TTask.Run (AProc: TProc) za pomocą metody anonimowej oczekiwałbym, że zamknięcie zostanie zwolnione, gdy powiązany wątek roboczy zakończy wykonywanie "AProc". Wydaje się, że to się nie zdarza. W przypadku zakończenia programu, gdy pulę wątków (do której należy ten wątek generowany przez TTask) zostanie zwrócona, można w końcu zobaczyć, że te lokalnie przywoływane instancje zostaną zwolnione - tj. Zamknięcie zostanie najwidoczniej zwolnione.

Pytanie brzmi, czy jest to funkcja czy błąd? Czy mogę tu coś nadzorować?

Poniżej, po TTask.Run (...). Czekaj, oczekuję wywołania destruktora LFoo - co nie ma miejsca.

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; 

Poniżej znajduje się pełna przypadek testowy, który pokazuje, że „prosty” sposób anonimowy działa zgodnie z oczekiwaniami (Test2), ale kiedy podawany do TTask.Run nie robi (Test3)

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. 

Odpowiedz

19

Zrobiłem kilka analiza tego błędu, aby dowiedzieć się prawdziwy powód dlaczego ITask odbywało w TThreadPool.TQueueWorkerThread.Execute jak wspomniano w known issue.

Poniższy niewinny linia kodu jest problem:

Item := ThreadPool.FQueue.Dequeue; 

Dlaczego jest to, że tak jest? Ponieważ TQueue<T>.Dequeue jest oznaczony jako wbudowany, a teraz musisz wiedzieć, że kompilator nie stosuje tak zwanego return value optimization dla funkcji wstawionych zwracających zarządzany typ.

Oznacza to, że wiersz przed naprawdę przetłumaczony (bardzo uprościłem to) do tego kodu przez kompilator. tmp jest zmienną generowane kompilator - zastrzega miejsca na stosie w prologu metody:

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

Zmienna ta zostanie sfinalizowana w end metody. Możesz umieścić tam punkt przerwania i jeden na TTask.Destroy, a następnie zobaczysz, że gdy aplikacja kończy się po osiągnięciu końca metody, spowoduje to, że ostatnia instancja TTask zostanie zniszczona, ponieważ zmienna tymczasowa utrzymująca go przy życiu jest czyszczona.

Użyłem trochę hack, aby rozwiązać ten problem lokalnie. Dodałem tę procedurę lokalnej wyeliminować zmienna tymczasowa skradanie się metodą TThreadPool.TQueueWorkerThread.Execute:

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

a następnie zmienił kod wewnątrz metody:

InternalDequeue(Item); 

ten będzie nadal powodują Dequeue produkować zmiennej tymczasowej ale teraz to żyje tylko w metodzie InternalDequeue i jest czyszczone po jego wyjściu.

Edytuj (09.11.2017): Naprawiono to w 10.2 w kompilatorze. Teraz wstawia końcowy blok po przypisaniu zmiennej temp do rzeczywistej, więc zmienna tymczasowa nie powoduje dodatkowego odniesienia dłużej niż powinna.

+2

Mam nadzieję, że twoje odkrycia przyspieszą rozwiązanie tego problemu. –

+3

Biorąc pod uwagę, że używasz inline dla wydajności, prawdopodobnie powinieneś używać "var" zamiast "out", to uniknie niejawnego wywołania IntfClear (które w przeciwnym razie pokonałoby cały punkt użycia inline) –

+1

@EricGrange Dobry punkt - czasami Nadal zapominam o tym, że Delphi oczyszcza przekazany param przed wywołaniem. W każdym razie FWIW nie używam inline w tym przypadku, RTL ma;) –

18

to jest znany problem: TThreadPool worker thread holds reference to last executed task

tymczasowa zmienna w TThreadPool.TQueueWorkerThread.Execute utrzymuje odniesienie do ostatniej wykonanej obróbki przedmiotu (zadania), który jest tylko zwolniony po zakończeniu metody Execute.

Będąc w puli wątek jest zwykle utrzymywany przy życiu aż do zniszczenia puli , co oznacza dla puli Domyślnej podczas finalizacji jednostki . Tak więc ostatnie wykonane zadania nie są zwalniane do momentu zakończenia programu.

+0

Nie wiedziałem o RSP-12462, ale wciąż jest pytanie (przynajmniej dla mnie), czy anonimowa metoda (która jest również używana w 12462) nie powinna zwalniać zamknięcia, gdy kończy się wykonywanie - nawet jeśli zawierający TTask nie został zwolniony (jak pokazuje test) –

+1

Zadanie samo trzyma zamknięcie, więc w razie nieszczelności zadania, zamknięcie również przecieka. –

Powiązane problemy