У Алены в посте про явные и скрытые особенности volatile-переменных в C++ идет обсуждение того, почему этот volatile необходим в многопоточных приложениях. И я задумался, почему его нет в Delphi, и как оно вообще без него работает.

В частности есть совершенно классическая модель работы thread'а, когда он что-то делает в цикле, и периодически проверяет, не надо ли экстренно прерваться:

procedure Execute;
begin
  while not Terminated do
  begin
    ...
  end;
end;

Так вот в C++, если эта самая Terminated — обычная булевская переменная, то компилятор может внести оптимизацию. Видя, что переменная не меняется в цикле, он считает ее один раз в регистр и не будет каждый раз лезть за ней в память. И получается, что попытки изменить ее извне thread'а ничего не изменят, цикл будет крутиться вечно. Чтобы такого не происходило, переменную объявляют как volatile, что заставляет компилятор не применять к ней таких оптимизаций.

В Delphi такого слова нет. И компилятор у Delphi весь из себя тоже оптимизирующий, и я где-то даже определенно встречал, что переменные из циклов он исключать умеет. И тем не менее, хоть я и писал довольно много многопоточного софта, но ни разу таких проблем не встречал. Почему?

У меня появилась одна теория. Ведь Terminated в Delphi — это не переменная, а метод, который читает пирватную FTerminated у класса TThread. И устанавливается она тоже не напрямую, а через метод Terminate. "Ага!" — подумал я, и написал пример, где вместо Terminated используется простая булевская переменная. Все равно работает!

Вот не знает ли кто, как Delphi обходится без volatile? Очень интересно...

Комментарии: 7 (особо ценных: 1)

  1. Alena

    Чтобы такого не происходило, переменную объявляют как volatile, что заставляет компилятор не применять к ней таких оптимизаций.

    Хочу добавить, что мой пост был в том числе и о том, что доступ к переменной из разных потоков - это некорректное использование volatile. Несмотря на то, что встречается оно довольно часто.

  2. AL-one

    (Хоть и не в тему) А автор не боится, что сейчас начнется "холли-вар" между делфистами и сиплюсплюсниками ? :)
    (В тему) Мне кажется, что делфийский оптимизатор достаточно умен, чтобы не трогать условия выхода из цикла.

  3. Elf

    Хм.. А я как-то и не задумывался никогда про компилятор и его особенности. Просто делал многопоточные программы и всё :-)
    Правда, и C++ я не знаю. Хотя, похоже, хорошо что не знаю, если там программист должен заботливо рассказывать компилятору сказки, что можно, а что нельзя делать с его кодом :)

    P.S. Это только у меня такие мелкие шрифты в комментах? Читать невозможно абсолютно. Только кнопкой "+" и спасаюсь... :)

  4. Иван Сагалаев

    Проверил идею с условием выхода — не оно. В треде сделал такой цикл:

    var
      Value:String;
    
    ...
    
    while not Terminated do
    begin
      Form1.Label1.Caption:=Value;
    end;
    

    Переменная Value — глобальная внутри модуля (под implementation определена). Меняю ее извне треда — начинает присваиваться новое значение.

    Вот у меня другая идея возникла. Похоже, делфийский компилятор делает цикловые оптимизации только для локальных переменных в функции, что безопасно, потому что их извне поменять просто нельзя.

    P.S. Холиваров не боюсь :-). Не будет их.

  5. AL-one

    Скорее и то, и то. Т.е. компилятор не трогает нелокальные переменные и не трогает переменные в условиях цикла. Хотя есть такой пример:

    for i := 5 to 10 do
      OtherProc;
    

    Вот здесь переменная i пробегает значения от 6 до 0, т.к. она не используется внутри цикла, а с нулем сравнивать легче. Для нее даже не заводится место в стеке, а используется регистр процессора.

    Но опять-таки компилятор не тронет счетчик цикла, если он используется внутри процедуры OtherProc.

  6. enternet

    Особо ценный комментарий

    2 Иван Сагалаев:
    Именно так. Компилятор в данном случае оптимизирует использование только локальных переменных.
    В общем случае - компилятор дельфийный достаточно "туп". Оптимизации там минимум. Я, например, несколько раз занимался декомпиляцией в исходный код 8-) Ответственно заявляю - похоже на табличную подстановку. НО! Не вижу ничего плохого в таком подходе. Из-за ограниченного набора используемых команд и их примитивной группировки дельфийный код выполняется современными процессорами замечательно быстро - конвейеры процессора загружаются как нужно. Это во времена 486 наблюдались проблемы с таким подходом. Но уже начиная с первых пентиумов (2 конвейера) правила игры кардинально поменялись. А с современной точки зрения оптимизация вовсе нафиг не нужна - её делают сами процессоры. 8-)

  7. John

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

    program Project1;
    {$APPTYPE CONSOLE}
    uses
      SysUtils,Windows;
    var
      global:boolean;
          p:integer;
      threadID:cardinal;
      threadH:THandle;
    type
      pint=^integer;
    function testThread(p:pint):integer;stdcall;
    begin
      //global:=true;//#1
    end;
    begin
      global:=false;
      threadH:=CreateThread(nil,0,@testThread,@p,0,threadID);
      while not global do;//#2
      writeln('That''s all, folks!');
      CloseHandle(threadH);
    end.
    

    Если #1 за-комментировано, в месте #2 Delphi генерирует:

    Project1.dpr.20: while not global do;//#2
    0040916C 84DB             test bl,bl
    0040916E 74FC             jz $0040916c
    

    Если же #1 не за-комментировано, то:

    Project1.dpr.20: while not global do;//#2
    00409170 803D20E2400000   cmp byte ptr [$0040e220],$00
    00409177 74F7             jz $00409170
    

    Эх, ну никого воображения.. зато вот ромашки дёргать все умеют- в регистре, не в регистре, в регистре, не в регистре.. ещё к гадалке сходите..

Добавить комментарий