Вопрос по – Делает ли Delphi переменную перед созданием объекта?

6

Делает ли Delphi переменную экземпляра до полного создания объекта?

Другими словами, учитывая переменную:

var
   customer: TCustomer = nil; 

Затем мы создаем клиента и присваиваем его переменной:

customer := TCustomer.Create;

Возможно ли, чтоcustomer может быть неnil, но не указывают на полностью построенныйTCustomer?

Это становится проблемой при выполнении отложенной инициализации:

function SacrifialCustomer: TCustomer;
begin
   if (customer = nil) then
   begin
      criticalSection.Enter;
      try
         customer := TCustomer.Create;
      finally 
         criticalSection.Leave;
      end;
   end;
   Result := customer;
end;

Ошибка в строке:

if (customer = nil) 

Возможно, что другой поток вызывает:

customer := TCustomer.Create;

и переменной присваивается значение до начала строительства, Это заставляет потокassume тотcustomer является допустимым объектом просто потому, что переменная назначена.

Может ли эта многопоточная ошибка синглтона произойти в Delphi (5)?

Bonus Question

Есть ли принятый, потокобезопасный,однократная инициализация шаблон дизайна для Delphi? Многие люди реализовалиsingletons в Delphi путем переопределенияNewInstance а такжеFreeInstance; их реализации потерпят неудачу в нескольких потоках.

Строго говоря, я не после ответа о том, как реализовать иsingleton, ноlazy-initialization, В то время какsingletons Можноuse ленивая инициализация,lazy initialization не ограничивается одиночками.

Update

Два человека предложили ответэто содержит распространенную ошибку.broken double-checked locking algorithm translated to Delphi:

// Broken multithreaded version
// "Double-Checked Locking" idiom
if (customer = nil) then
begin
   criticalSection.Enter;
   try
      if (customer = nil) then
         customer := TCustomer.Create;
   finally
      criticalSection.Leave;
   end;
end;
Result := customer;

ОтВикипедия:

Intuitively, this algorithm seems like an efficient solution to the problem. However, this technique has many subtle problems and should usually be avoided.

Еще одно ошибочное предложение:

function SacrificialCustomer: TCustomer;
var
  tempCustomer: TCustomer;
begin
   tempCustomer = customer;
   if (tempCustomer = nil) then
   begin
      criticalSection.Enter;
      try
         if (customer = nil) then
         begin
            tempCustomer := TCustomer.Create;
            customer := tempCustomer;
         end;
      finally
         criticalSection.Leave;
      end;
   end;
   Result := customer;
end;

Update

Я создал некоторый код и посмотрел на окно процессора. Кажется, что этот компилятор с моими настройками оптимизации в этой версии Windows, с этим объектом, сначала создает объект,then назначает переменную:

customer := TCustomer.Create;
       mov dl,$01
       mov eax,[$0059d704]
       call TCustomer.Create
       mov [customer],eax;
Result := customer;
       mov eax,[customer];

Конечно, я не могу сказать, что это всегда гарантировано.

