Delphi — обновление глобальной строки из второго потока

#multithreading #delphi #delphi-xe

#многопоточность #delphi #delphi-xe

Вопрос:

Я экспериментирую с многопоточностью в Delphi (XE) и столкнулся с проблемой использования глобальной переменной между основным потоком VCL и вторым рабочим потоком.

Мой проект включает в себя 2-й рабочий поток, который сканирует некоторые файлы и обновляет строку globalvar с текущим именем файла. Затем этот globalvar распознается с помощью таймера в основном потоке VCL и обновляет строку состояния.

Однако я заметил, что иногда возникает «Недопустимая операция с указателем» … или «Не хватает памяти», или рабочий поток просто перестает отвечать (вероятно, взаимоблокировка).

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

 type
  TSyncThread = class(TThread)
  protected
    procedure Execute; override;
end;

var
  Form11: TForm11;
  ProgressString : String;
  ProgressCount : Int64;
  SyncThread : TSyncThread;
  CritSect : TRTLCriticalSection;

implementation

{$R *.dfm}

procedure TForm11.StartButtonClick(Sender: TObject);
begin
  Timer1.Enabled := true;
  SyncThread := TSyncThread.Create(True);
  SyncThread.Start;
end;

procedure TForm11.StopbuttonClick(Sender: TObject);
begin
  Timer1.Enabled := false;
  SyncThread.Terminate;
end;

procedure TForm11.Timer1Timer(Sender: TObject);
begin
  StatusBar1.Panels[0].Text := 'Count: '   IntToStr(ProgressCount);
  StatusBar1.Panels[1].Text := ProgressString;
end;

procedure TSyncThread.Execute;
var
  i : Int64;
begin
  i := 0;
  while not Terminated do begin
    inc(i);
    EnterCriticalSection(CritSect);
    ProgressString := IntToStr(i);
    ProgressCount := i;
    LeaveCriticalSection(CritSect);
  end;
end;

initialization
  InitializeCriticalSection(CritSect);
finalization
  DeleteCriticalSection(CritSect);
  

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

Мой вопрос в том, нужно ли запускать операцию чтения глобальной переменной в таймере VCL в критической секции? — если да, то почему?. Насколько я понимаю, это только чтение, и поскольку записи уже выполняются в критической секции, я не понимаю, почему это приводит к проблеме. Если я также помещаю чтение в таймере в критический раздел — это работает нормально …. но я недоволен, просто делая это, не зная почему!

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

Комментарии:

1. Знаете ли вы, что вы можете избавиться от блокировки и просто использовать целочисленную переменную для обмена данными между потоками? Это немного запутанно с 64-битным целым числом, но тривиально с 32-битным целым числом.

Ответ №1:

Строка Delphi выделяется в куче, это не статический буфер где-то. Сама переменная — это просто указатель. Когда ваш поток чтения обращается к строке, и в то же время эта самая строка освобождается другим потоком, происходят плохие вещи. Вы обращаетесь к уже освобожденной памяти, возможно, выделенной снова для чего-то другого и т.д.

Даже если эта строка была статическим буфером, операции обновления не являются атомарными, поэтому вы можете использовать поврежденную строку, которая обновляется в этот самый момент (половина новых данных и половина старых).

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

Комментарии:

1. Спасибо! — это был невероятно быстрый ответ, который подробно объясняет проблему — именно то, что я хотел. Мне всегда нравится знать, что происходит, а не просто слепо делать что-то, потому что это работает.