Kaptain.
Telegram /
LinkedIn /
Email /
GIT /
RSS /
GPG /
Заказ печатных плат

№ 4467 В разделе
Programming
от August 19th, 2012,
В подшивках: Lazarus, Pascal
Наверняка вы делаете не бесполезные программы и частенько они выполняют долгие операции. На столько долгие, что интерфейс замирает пока программу не “отпустит”. На помощь приходит многопоточность! Смысл в том, чтобы отделить долгоиграющую подпрограмму от основной программы и иногда синхронизировать потоки для обновления переменных или интерфейса. Именно это мне и пришлось сделать при доработке программы отправки команд на контроллер освещения через Bluetooth.
Демонстрация незамирающего окна:
Update (19.08.2012): В конце этой статьи есть дополнение о создании очередей и ожидании конца выполнения смежной задачи, а также видео для демонстрации.
У Windows и Linux разные способы работы с потоками, поэтому сначала придется наложить небольшой патч на файл проекта (например, myproject.lpr). По умолчанию uses выглядит так:
uses
{$IFDEF UNIX}{$IFDEF UseCThreads}
cthreads,
{$ENDIF}{$ENDIF}
Interfaces, // this includes the LCL widgetset
Нужно его изменить на такой вариант:
uses
{$IFNDEF WIN32}
{$DEFINE UseCThreads}
{$IFDEF UseCThreads}
cthreads,
{$ENDIF}{$ENDIF}
Interfaces, // this includes the LCL widgetset
Теперь можно создать класс нужного нам потока. В вашем любимом модуле сразу после uses добавьте описание нового класса:
Type TSerialThread = class(TThread) private fStatusText, fDataOut, fDataIn, fPortName, fStatus : String; fErrorCode : Integer; fStatusGet : Boolean; procedure Draw; procedure UpdateStatus; protected procedure Execute; override; public Constructor Create(CreateSuspended : boolean); end;
Чуть ниже идет определение глобальных переменных var. Добавьте в него SerThread : TSerialThread;.
Теперь нужно описать как работает новый класс. Согласно документации при создании класса можно определить некоторые параметры. Поскольку определять особо и нечего, то сделаем самый простой конструктор:
constructor TSerialThread.Create(CreateSuspended : boolean);
begin
FreeOnTerminate := True;
inherited Create(CreateSuspended);
end;
Одного конструктора мало. Должны быть функции обновления интерфейса программы:
procedure TSerialThread.Draw;
// Показать состояние устройства на форме
var
state: integer;
begin
//нарисовать ответ устройства на форме
state := Hex2Dec(fStatus);
if (odd(state shr 0)) then Form1.ToggleBox16.Checked := true else
Form1.ToggleBox16.Checked := false;
if (odd(state shr 1)) then Form1.ToggleBox15.Checked := true else
Form1.ToggleBox15.Checked := false;
...
if (odd(state shr 14)) then Form1.ToggleBox2.Checked := true else
Form1.ToggleBox2.Checked := false;
if (odd(state shr 15)) then Form1.ToggleBox1.Checked := true else
Form1.ToggleBox1.Checked := false;
Form1.Timer1.Enabled:=True; //разрешить автоматическое получение
//состояния устройства каждые 10 секунд
Form1.Button3.Enabled:=True;
end;
procedure TSerialThread.UpdateStatus;
//обновить статус в StatusBar
begin
Form1.StatusBar1.SimpleText:=fStatusText;
end;
И, наконец, основная программа запускаемого потока:
procedure TSerialThread.Execute;
var
ser: TBlockSerial;
answer: String;
begin
ser:=TBlockSerial.Create;
try
fStatusText := 'Инициализация порта ввода\вывода...';
Synchronize(@UpdateStatus);
ser.connect(fPortName);
if ser.LastError>0 then
begin
fStatusText := ser.LastErrorDesc;
Synchronize(@UpdateStatus);
Exit;
end;
fStatusText := 'Подключение...';
Synchronize(@UpdateStatus);
ser.config(9600,8,'N',0,false,false);
if ser.LastError>0 then
begin
fStatusText := ser.LastErrorDesc;
Synchronize(@UpdateStatus);
Exit;
end;
fStatusText := 'Посылка команды...';
Synchronize(@UpdateStatus);
ser.sendstring(fDataIn);
if ser.LastError>0 then
begin
fStatusText := ser.LastErrorDesc;
Synchronize(@UpdateStatus);
Exit;
end;
fStatusText := 'Ожидание ответа...';
Synchronize(@UpdateStatus);
answer := ser.RecvTerminated(2000,#13);
if ser.LastError>0 then
begin
fStatusText := ser.LastErrorDesc;
Synchronize(@UpdateStatus);
Exit;
end;
if fStatusGet then
begin
fStatusText := 'Ожидание информации о состоянии...';
Synchronize(@UpdateStatus);
fStatus := ser.RecvTerminated(2000,#13);
fStatus := ExtractWord(2, fStatus, StdWordDelims);
if ser.LastError>0 then
begin
fStatusText := ser.LastErrorDesc;
Synchronize(@UpdateStatus);
Exit;
end;
if fStatus = '' then fStatus:='0000';
Synchronize(@Draw);
end;
fStatusText := 'Команда выполнена';
Synchronize(@UpdateStatus);
finally
ser.free;
end;
end;
На этом создание нового класса завершено. Теперь в основной программе нужно создать поток и запустить его выполнение.
SerThread := TSerialThread.Create(True); SerThread.fPortName:=Form1.Edit1.Text; SerThread.fDataIn:=chr(5)+chr(0)+chr(0)+chr(0); SerThread.fStatusGet:=True; SerThread.Resume;
Переменная fStatusGet заставит вызываться не только функцию обновления StatusBar, но и вторую функцию обновления интерфейса.
Обновление от 18.08.2012: Очереди и блокировки
На момент написания первой версии статьи программа была в зачаточном состоянии и практически ничего не умела. Основная проблема состояла в том, что я работал с последовательным портом, который не может работать одновременно с двумя процессами. Проблема программы состояла в том, что при нажатии на кнопку “выключить все” запускался не один поток, а сразу два, которые работали параллельно. В итоге посылалась только команда выключить, а вторая команда отваливалась в связи с невозможностью открыть порт. В программу пришлось ввести несколько переменных, несколько процедур в класс потока и слегка изменить алгоритм. Начнем с изменений в самом классе потока.
fBusy: boolean; procedure Busy; procedure CheckBusy; procedure ThreadTerminate;
Переменная fBusy это внутренний флаг статуса занятости порта, копирующий состояние флага занятости в основной программе. Он изменяется прямо в потоке, а процедурой Busy переностися в глобальную переменную SerialBusy: boolean;. Процедура CheckBusy выполняет перенос глобального флага занятости порта в переменную fBusy.
Для визуальных эффектов на форму добавлен обычный TShape, показывающий занятость порта, а также дополнительный TStatusBar (я знаю что в TStatusBar есть встроеные панельки; меня просто ломает все переписывать) в котором показывается количество заданий.
procedure TSerialThread.Busy; begin if fBusy = True then begin Form1.Shape1.Brush.Color:=clRed; SerialBusy := True; end else begin Form1.Shape1.Brush.Color:=clGreen; SerialBusy :=False; end; end; procedure TSerialThread.CheckBusy; begin if SerialBusy = True then begin Form1.Shape1.Brush.Color:=clRed; fBusy := True; end else begin Form1.Shape1.Brush.Color:=clGreen; fBusy :=False; end; end;
Чтобы количество заданий работало, необходимо завести глобальную переменную в теле оснофной программы SerialQueued: Integer;. Именно ей нужно делать инкремент или декремент при создании или завершении потока. Я добавил эти действия в конструктор Create и процедуру ThreadTerminate.
constructor TSerialThread.Create(CreateSuspended: boolean); begin FreeOnTerminate := True; inherited Create(CreateSuspended); Form1.Shape1.Brush.Color:=clRed; inc(SerialQueued); Form1.StatusBar2.SimpleText:=IntToStr(SerialQueued); end; procedure TSerialThread.ThreadTerminate; begin Dec(SerialQueued); Form1.StatusBar2.SimpleText:=IntToStr(SerialQueued); if SerialQueued > 0 then Form1.Shape1.Brush.Color:=clRed; end;
Теперь необходимо сделать проверку флагов прямо в процедуре Execute и ждать освобождения порта. Обратите внимание на условие (not Terminated) в цикле. Оно обязательно для всех циклов внутри потоков!
procedure TSerialThread.Execute; var ser: TBlockSerial; answer: string; begin Synchronize(@CheckBusy); while (not Terminated) and (fBusy) do begin //fStatusText := 'Ожидание очереди...'; //Synchronize(@UpdateStatus); sleep(1500); Synchronize(@CheckBusy); end; fBusy := True; Synchronize(@Busy); ser := TBlockSerial.Create; try fStatusText := 'Инициализация порта ввода\вывода...'; Synchronize(@UpdateStatus); ser.connect(fPortName); if ser.LastError > 0 then begin fStatusText := ser.LastErrorDesc; Synchronize(@UpdateStatus); fBusy := False; Synchronize(@Busy); Synchronize(@ThreadTerminate); Terminate; Exit; end; fStatusText := 'Подключение...'; Synchronize(@UpdateStatus); ser.config(9600, 8, 'N', 0, False, False); if ser.LastError > 0 then begin fStatusText := ser.LastErrorDesc; Synchronize(@UpdateStatus); fBusy := False; ser.Free; Synchronize(@Busy); Synchronize(@ThreadTerminate); Terminate; Exit; end; fStatusText := 'Посылка команды...'; Synchronize(@UpdateStatus); ser.sendstring(fDataIn); if ser.LastError > 0 then begin fStatusText := ser.LastErrorDesc; Synchronize(@UpdateStatus); fBusy := False; ser.Free; Synchronize(@Busy); Synchronize(@ThreadTerminate); Terminate; Exit; end; fStatusText := 'Ожидание ответа...'; Synchronize(@UpdateStatus); answer := ser.RecvTerminated(2000, #13); if ser.LastError > 0 then begin fStatusText := ser.LastErrorDesc; Synchronize(@UpdateStatus); fBusy := False; ser.Free; Synchronize(@Busy); Synchronize(@ThreadTerminate); Terminate; Exit; end; if fStatusGet then begin fStatusText := 'Ожидание информации о состоянии...'; Synchronize(@UpdateStatus); fStatus := ser.RecvTerminated(2000, #13); fStatus := ExtractWord(2, fStatus, StdWordDelims); if ser.LastError > 0 then begin fStatusText := ser.LastErrorDesc; Synchronize(@UpdateStatus); fBusy := False; ser.Free; Synchronize(@Busy); Synchronize(@ThreadTerminate); Terminate; Exit; end; if fStatus = '' then fStatus := '0000'; Synchronize(@Draw); end; fStatusText := 'Команда выполнена'; Synchronize(@UpdateStatus); finally end; ser.Free; fBusy := False; Synchronize(@Busy); Synchronize(@ThreadTerminate); Terminate; end;
Заметьте, что в каждой обработке ошибки я начал уничтожать объект ser. Это связано с тем, что он не уничтожится простым Terminate. Всегда очищайте память от созданых объектов! Это позволит избежать утечек и прочих “непонятных” ошибок.
Теперь дело осталось за малым. В теле основной программы нужно запустить поочередно 2 потока с командами и ждать результата 🙂
procedure TForm1.Button5Click(Sender: TObject); begin Timer1.Enabled := False; Button3.Enabled := False; SerThread := TSerialThread.Create(True); SerThread.fPortName := Edit1.Text; SerThread.fDataIn := chr(4) + chr(0) + chr(1) + chr(0); SerThread.Resume; SerThread := TSerialThread.Create(True); SerThread.fPortName := Form1.Edit1.Text; SerThread.fDataIn := chr(5) + chr(0) + chr(0) + chr(0); SerThread.fStatusGet := True; SerThread.Resume; Timer1.Enabled := True; Button3.Enabled := True; end;
Ну и, конечно же, обещаное в начале статьи видео с демонстрацией работы:
А про критические секции (когда кусок кода не может быть выполнен одновременно несколькими процессами) я расскажу вам в следующий раз.
Fortune cookie: "Religion is the highest vanity." [Friedrich Hebbel]
Познавательно.