Является ли потоком назначения указателя метода безопасным?
Пример:
Предположим, у меня будет следующий поток (пожалуйста, не учитывайте то, что используется в этом примере, метод выполнения контекста потока, это просто для объяснения):
type
TSampleThread = class(TThread)
private
FOnNotify: TNotifyEvent;
protected
procedure Execute; override;
public
property OnNotify: TNotifyEvent read FOnNotify write FOnNotify;
end;
implementation
procedure TSampleThread.Execute;
begin
while not Terminated do
begin
if Assigned(FOnNotify) then
FOnNotify(Self); // <- this method can be called anytime
end;
end;
Тогда предположим, что я хотел бы изменить метод события OnNotify
из основного потока в любое время, когда мне нужно. Этот основной поток реализует метод обработчика событий как метод ThreadNotify
здесь:
type
TForm1 = class(TForm)
Button1: TButton;
Button2: TButton;
procedure Button1Click(Sender: TObject);
procedure Button2Click(Sender: TObject);
private
FSampleThread: TSampleThread;
procedure ThreadNotify(Sender: TObject);
end;
implementation
procedure TForm1.ThreadNotify(Sender: TObject);
begin
// do something; unimportant for this example
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
FSampleThread.OnNotify := nil; // <- can this be changed anytime ?
end;
procedure TForm1.Button2Click(Sender: TObject);
begin
FSampleThread.OnNotify := ThreadNotify; // <- can this be changed anytime ?
end;
Вопрос:
Можно ли изменить метод, который можно вызвать из рабочего потока из другого контекста потока в любое время? Безопасно ли делать то, что показано в приведенном выше примере?
Я не совсем уверен, если это абсолютно безопасно, по крайней мере, поскольку указатель метода на самом деле представляет собой пару указателей, и я не знаю, могу ли я считать его как атомную операцию.
Ответы
Ответ 1
Нет, это не потокобезопасно, потому что эта операция никогда не будет "атомарной". TNotifyEvent
состоит из двух указателей, и эти указатели никогда не будут назначены одновременно: один будет назначен, затем будет назначен другой.
32-разрядный Ассемблер, сгенерированный для назначения TNotifyEvent
, состоит из двух различных команд ассемблера, примерно так:
MOV [$00000000], Object
MOV [$00000004], MethodPointer
Если бы это был единственный указатель, тогда у вас были бы некоторые параметры, поскольку эта операция является атомарной: параметры, которые у вас зависят, зависят от того, насколько сильной является модель памяти процессора:
- Если процессор поддерживает модель "последовательной согласованности", то любое чтение, которое происходит после того, как вы напишете память, увидит новое значение, гарантированное. Если это случай, вы можете просто написать свое значение, нет необходимости в барьерах памяти или использовании методов
Interlocked
.
- Если процессор более расслаблен о переупорядочении магазинов и загрузок, вам нужен "барьер памяти". В этом случае самым простым решением является использование InterlockedExchangePointer
К сожалению, я не знаю, насколько сильна модель памяти нынешних процессоров Intel. Там могут возникнуть некоторые косвенные доказательства, которые предполагают некоторый переупорядочивание, рекомендуется использовать Interlocked
, но я не видел окончательного заявления Intel, которое говорит то или другое.
Доказательства:
- Современный процессор использует "предварительную выборку" - это автоматически подразумевает некоторый уровень переупорядочения загрузки/хранения.
- SSE представила конкретные инструкции для работы с кэшем CPU.
Ответ 2
Помимо размера регистра, есть две операции. Проверка и последующее исполнение. Чтобы свести к минимуму, создайте локальный var и используйте его. Но в любом случае, это по-прежнему не является 100-процентным потокобезопасным
var
LNotify: TNotifyEvent;
begin
...
LNotify := FOnNotify;
if Assigned(LNotify) then
LNotify(Self);
end;