Зависание потоков при использовании критических секции

Вопросы программирования и использования среды Lazarus.

Модератор: Модераторы

Зависание потоков при использовании критических секции

Сообщение Sasha » 21.10.2017 22:55:10

Не могу понять, почему зависает основной поток при попытке войти в критическую секцию (или при захвате семафора), казалось бы он должен встать
в очередь, вместо этого о виснет.
Программа очень проста, поток увеличивает счётчик внутри критической секции, главный поток выводит значение счётчика, внутри критической секции, но обламывается
при попытке войти в неё. Причём, что интересно если внутри потока убрать Sleep, то заход в критическую секцию выполняется без проблем.
Вот код.
Код: Выделить всё
unit Unit1;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, FileUtil, Forms, Controls, Graphics, Dialogs, StdCtrls;


type

  TMyThread=class(TThread)
  private
  protected
    procedure Execute; override;
  public
    Counter:Integer;
  end;


  { TForm1 }

  TForm1 = class(TForm)
    Button1: TButton;
    Button2: TButton;
    Button3: TButton;
    Memo1: TMemo;
    procedure Button1Click(Sender: TObject);
    procedure Button2Click(Sender: TObject);
    procedure Button3Click(Sender: TObject);
    procedure FormClose(Sender: TObject; var CloseAction: TCloseAction);
    procedure FormCreate(Sender: TObject);
  private
    FMyTh:TMyThread;
  end;


var
  Form1: TForm1;
  FCritSec:TRTLCriticalSection;
  MySem:Pointer;

implementation

{$R *.lfm}

{ TMyThread }

procedure TMyThread.Execute;
begin
  repeat
    EnterCriticalsection(FCritSec);
    //SemaphoreWait(MySem);
      Inc(Counter);
      WriteLn(Format('Counter=%d',[Counter]));
      Sleep(100);
    //SemaphorePost(MySem);
    LeaveCriticalsection(FCritSec);
  until CheckTerminated;
end;

{ TForm1 }

procedure TForm1.Button1Click(Sender: TObject);
begin
  FMyTh:=TMyThread.Create(True);
  FMyTh.FreeOnTerminate:=True;
  FMyTh.Start;
end;

procedure TForm1.Button2Click(Sender: TObject);
begin
  EnterCriticalsection(FCritSec);
  //SemaphoreWait(MySem);

  Memo1.Lines.Append(Format('Counter=%d',[FMyTh.Counter]));
  Sleep(5000);

  //SemaphorePost(MySem);
  LeaveCriticalsection(FCritSec);
end;

procedure TForm1.Button3Click(Sender: TObject);
begin
  if Assigned(FMyTh) then
   FMyTh.Terminate;
end;

procedure TForm1.FormClose(Sender: TObject; var CloseAction: TCloseAction);
begin
  Button3Click(nil);
  DoneCriticalsection(FCritSec);
  SemaphoreDestroy(MySem);
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
  InitCriticalSection(FCritSec);
  MySem:=SemaphoreInit;
  SemaphorePost(MySem);
end;


end.
Sasha
новенький
 
Сообщения: 41
Зарегистрирован: 07.12.2015 01:27:43

Re: Зависание потоков при использовании критических секции

Сообщение скалогрыз » 21.10.2017 23:50:53

Пфф... твой поток-счётчик, при запуске - занимает критическую секцию и не отпускает её большую часть времени! А большую часть времени твой поток спит.
Когда твой поток не спит, то он дейтсивтельно освобождает секцию, но быстренько меняет переменные, захватывает секцию и снова запысает.

А лекарство простое - не спать с занятыми объектами синхронизации.
Код: Выделить всё

procedure TMyThread.Execute;
begin
  repeat
    EnterCriticalsection(FCritSec);
    //SemaphoreWait(MySem);
       Inc(Counter);
       WriteLn(Format('Counter=%d',[Counter])); // не стоит часто баловаться консолью из потока
    LeaveCriticalsection(FCritSec);
      Sleep(100);
  until CheckTerminated;
end;

