Как я могу захватывать переменные анонимным методом при использовании его в OTL?
Что я хочу сделать:
У меня есть несколько объектов в списке родов. Я хочу захватить каждый из этих объектов в анонимном методе и выполнить этот метод как отдельную задачу OTL.
Это упрощенный пример:
program Project51;
{$APPTYPE CONSOLE}
uses
SysUtils, Generics.Collections, OtlTaskControl, OtlTask;
type
TProc = reference to procedure;
type
TMyObject = class(TObject)
public
ID: Integer;
constructor Create(AID: Integer);
end;
constructor TMyObject.Create(AID: Integer);
begin
ID := AID;
end;
var
Objects: TList<TMyObject>;
LObject: TMyObject;
MyProc: TProc;
begin
Objects := TList<TMyObject>.Create;
Objects.Add(TMyObject.Create(1));
Objects.Add(TMyObject.Create(2));
Objects.Add(TMyObject.Create(3));
for LObject in Objects do
begin
//This seems to work
MyProc := procedure
begin
Writeln(Format('[SameThread] Object ID: %d',[LObject.ID]));
end;
MyProc;
//This doesn't work, sometimes it returns 4 lines in console!?
CreateTask(
procedure(const Task: IOmniTask)
begin
Writeln(Format('[Thread %d] Object ID: %d',[Task.UniqueID, LObject.ID]));
end
).Unobserved.Run;
end;
Sleep(500); //Just wait a bit for tasks to finish
Readln;
end.
И вот результат:
Как видите, захват в основном потоке работает нормально. Тем не менее, я не знаю, был ли захвачен указатель на объект или только его поле идентификатора?
Когда я пытаюсь захватить объект и передать анонимный метод CreateTask
Функциональные вещи становятся странными.
Прежде всего, только третий экземпляр TMyObject
казалось захваченным. Во-вторых, у меня есть четыре сообщения в журнале консоли, несмотря на тот факт, что у меня есть только три объекта в общем списке. Второе поведение противоречиво, иногда у меня три сообщения в консоли, иногда четыре.
Пожалуйста, объясните мне причину двух проблем, упомянутых выше, и предложите решение, которое устраняет проблему и позволяет мне передавать каждый экземпляр объекта в отдельную задачу OTL. (Я не хочу использовать обычные TThread
учебный класс.)
2 ответа
Документация описывает, что происходит:
Обратите внимание, что захват переменных захватывает переменные, а не значения. Если значение переменной изменяется после захвата путем создания анонимного метода, значение переменной, захваченной анонимным методом, также изменяется, потому что это одна и та же переменная с одинаковым хранилищем.
В вашем коде есть только один LObject
переменная, поэтому все анонимные методы, которые вы создаете, ссылаются на нее. По мере того, как ваш цикл прогрессирует, значение LObject
изменения. Задачи еще не получили возможности начать выполнение, поэтому, когда они, наконец, запускаются, цикл завершается и LObject
имеет свое окончательное значение. Формально это окончательное значение не определено после цикла.
Чтобы получить значение переменной цикла, оберните создание задачи в отдельную функцию:
function CreateItemTask(Obj: TMyObject): TOmniTaskDelegate;
begin
Result := procedure(const Task: IOmniTask)
begin
Writeln(Format('[Thread %d] Object ID: %d',[Task.UniqueID, Obj.ID]));
end;
end;
Затем измените код вашего цикла:
CreateTask(CreateItemTask(LObject)).Unobserved.Run;
Анонимные процедуры фиксируют переменные, а не значения. Итак, вы захватываете переменную LObject. Поскольку это переменная цикла, значение LObject изменяется. Анонимные процедуры оценивают LObject, когда они выполняются, а не когда создаются анонимные процедуры.
Вместо того, чтобы использовать анонимную процедуру, я бы, вероятно, просто использовал метод TMyObject. Попробуйте написать код таким образом, и я предсказываю, что вам будет легче понять.
procedure TMyObject.TaskProc(const Task: IOmniTask);
begin
Writeln(Format('[Thread %d] Object ID: %d', [Task.UniqueID, Self.ID]));
end;
Причина получения 4 строк вывода, а не 3, вероятно, заключается в том, что WriteLn не является потокобезопасным. Оберните вызов WriteLn в замок, чтобы очистить это.