@ BorisTreukhov Этот принятый код может страдать от одной и той же проблемы в зависимости от поведения компилятора. Ian Boyd
@ IanBoyd - я изменил вашу реализацию Багги временная переменная подсказка к тому, что я имел в виду (надеюсь, ты не против). Скорее всего, вы правы в том, что это все еще имеет проблемы, но, как я уже упоминал в комментариях, я их не понимаю. Lieven Keersmaekers
@ Ливен Ну, я ушел, потому что слишком зависим. Тем не менее, дважды проверил блокировку, как реализованоhatchet безопасен на всех известных компиляторах Delphi в Windows на x86 и x64 благодаря модели памяти на этих платформах. Я ожидаю, что это будет безопасно на Mac OS на x86, но не знаю. David Heffernan
stackoverflow.com / questions / 4475080 / как-дважды-проверять-блокировать-быть-реализованным-в-delphi Я думаю, что этот вопрос скорее о двойной проверке идиомы блокировки в delphi, чем о инициализации переменной. Boris Treukhov
Да, в самом деле :-(. / Stackoverflow.com вопросы / 232075 / ... Boris Treukhov

Ваш Ответ

4   ответа
9

Как я могу, используя Delphi 5, нацеленную на аппаратное обеспечение x86, реализовать поточно-ориентированную ленивую инициализацию синглтона.

Насколько я знаю, у тебя есть три варианта.

1. Используйте замок

function GetCustomer: TCustomer;
begin
  Lock.Acquire;
  try
    if not Assigned(Customer) then // Customer is a global variable
      Customer := TCustomer.Create;
    Result := Customer;
  finally 
    Lock.Release;
  end;
end;

Недостатком этого является то, что если есть спор оGetCustomer тогда сериализация блокировки будет препятствовать масштабированию. Я подозреваю, что люди беспокоятся об этом гораздо больше, чем необходимо. Например, если у вас есть поток, который выполняет большую работу, этот поток может взять локальную копию ссылки на синглтон, чтобы уменьшить конкуренцию.

procedure ThreadProc;
var
  MyCustomer: TCustomer;
begin
  MyCustomer := GetCustomer;
  // do lots of work with MyCustomer
end;

2. Двойная проверка блокировки

Это метод, который позволяет вам после создания синглтона избежать конфликта блокировок.

function GetCustomer: TCustomer;
begin
  if Assigned(Customer) then
  begin
    Result := Customer;
    exit;
  end;

  Lock.Acquire;
  try
    if not Assigned(Customer) then
      Customer := TCustomer.Create;
    Result := Customer;
  finally 
    Lock.Release;
  end;
end;

Двойная проверка блокировки - это техника с довольно изменчивой историей. Самая известная дискуссия это Декларация "Двойная проверка заблокирована". Это устанавливается в основном в контексте Java, и описанные проблемы не относятся к вашей ситуации (компилятор Delphi, аппаратное обеспечение x86). Действительно, для Java с появлением JDK5 мы можем теперь сказать, что двойная проверка блокировки зафиксирована.

Delphi-компилятор не переупорядочивает запись в синглтон-переменную относительно конструкции объекта. Более того, сильная модель памяти x86 означает, что переупорядочение процессора не нарушает этого. Видеть Кто заказывал заборы памяти на x86?

Проще говоря, двойная проверка блокировки не нарушена на Delphi x86. Более того, модель памяти x64 также является надежной, и блокировка с двойной проверкой там тоже не сломана.

3. Сравните и поменяйте местами

Если вы не возражаете против возможности создания нескольких экземпляров одноэлементного класса, а затем отбрасывания всех, кроме одного, вы можете использовать сравнение и обмен. Последние версии VCL используют эту технику. Это выглядит так:

function GetCustomer;
var
  LCustomer: TCustomer;
begin
  if not Assigned(Customer) then 
  begin
    LCustomer := TCustomer.Create;
    if InterlockedCompareExchangePointer(Pointer(Customer), LCustomer, nil) <> nil then
      LCustomer.Free;
  end;
  Result := Customer;
end;
Также есть варианты «Занято-Ожидание инициализации», проверьте мой ответ! GJ.
Я воспользуюсь этим. Хотя, ради клиентов, работающих под управлением Windows 2000, я буду использоватьInterlockedCompareExchange - но техника та же. Это наносит ущерб, когда этоICustomer вместо тогоTCustomer; но это ничего, чтоAddRef не могу исправить. Ian Boyd
Да, на D5,InterlockedCompareExchange Это хорошо.InterlockedCompareExchangePointer нужен на x64. David Heffernan
ЯвляетсяInterlockedCompareExchange не подходит для 32-разрядных исполняемых файлов, даже для 64-разрядных версий Windows? Ian Boyd
InterlockedCompareExchange подходит для 32-битных указателей. Ваши указатели имеют ширину 32 бита. David Heffernan
6

у вас все еще остается та же проблема. Если два потока попадают в SacrifialCustomer практически одновременно, оба могут выполнить тестif (customer = nil) до того, как один из них войдет в критическую секцию.

Одним из решений этой проблемы является двойная проверка блокировки (проверьте снова после входа в критическую секцию). С Delphi это работает на некоторых платформах, но не гарантируется работа на всех платформах. В других решениях используется статическая конструкция, которая работает во многих языках (не уверен в Delphi), потому что статическая инициализация происходит только при обращении к классу, поэтому она в действительности ленива, а статические инициализаторы по своей природе безопасны для потоков. Другой - использование блокированного обмена, который объединяет тестирование и присваивание в элементарную операцию (пример Delphi см. Во втором ответе здесь: Как должна быть реализована «Двойная проверка блокировки» в Delphi?).

Этот код рухнетесл Delphi выполняет присваивание переменной перед созданием объекта.if (customer=nil) ложно, за исключением того, что это недопустимый объект клиента. Ian Boyd
@ IanBoyd - у меня здесь не работает Delphi, но вы наверняка можете убедиться в этом, глядя на код ассемблера, наблюдая, как переменная клиента получает значение. Один верный способ предотвратить эту проблему - всегда входить в критическую секцию и жить с влиянием производительности. Lieven Keersmaekers
@ IanBoyd - кстати, вы читали ссылку, предоставленную Борисом? Механизм блокировки с двойной проверкойможе будет сломан в зависимости от используемых моделей памяти или (Я бы поверил) специфичные для компилятора реализации (например, назначение клиенту до завершения строительства (любить иронию:)). Lieven Keersmaekers
@ Lieven OnОбратная сторон: "Поскольку синхронизация метода может снизить производительность в 100 и более раз, [3] издержки на получение и снятие блокировки при каждом вызове этого метода кажутся ненужными: после завершения инициализации получение и освобождение блокировок показаться ненужным. " Ian Boyd
@ Lieven Настоящая проблема в том, что я не хотел бы пытаться решить проблему многопоточного синглтона - только заново изобретать те же ошибки, которые уже были решены. Ian Boyd
5

Delphi не присваивает значение целевой переменной до возврата конструктора. Большая часть библиотеки Delphi Полагается по этому факту. (Поля объектов инициализируются равными нулю; необработанное исключение в конструкторе объекта вызывает его деструктор, который, как ожидается, вызоветFree на всех полях объекта, которые назначал конструктор. Если бы эти поля имели ненулевые значения, то возникли бы дополнительные исключения.)

Я предпочитаю не отвечать на вопрос о бонусе, потому что он не связан с основным вопросом, и потому что это гораздо более обширная тема, чем уместная запоздалая мысль.

Иногда люди не хотят отвечать на вопрос, а вместо этого хотят ответить на «реальную» ситуацию, стоящую за вопросом. Это не один из тех времен. На этот раз, однако, я бы хотел, чтобы у меня было решение моей «настоящей» проблемы. я вниз по кроличьей норе; память баров, переупорядочивание операций памяти, NUMA, сброс кеша. Я просто хотел синглтон: Ian Boyd
Никакой кроличьей норы здесь. Двойная проверка блокировки работает на x86 и x64. Это не Java. Это Дельфи. В любом случае блокировка с двойной проверкой больше не нарушается в Java, а volatile в JDK 5. David Heffernan
По крайней мере, это не для меня. Все остальные, похоже, стремятся проигнорировать главный вопрос и сразу перейти к более сложному. Но если ты этого хочешь, Ян, тогда спросичт вопрос. Вы могли бы остановиться после мотивирующего примера (часть между основным вопросом и «бонусным»). Это адекватно показывает, почему вы заинтересованы в порядке операций. Если переменная назначена раньше, она не будет работать; если он назначен только после построения, он будет работать в однопоточных программах. Он сломан в многопоточных программах с двойной проверкой или без нее. Rob Kennedy
1

customer указатель как переменная атомарной блокировки, которая предотвращает создание нескольких объектов. Подробнее о вы можете прочитать на Занятая Ожидание Инициализация Читайте также: На оптимистической и пессимистической инициализации

Похожие вопросы