procedure TForm1.Button2Click(Sender: TObject);
begin
  EnterCriticalsection(FCritSec);
  //SemaphoreWait(MySem);

  Memo1.Lines.Append(Format('Counter=%d',[FMyTh.Counter]));
  //  Sleep(5000); // спать в основном потоке,  вредно для здоровья!
  // вдруг ты с конечным пользователем повстречаешься случайно в реальной жизни!

  //SemaphorePost(MySem);
  LeaveCriticalsection(FCritSec);
end;

(Спать с занятыми объектами синхронизации можно, но только если ты сотрудник майкрософт!)

P.S. а потом у тебя будут непонятные ошибки при закрытии формы, но это совсем другая проблема.
скалогрыз
долгожитель
 
Сообщения: 1803
Зарегистрирован: 03.09.2008 02:36:48

Re: Зависание потоков при использовании критических секции

Сообщение Sasha » 22.10.2017 00:26:56

Со Sleep после выхода из критической секции работает, но тогда получается, что у объектов синхронизации нет никаких очередей в которые могут выстроиться потоки?

Sleep тут просто для примера стоит, проблема возникает в реальном проекте, там аналогом Sleep`а является чтение через сокета.
Про майкрософт не очень понял, если это намёк, что в винде это будет работать, то нифига, прога одинаково себя ведёт в Линуксе и в Винде.
P.S. При закрытии формы кстати проблем вообще никаких.
Sasha
новенький
 
Сообщения: 41
Зарегистрирован: 07.12.2015 01:27:43

Re: Зависание потоков при использовании критических секции

Сообщение скалогрыз » 22.10.2017 01:04:28

Sasha писал(а):Со Sleep после выхода из критической секции работает, но тогда получается, что у объектов синхронизации нет никаких очередей в которые могут выстроиться потоки?

не понимаю эту фразу.
Очереди не выстраиваются в потоки, это потоки выстраиваются в очередь (ожидания).

Смотри что происходит:
* Твой поток-счётчик, запускается и забирает критическую секцию (ну и спит с ней).
* В основном потоке, приходит сообщение: "Нажата кнопка".
* Основной поток идёт к крит-секции и ждёт пока она освободится (т.е. пока спит поток-счётчик).
* После захвата крит-секции он засыпает на 5 секунд (что вызывает эффект "залипания" т.к. другие сообщения не обрабатываются).
* (к этому времени, поток-счётчик уже ждёт своей очереди на сон, ожидая крит-секцию)
* Поспав пять секунд, основной поток возвращается в норму.
Т.е. программа не виснет насовсем, а только подвисает, на 5-ти секундные периоды?

Ммм... кстати, а вот про writeln() - у тебя же консоль включена? а то там интересные вещи могут происходить, если консоли нет, а writeln() есть.
"интересные вещи" лечатся вот так:
Код: Выделить всё
procedure TMyThread.Execute;
begin
  repeat
    EnterCriticalsection(FCritSec);
    try
      //SemaphoreWait(MySem);
        Inc(Counter);
        WriteLn(Format('Counter=%d',[Counter]));
        Sleep(100);
      //SemaphorePost(MySem);
    finally
      LeaveCriticalsection(FCritSec);
    end;
  until CheckTerminated;
end;

ну или так:
Код: Выделить всё
procedure TMyThread.Execute;
begin
  repeat
    EnterCriticalsection(FCritSec);
    try
      //SemaphoreWait(MySem);
        Inc(Counter);
        WriteLn(Format('Counter=%d',[Counter]));
        Sleep(100);
      //SemaphorePost(MySem);
    except
    end;
    LeaveCriticalsection(FCritSec);
  until CheckTerminated;
end;
скалогрыз
долгожитель
 
Сообщения: 1803
Зарегистрирован: 03.09.2008 02:36:48

Re: Зависание потоков при использовании критических секции

Сообщение Sasha » 22.10.2017 01:41:41

Может я плохо объяснился, но то как Вы описали работу критических секций, так я их и понимаю.
Потоки должны выстраиваться в очередь, но как раз эта тестовая программа показывает, что этого не происходит.
Я могу отличить зависание на 5 секунд от зависания на неопределённое время (в винде если очень долго подождать, то главный поток всё же входит в критическую секцию).
Скомпильте программу у себя, и проверьте как она в действительности работает. При нажатии на кнопку окно виснет, а поток совершенно спокойно выдаёт в терминал очередное значение счётчика, и выдаёт и выдаёт и выдаёт.
Sasha
новенький
 
Сообщения: 41
Зарегистрирован: 07.12.2015 01:27:43

Re: Зависание потоков при использовании критических секции

Сообщение скалогрыз » 22.10.2017 02:05:06

Sasha писал(а):При нажатии на кнопку окно виснет, а поток совершенно спокойно выдаёт в терминал очередное значение счётчика, и выдаёт и выдаёт и выдаёт.

тогда вот такой вот вариант:
Код: Выделить всё
procedure TMyThread.Execute;
begin
  repeat
    EnterCriticalsection(FCritSec);
    //SemaphoreWait(MySem);
      Inc(Counter);
      WriteLn(Format('Counter=%d',[Counter]));
      Sleep(100);
    //SemaphorePost(MySem);
    LeaveCriticalsection(FCritSec);
    Sleep(5); // если заработает, то попробуй Sleep(1) а потом Sleep(0)
  until CheckTerminated;
end;
скалогрыз
долгожитель
 
Сообщения: 1803
Зарегистрирован: 03.09.2008 02:36:48

Re: Зависание потоков при использовании критических секции

Сообщение Sasha » 22.10.2017 02:21:11

Такое я пробовал, так работает, но это не выход.
Любой sleep даёт существенную задержку (особенно в винде).
Самое странное во всём этом, это то, что если sleep в потоке убрать, то основной поток совершенно спокойно захватывает критическую секцию, спит 5 (задерживая поток-счётчик), выходит, и поток-счётчик продолжает считать и выводить, т.е. механизм очередей вроде как работает, но если вернуть слип, то механизм вставания потоков в очередь перестаёт работать.
Sasha
новенький
 
Сообщения: 41
Зарегистрирован: 07.12.2015 01:27:43

Re: Зависание потоков при использовании критических секции

Сообщение скалогрыз » 22.10.2017 02:36:17

Sasha писал(а):Любой sleep даёт существенную задержку (особенно в винде).

хех, поведение Sleep(0) менялось от винды к винде.
То что ты ищешь, это SwitchToThread
Код: Выделить всё
procedure TMyThread.Execute;
begin
  repeat
    EnterCriticalsection(FCritSec);
    //SemaphoreWait(MySem);
      Inc(Counter);
      WriteLn(Format('Counter=%d',[Counter]));
      Sleep(100);
    //SemaphorePost(MySem);
    LeaveCriticalsection(FCritSec);
    SwitchToThread;
  until CheckTerminated;
end;


проблема в том, что ты ожидаешь "очереди" по типу "первый пришёл, первый вышел". Но критические секции очерёдность не гарантируют.
(я вообще не знаю ни одного объекта синхронизации, который гарантировал бы очерёдность. Очередь - это хороший такой расход памяти, с необходимостью синхронизации)

То что происходит у тебя, так это поток-счётик, поспал (что дало возможность основному залочиться). Потом проснулся, вышел из секции. Но его квант времени работы не закончился (и совсем не факт, что основной поток проснётся в этом же время. Это вопрос уже к ОСи, как она будет распоряжаться жезелом).
По-этому поток-счётчик, закончил цикл, и залочил критическую секцию снова (она же уже свободна!), после чего успешно уснул.
(в итоге основной поток, получивь время на исполнение, проверил критическую секцию и продолжил ждать)
Освобождение критической секции <> освобождение кванта времени.

Sasha писал(а):Самое странное во всём этом, это то, что если sleep в потоке убрать, то основной поток совершенно спокойно захватывает критическую секцию, спит 5 (задерживая поток-счётчик), выходит, и поток-счётчик продолжает считать и выводить, т.е. механизм очередей вроде как работает, но если вернуть слип, то механизм вставания потоков в очередь перестаёт работать.

это вопрос к Оси, как она занимается расспределением кванта времени на потоки.
Код который ты привёл выше (со слипами), не работает у тебя. На другой машине, на другой ОСи он может вполне успешно заработать.

Но на самом деле вопрос к коду. Тебе не следует спать/выполнять что-либо трудоёмкое в критической секции. Код внутрни критической секции должен занимать минимально возможное время.
скалогрыз
долгожитель
 
Сообщения: 1803
Зарегистрирован: 03.09.2008 02:36:48

Re: Зависание потоков при использовании критических секции

Сообщение Sasha » 22.10.2017 03:00:43

Функции SwitchToThread, я не нашёл, есть ThreadSwitch, но работает коряво, главный поток может ждать 10, 20 секунд (может и дольше, не стал дальше смотреть), но потом всё же достучится до критической секции, а поток-счётчик, продолжает себе работать. Короче на работает принудительная смена потока.
По поводу разных осей, проверял на разных осях (Линукс, Win7, WinXP). Тут проблема не в ОС, такой же код на Delphi работает так как ожидается, так что это что-то в реализации FPC.
Если критические секции сделаны для синхронизации доступа к данным, то на их работу не должно влиять то, сколько времени я эти данные получаю, я же понимаю, что не стоит т.о. синхронизировать скажем 100 потоков, но для синхронизации 2-х 3-х потоков такое использование вполне нормально.
Sasha
новенький
 
Сообщения: 41
Зарегистрирован: 07.12.2015 01:27:43

Re: Зависание потоков при использовании критических секции

Сообщение скалогрыз » 22.10.2017 03:45:03

Sasha писал(а):Функции SwitchToThread, я не нашёл

Код: Выделить всё
function  SwitchToThread () : LongBool; stdcall; external 'kernel32' name 'SwitchToThread';

procedure TMyThread.Execute;
begin
  repeat
    EnterCriticalsection(FCritSec);
    //SemaphoreWait(MySem);
      Inc(Counter);
      WriteLn(Format('Counter=%d',[Counter]));
      Sleep(100);
    //SemaphorePost(MySem);
    LeaveCriticalsection(FCritSec);
    SwitchToThread;
  until CheckTerminated;
end;

Sasha писал(а):Тут проблема не в ОС, такой же код на Delphi работает так как ожидается, так что это что-то в реализации FPC.

по-моему, вывод напрашивается сам собой ;)
скалогрыз
долгожитель
 
Сообщения: 1803
Зарегистрирован: 03.09.2008 02:36:48

Re: Зависание потоков при использовании критических секции

Сообщение Sasha » 22.10.2017 03:52:07

Виндовые решения не подходят. Реальная программа работает на машине с линуксом.
Судя по описанию работы критической секции, очередь из одного элемента там обязана быть.
Похоже, что в FPC какая-т косячная реализация :( .
Sasha
новенький
 
Сообщения: 41
Зарегистрирован: 07.12.2015 01:27:43

Re: Зависание потоков при использовании критических секции

Сообщение скалогрыз » 22.10.2017 06:24:43

а ты на последней делфи тестируешь под линуксом? или в Kylix-e?

ThreadSwitch реализован через sched_yield
Критическая секция через pthread_mutex.
Чисто ОС API, ничего от себя нет.

Sasha писал(а):Судя по описанию работы критической секции, очередь из одного элемента там обязана быть.

где такое написано?

правильно ли я тебя понимаю, что вот такой код:
Код: Выделить всё
procedure SomeFunc;
begin
  EnterCriticalSection(cs);
  ...do something ...
  LeaveCriticalSection(cs); // 1
  ...
  EnterCriticalSection(cs); // 2
  ...do something else...
  LeaveCriticalSection(cs);
end;

гарантирует тот факт, что EnterCriticalSection 2 произойдёт только после того как отработал соседний поток ожидающий cs?

проблема в коде. Исключать нужно короткие (по времени исполнения) куски кода.
скалогрыз
долгожитель
 
Сообщения: 1803
Зарегистрирован: 03.09.2008 02:36:48

Re: Зависание потоков при использовании критических секции

Сообщение olegy123 » 22.10.2017 08:14:23

Пока работает Sleep - другие отдыхаю..

по логике..
стоишь на остановке ждешь своего транспорта.. а водитель решил поспать.. пока он не высыпется - никуда не уедешь.

Добавлено спустя 10 минут 35 секунд:
Sasha писал(а):Похоже, что в FPC какая-т косячная реализация

на си точно также будет работать

Добавлено спустя 5 минут 9 секунд:
Sasha писал(а):Может я плохо объяснился, но то как Вы описали работу критических секций, так я их и понимаю.

Не понимаешь - у тебя один поток сильно зависит от другого.. Как поршни в двигателе - клинит один поршень на время - все остальные никуда не двигаются.
olegy123
долгожитель
 
Сообщения: 1643
Зарегистрирован: 25.02.2016 12:10:20

Re: Зависание потоков при использовании критических секции

Сообщение Sasha » 22.10.2017 13:30:48

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

"гарантирует тот факт, что EnterCriticalSection 2 произойдёт только после того как отработал соседний поток ожидающий cs?" - Если честно, не очень понял этот вопрос.
Вы писали:
"
* Твой поток-счётчик, запускается и забирает критическую секцию (ну и спит с ней).
* В основном потоке, приходит сообщение: "Нажата кнопка".
* Основной поток идёт к крит-секции и ждёт пока она освободится (т.е. пока спит поток-счётчик).
* После захвата крит-секции он засыпает на 5 секунд (что вызывает эффект "залипания" т.к. другие сообщения не обрабатываются).
* (к этому времени, поток-счётчик уже ждёт своей очереди на сон, ожидая крит-секцию)
* Поспав пять секунд, основной поток возвращается в норму.
"
я с этим согласен, именно такого поведения я и ожидал, проблема в " Основной поток идёт к крит-секции и ждёт пока она освободится (т.е. пока спит поток-счётчик). ", основной поток никак не может дождаться критической секции не смотря на то, что поток-счётчик уходит в Sleep, что означает, что у основного потока куча времени на то, чтобы захватить КС.

" Исключать нужно короткие (по времени исполнения) куски кода." - что это значит? Как я уже писал, если внутри потока счётчика убрать Sleep (т.е. поток-счётчик только и будет входить в КС, увеличивать счётчик, выводить его, выходить из КС) , то приведённый в самом начале код, работает как ожидается.

"Пока работает Sleep - другие отдыхаю.." - поработает Sleep, другие работают. Sleep для каждого потока свой.
" у тебя один поток сильно зависит от другого.." - Тут как раз наоборот, поток-счётчик никак не хочет быть зависимым от основного потока, потому, что при попытке входа основным потоком в КС, поток счётчик работает так, как будто никто другой и не пытается войти в КС.

Добавлено спустя 10 минут 34 секунды:
Вот http://rsdn.org/article/baseserv/critsec.xml где написано. Там сказано, что поток который выходит из КС, сам проверяет наличие попытки другого потока войти в КС, если такая попытка была (пока он занимался своими делами, к примеру тупо спал), то поток хозяин КС выставляет объект синхронизации в сигнальное состояние, и поток который ожидал этот сигнальный объект пробуждается.

Добавлено спустя 5 минут 44 секунды:
Код: Выделить всё
  repeat
    CritSec.Acquire;
      Inc(Counter);
      WriteLn(Format('Counter=%d',[Counter]));
      Sleep(100);
    CritSec.Release;
    [b]SwitchToThread;[/b]
  until Self.Terminated;

Использование SwitchToThread; тоже не помогает (попробовал в Винде), как обычно, основной поток висит на ожидании КС, поток-счётчик, считает, выводит и не парится.

Добавлено спустя 24 минуты 13 секунд:
Вот другой вариант на Дельфи.
Код: Выделить всё
unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, SyncObjs, ExtCtrls;

type
  TSimpleThread = class(TThread)
  protected
    procedure Execute; override;
  public
    Delta:Integer;
  end;


  TForm1 = class(TForm)
    Button1: TButton;
    Button2: TButton;
    Memo1: TMemo;
    Button3: TButton;
    Timer1: TTimer;
    Button4: TButton;
    procedure Button1Click(Sender: TObject);
    procedure Button3Click(Sender: TObject);
    procedure Button2Click(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    procedure FormClose(Sender: TObject; var Action: TCloseAction);
    procedure Timer1Timer(Sender: TObject);
    procedure Button4Click(Sender: TObject);
  private
    FSTh1:TSimpleThread;
    FSTh2:TSimpleThread;
  end;

var
  Form1: TForm1;
  CritSec:TCriticalSection;
  Counter:Integer;

implementation

{$R *.dfm}

{ TSimpleThread }

procedure TSimpleThread.Execute;
begin
  repeat
    CritSec.Acquire;
    Counter:=Counter+Self.Delta;
    Sleep(100);
    CritSec.Release;
  until Self.Terminated;
end;


procedure TForm1.Button1Click(Sender: TObject);
begin
  FSTh1:=TSimpleThread.Create(True);
  FSTh1.FreeOnTerminate:=True;
  FSTh1.Delta:=1;

  FSTh2:=TSimpleThread.Create(True);
  FSTh2.FreeOnTerminate:=True;
  FSTh2.Delta:=-1;

  FSTh1.Resume;
  FSTh2.Resume;
end;

procedure TForm1.Button3Click(Sender: TObject);
begin
  FSTh1.Terminate;
end;

procedure TForm1.Button2Click(Sender: TObject);
begin
  CritSec.Acquire;
  Memo1.Lines.Append(Format('Counter=%d',[Counter]));
  Sleep(1000);
  CritSec.Release;
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
  CritSec:=TCriticalSection.Create;
end;

procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction);
begin
  CritSec.Free;
end;

procedure TForm1.Timer1Timer(Sender: TObject);
begin
  Memo1.Lines.Append(Format('Counter=%d',[Counter]));
end;

procedure TForm1.Button4Click(Sender: TObject);
begin
  FSTh2.Terminate;
end;

end.


Основной поток только выводит (по таймеру) значение счётчика в Memo. Один поток уменьшает счётчик, другой увеличивает. Казалось бы, если потоки используют одну критическую секцию, то значение счётчика не должно меняться, вместо этого счётчик постоянно меняется, значит один поток спокойно себе работает заходя в КС, меняя счётчик, и выходя из КС, а другой поток не может дождаться входа в КС, хотя другой поток постоянно захватывает и освобождает КС.
Sasha
новенький
 
Сообщения: 41
Зарегистрирован: 07.12.2015 01:27:43

Re: Зависание потоков при использовании критических секции

Сообщение serbod » 22.10.2017 15:39:33

Sasha
Для начала, CriticalSection это кабинка, в которой может находиться только один.

Если ты читаешь из сокета в отдельном потоке, то тебе не нужна "кабинка", если все операции с сокетом выполняются только внутри потока. В начале Execute() инициализируешь и открываешь сокет, а потом в цикле читаешь/пишешь. Чтобы цикл не был слишком коротким и не грузил процессор, можно в него добавить Sleep(1) или ожидание события/семафора с таймаутом. Но не полную блокировку, чтобы была возможность остановить поток через Terminate(). Данные передаешь в основной поток либо через Synchronize(), либо через thread-safe очередь сообщений.

А если у тебя какой-то общедоступный объект, например Canvas или STDOUT - то не следует занимать его надолго. Зашел в "кабинку", сделал свое дело и вышел. Никаких Sleep() и WaitFor(). Тем более в основном потоке.
Аватара пользователя
serbod
постоялец
 
Сообщения: 449
Зарегистрирован: 16.09.2016 11:03:02
Откуда: Минск

След.

Вернуться в Lazarus

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и гости: 40

Рейтинг@Mail.ru