Rozdział 6 Aplikacje wielowątkowe
Możliwość tworzenia aplikacji wielowątkowych posługujących się zaletami programowania współbieżnego jest jedną z najbardziej atrakcyjnych technik, oferowanych w Windows przez 32-bitowy interfejs programisty. Teoretycznie rzecz biorąc, każdej części pisanego kodu można przyporządkować oddzielny wątek (ang. thread), stanowiący pewien obiekt wykorzystywany przez system operacyjny w ramach danego procesu. Każda projektowana przez nas aplikacja ma co najmniej jeden wątek główny, w którym możemy tworzyć kolejne, zwane wątkami drugorzędnymi. Wielowątkowość nierozerwalnie wiąże się z pojęciem wielozadaniowości. W Win32 API to właśnie wątki są tymi obiektami, które mogą ubiegać się o czas procesora. Nie ma wówczas możliwości całkowitego podporządkowania pracy procesora pojedynczemu wątkowi. System operacyjny sam decyduje, jaki czas należy przydzielić poszczególnym wątkom, po upływie którego mogą zostać wywłaszczone. Niekiedy nazywamy to wielozadaniowością z wywłaszczeniem.
O naturze wątków napisano już bardzo wiele, wystarczy wymienić znakomite książki: „Delphi 3. Księga eksperta”, wyd. Helion (1998) czy „Delphi 4. Vademecum profesjonalisty. Tom 1”, wyd. Helion (1999). W rozdziale tym nie będzie nas jednak interesować cała, niezwykle szeroka oferta programistyczna udostępniana przez wielowątkowość. Chociaż technika programowania współbieżnego oferuje nam olbrzymie możliwości, ma jednak i drugą stronę. Niewłaściwe je użycie może się okazać katastrofalne w skutkach dla działającej aplikacji. Trzeba zdawać sobie sprawę, że pisane przez nas programy są tworami dosyć specyficznymi. Technika ich projektowania znacznie odbiega od sposobu tworzenia stron WWW, skomplikowanych arkuszy kalkulacyjnych lub baz danych. O ile w przypadku wymienionych aplikacji po drugiej stronie jest zawsze inny człowiek (odbiorca), który bywa czasami wyrozumiały na popełnione przez nas niewielkie błędy, to projektując program sterujący jakimś urządzeniem, tego komfortu już nie mamy. Testy takich programów są zawsze bezlitosne, a ich ocena wyraża się prostą logiką zero-jedynkową (FALSE or TRUE). Gdy nieopatrznie wpiszemy jakąś komórkę w arkuszu kalkulacyjnym, błąd taki możemy naprawić stosunkowo prosto. Co się natomiast stanie, gdy do zasilacza wysokiego napięcia podłączonego do jakiegoś przyrządu wyślemy komendę: :VOLTage 550 zamiast prawidłowej :VOLTage 250? Różnica niby nieznaczna, tylko jedna cyfra... ale niekiedy po urządzeniu może pozostać jedynie wspomnienie, zaś na naszym koncie spory debet. Dlatego dalej skoncentrujemy się na pewnych podstawowych, ale skutecznych metodach posługiwania się techniką programowania współbieżnego z perspektywą użytecznego, a zarazem bezpiecznego jej wykorzystania w aplikacjach realizujących szeregową transmisję danych poprzez interfejs RS 232C.
Najważniejszy jest Użytkownik
Projektując każdą aplikację, musimy pamiętać, że oprócz poprawnego działania musi charakteryzować się jeszcze kilkoma bardzo ważnymi cechami. Dokładny opis podstawowych reguł dotyczących sposobu tworzenia programów o różnorodnym przeznaczeniu był tematem wielu publikacji, dlatego w tym miejscu przedstawię tylko najważniejsze spośród nich, mające zarazem bezpośrednie odniesienie to tego, co już stworzyliśmy. Wielowątkowe działanie naszych programów uwzględnimy, dostosowując je do nowych wymagań, jakie przed nimi zostaną postawione.
Użytkownik steruje programem
Dla większości z nas irytującą bywa sytuacja gdy podczas pracy z jakąś aplikacją uświadomimy sobie, że w pewnym momencie kontrolę nad nami zaczął sprawować komputer. Testując przedstawione do tej pory programy realizujące transmisję szeregową na pewno nie raz mieliśmy takie odczucie. Najbardziej widoczne jest to w przypadku programów transmitujących i odbierających pliki. Gdy zaczęliśmy transmisję wybranego wcześniej pliku praktycznie nie można było już nic zrobić. Aplikacja, pozostając nieruchomą na ekranie, nie reagowała na próbę naciśnięcia jakiegokolwiek przycisku aż do momentu zakończenia danego zadania. Pierwszym określeniem, jakie przychodzi mi wówczas na myśl, jest bezwładność i pewna ociężałość takiego produktu. Niewielu Użytkownikom podobają się tak działające aplikacje. Wyzwaniem będzie wówczas dla programisty wymyślenie sposobu, dzięki któremu program stanie się bardziej przyjazny wobec otoczenia.
Możliwość anulowania decyzji
Uwzględnienie tej opcji wynika bezpośrednio z poprzedniego punktu. Poprawnie zaprojektowane programy komunikacyjne powinny przynajmniej częściowo umożliwiać Użytkownikowi anulowanie, nawet w czasie transmisji niektórych podjętych wcześniej decyzji. Ponownie posłużmy się przykładem aplikacji transmitującej plik. Pamiętamy, że jednym ze sposobów zabezpieczenia się przed wysłaniem niechcianego zbioru danych jest obejrzenie go tuż przed transmisją w jednym z okien edycyjnych. Niestety, konstrukcja dotychczasowych programów umożliwiała nam jedynie obejrzenie pliku, nie mieliśmy natomiast żadnej możliwości przerwania w dowolnym momencie jego transmisji i ewentualnie wznowienia jej, nie ingerując zbytnio w tempo działania aplikacji. Ten punkt odnosi się głównie do problemu transmisji większych pakietów danych, gdyż trudno wyobrazić sobie sytuację anulowania decyzji w trakcie wysyłania jednego znaku.
Możliwość odbioru komunikatu nawet w trakcie wysyłania danych
Uwzględnienie tej opcji w naszych aplikacjach może wydać się nieco dziwne. Każdy Czytelnik zdaje sobie oczywiście sprawę z pewnych ograniczeń, jakie nakłada na nas sam fakt posługiwania się szeregową transmisją asynchroniczną. Wykorzystanie w naszych algorytmach tej własności wcale nie będzie wymagało zastosowania jakiegoś wyszukanego okablowania czy niezrozumiałej modyfikacji protokołu transmisji. Zupełnie wystarczy, jeżeli w pełni wykorzystamy poznane już zalety, związane z podwójnym buforowaniem danych.
Możliwość wysłania odrębnej informacji w trakcie transmisji pliku
Posługując się różnego rodzaju programami nadzorującymi proces transmisji szeregowej, możemy spotkać się z koniecznością wysłania jakiejś wiadomości (niekoniecznie bardzo krótkiej) w trakcie transmisji dłuższej porcji informacji. W takich sytuacjach musimy uwzględnić fakt zachwiania parytetu kolejki znaków w buforze wyjściowym. Wcześniej została omówiona funkcja TransmitCommChar(), jednak stosowanie jej bywa nieco uciążliwe, głównie z tego powodu, że argumentem jej może być tylko jeden znak. Naprawdę funkcjonalna aplikacja do transmisji szeregowej powinna posiadać opcję, pozwalającą na szybką modyfikację kolejki znaków będących już w buforze wyjściowym, jednak bez naruszenia ich fizycznej spójności.
Być może postulaty dotyczące spodziewanego rozwoju pisanych do tej pory programów zawarte w powyższych punktach nieco zaniepokoiły niektóre osoby. Można by odnieść ze wszech miar błędne wrażenie, że to, co zrobiliśmy do tej pory, zostanie poddane jakiejś strasznie skomplikowanej modyfikacji w celu dostosowania stworzonych już i poprawnie działających przecież aplikacji do tych nowych warunków. Ktoś mógłby się spodziewać, że oto czeka nas żmudny proces poznawania wielu kolejnych funkcji Win32 API, struktur czy typów danych, nie mówiąc już o konieczności zapoznania się ze specyficznymi własnościami Delphi czy C++Buildera. Już w tym miejscu mogę obiecać, że choć nie unikniemy tego całkowicie, to jednak zrobię to w formie jak najbardziej przystępnej. Wszystkie zaprojektowane do tej pory aplikacje zachowają swój oryginalny kształt. Uwzględnimy możliwość ich pracy wielowątkowej, uwzględnimy tylko nieznacznie je wzbogacając.
Pamiętając o wszystkim, czego dokonaliśmy do tej pory oraz mając na uwadze przedstawione nowe zadania, stojące przed naszymi programistycznymi produktami, nie pozostaje nam już nic innego, jak tylko uzupełnić stworzone już aplikacje o możliwość ich pracy wielowątkowej. Po przeczytaniu sporego fragmentu tej książki osoby preferujące Delphi mogły poczuć się nieco zawiedzione tym, że zawsze nowy temat rozpoczynałem do przykładów pisanych w C++Builderze. Aby im to wynagrodzić, tym razem zaczniemy od Object Pascala.
Delphi
Jak zapewne wiemy, istnieje w Delphi pewna klasa służąca implementacji mechanizmów, którymi charakteryzują się wątki. Jest nią TThread. Korzystając z jej właściwości oraz metod, z powodzeniem uwzględnić można bardzo wiele aspektów wielowątkowości. W celu utworzenia nowego wątku można posłużyć się niezwykle ciekawymi właściwościami parametrów konstruktora TThread.Create(), którego definicję przytoczę za Borland Delphi Visual Component Library:
constructor TThread.Create(CreateSuspended: Boolean);
var
Flags: DWORD;
begin
inherited Create;
AddThread;
FSuspended := CreateSuspended;
Flags := 0;
if CreateSuspended then Flags := CREATE_SUSPENDED;
FHandle := BeginThread(nil, 0, @ThreadProc, Pointer(Self), Flags,
FThreadID);
end;
Przedstawiony konstruktor dokonuje wywołania funkcji BeginThread() zdefiniowanej w Borland Delphi Run-Time Library Win32 API Interface Unit:
function BeginThread(SecurityAttributes: Pointer; StackSize: LongWord;
ThreadFunc: TThreadFunc; Parameter: Pointer;
CreationFlags: LongWord; var ThreadId: LongWord):
Integer;
var
P: PThreadRec;
begin
New(P);
P.Func := ThreadFunc;
P.Parameter := Parameter;
IsMultiThread := TRUE;
Result := CreateThread(SecurityAttributes, StackSize,
@ThreadWrapper, P, CreationFlags, ThreadID);
end;
Jak zauważyliśmy, funkcja BeginThread() dokonuje z kolei wywołania kolejnej o nazwie CreateThread(), tworząc tym samym nowy wątek. Należy jednak pamiętać, że samo utworzenie wątku wcale nie musi oznaczać jego automatycznego uruchomienia. Jeżeli w konstruktorze TThread.Create() jako wartość parametru CreateSuspended obierzemy FALSE (0), wątek zostanie natychmiast uruchomiony. W przeciwnym wypadku (TRUE lub 1) funkcja CreateThread() zostanie wywołana z parametrem CREATE_SUSPENDED, powodując, że działanie nowo utworzonego wątku będzie zawieszone do czasu wywołania metody TThread.Resume:
procedure TThread.Resume;
begin
if ResumeThread(FHandle) = 1 then FSuspended := FALSE;
end;
Powtórne zawieszenie działania wątku nastąpi oczywiście po wywołaniu:
procedure TThread.Suspend;
begin
FSuspended := True;
SuspendThread(FHandle);
end;
Sam proces realizacji wątku odbywa się w ramach metody Execute, wywoływanej w funkcji :
function ThreadProc(Thread: TThread): Integer;
var
FreeThread: Boolean;
begin
Thread.Execute;
FreeThread := Thread.FFreeOnTerminate;
Result := Thread.FReturnValue;
Thread.FFinished := True;
Thread.DoTerminate;
if FreeThread then Thread.Free;
EndThread(Result);
end;
Widzimy, że zakończenie wątku nastąpi dzięki wywołaniu procedury EndThread() z parametrem, którego wartość równa się właściwości FReturnValue (w omawianej klasie domyślnie przyjmowane jest 0), będącej zarazem kodem zakończenia danego wątku. Kod ten można odczytać, wykorzystując w tym celu funkcję GetExitCodeThread().
Dla porównania prześledźmy jeden z możliwych sposobów tworzenia nowego wątku przy wykorzystaniu niektórych funkcji Win32 API. Postępując zgodnie z ideą poprzedniego rozdziału, skoncentrujemy się na jednej z metod bezpośredniego odwołania do Win32 API, gdzie zdefiniowana jest funkcja CreateThread(), za pomocą której można utworzyć i uruchomić nowy wątek w obrębie przestrzeni adresowej odpowiedniego procesu. Funkcja ta zwraca identyfikator nowo utworzonego wątku.
HANDLE CreateThread(LPSECURITY_ATTRIBUTES lpThreadAttributes,
DWORD dwStackSize,
LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter, DWORD dwCreationFlags,
LPDWORD lpThreadId);
W Object Pascalu skorzystamy z analogicznej definicji:
function CreateThread(SecurityAttributes: Pointer;
StackSize: LongWord; ThreadFunc: TThreadFunc;
Parameter: Pointer; CreationFlags: LongWord;
var ThreadId: LongWord): Integer; stdcall;
lpThreadAttributes jest wskaźnikiem do struktury SECURITY_ATTRIBUTES, określającej pewne atrybuty zabezpieczeń nowego wątku. Mam nadzieję, że nie będzie nikogo razić, jeżeli dalej przedstawię definicje właściwe zarówno Win32 API jak i Borland Delphi Run-Time Library Win32 API Interface Unit
typedef struct _SECURITY_ATTRIBUTES {
DWORD nLength;
LPVOID lpSecurityDescriptor;
BOOL bInheritHandle;
} SECURITY_ATTRIBUTES;
lub
PSecurityAttributes = ^TSecurityAttributes;
{$EXTERNALSYM _SECURITY_ATTRIBUTES}
_SECURITY_ATTRIBUTES = record
nLength: DWORD;
lpSecurityDescriptor: Pointer;
bInheritHandle: BOOL;
end;
TSecurityAttributes = _SECURITY_ATTRIBUTES;
{$EXTERNALSYM SECURITY_ATTRIBUTES}
SECURITY_ATTRIBUTES = _SECURITY_ATTRIBUTES;
W obu definicjach nLength jest rozmiarem struktury (rekordu). Przed przekazaniem tego rekordu (struktury) jako parametru w przypadku ogólnym należy wpisać do nLength wartość równą sizeof(SECURITY_ATTRIBUTES). lpSecurityDescriptor jest wskaźnikiem do deskryptora zabezpieczeń wątku jako obiektu. Jeżeli ustalimy NULL (w Pascalu NIL), obiektowi zostanie przydzielona wartość domyślna rodzaju zabezpieczeń w trakcie danego procesu, zaś identyfikator wątku nie będzie dziedziczony. Z kolei bInheritHandle specyfikuje, czy zwracany identyfikator utworzonego wątku jest dziedziczony przez nowy proces. Ustalenie jej jako TRUE zapewni, że ten identyfikator będzie mógł dziedziczyć każdy nowy proces.
dwStackSize jest rozmiarem obszaru pamięci (w bajtach), zwanej stosem, z której korzysta dany proces. Jeżeli przyjmiemy tu wartość 0, rozmiar stosu dla nowego wątku będzie taki sam jak dla wątku głównego. Stos jest automatycznie alokowany w pamięci procesu i automatycznie zwalniany po wstrzymaniu działania wątku. Jeżeli deklarowany rozmiar stosu przewyższa ilość dostępnej pamięci, nie zostanie przydzielony identyfikator do nowego wątku.
lpStartAddress stanowi wskaźnik do części aplikacji (lub funkcji) wykonywanej w danym wątku podając jednocześnie jej adres startowy. Funkcja może zawierać pojedynczy 32-bitowy argument, zwracając jednoczenie 32-bitową wartość.
lpParameter specyfikuje pojedynczą 32-bitową wartość parametru przekazywanego wątkowi.
dwCreationFlags podaje odpowiedni znacznik kontroli sposobu utworzenia nowego wątku. Jeżeli wyspecyfikujemy dobrze nam znany parametr CREATE_SUSPENDED, działanie wątku będzie zawieszone do czasu wywołania funkcji ResumeThread(). Jeżeli zostanie tu przypisana wartość 0, nowy wątek zostanie natychmiast uruchomiony.
lpThreadId jest wskaźnikiem do 32-bitowej zmiennej identyfikującej wątek. Nie należy jednak w żadnym wypadku mylić jej z identyfikatorem nowego wątku.
Opisana funkcja tworzy i przypisuje identyfikator do nowo powstałego wątku. Jeżeli wskaźnik do struktury zabezpieczeń obiektu nie jest używany, identyfikator ten może być wykorzystany przez dowolną funkcję, której wywołanie wymaga użycia unikalnego identyfikatora wątku jako obiektu Win32 API. Uruchomienie wątku zaczyna się od wywołania funkcji specyfikowanej przez lpStartAddress. Aby zatrzymać działający wątek, należy odwołać się do funkcji Win32 API:
VOID ExitThread(DWORD dwExitCode);
lub zdefiniowanej w Borland Delphi Run-Time Library procedury:
procedure ExitThread(ExitCode: Integer); stdcall;
gdzie dwExitCode (ExitCode) specyfikuje kod zakończenia danego wątku. Użycie tej funkcji (procedury) jest preferowaną metodą opuszczania działającego wątku. Gdy jest ona wywoływana (obojętnie czy w sposób jawny, czy też w inny), obszar aktualnego stosu zostanie zwolniony, zawieszając tym samym działanie wątku. W celu otrzymania kodu ostatniego wątku należy posłużyć się funkcją:
BOOL GetExitCodeThread(HANDLE hThread, LPDWORD lpExitCode);
która w Borland Delphi Run-Time Library definiowana jest jako:
function GetExitCodeThread(hThread: THANDLE; var lpExitCode: DWORD):
BOOL; stdcall;
hThread jest identyfikatorem wątku, (w Win NT musi być przydzielony z rodzajem dostępu THREAD_QUERY_INFORMATION), zaś lpExitCode jest wskaźnikiem do 32-bitowej zmiennej, reprezentującej kod zakończenia wątku.
Technika wykorzystania klasy TThread oraz sposoby posługiwania się funkcją CreateThread(), są — jak być może zauważyliśmy — nieco skomplikowane i przyznam w tym miejscu, że mało przydatne do naszych celów. Przecież przyjęliśmy zasadę, że nie będziemy wiele zmieniać w konstrukcji dotychczasowych algorytmów. Istnieje dużo prostszy sposób implementacji wątków w programach sterujących transmisją szeregową. Powróćmy do prezentowanej już funkcji BeginThread(). Funkcję tę można z powodzeniem wykorzystać w aplikacjach pisanych zarówno w Delphi jak i C++Builderze. Umiejętne jej użycie zapewni nam uruchomienie osobnego wątku, bez potrzeby jawnego i bezpośredniego odwoływania się do funkcji Win32 API CreateThread() (co wcale nie oznacza, że znajomość jej jest bezużyteczna). Wielką zaletą posługiwania się BeginThread() jest fakt, że możemy w niej odwołać się do normalnej funkcji Pascala lub C++, która już dzięki temu będzie mogła być potraktowana jako osobny wątek. Rolę takiej funkcji z powodzeniem może pełnić typ:
TThreadFunc = function(Parameter: Pointer): Integer;
TThreadFunc definiuje pewien typ funkcji, która już w momencie użycia traktowana jest jako adres startowy nowego wątku (obiektu Win32). Może być on przekazywany bezpośrednio do BeginThread() lub do funkcji Win32 API CreateThread(). 32-bitowy wskaźnik Parameter jest przekazywany bezpośrednio do BeginThread(). Dodam na marginesie, że dla naszych specyficznych celów stosowanie tego parametru nie jest wymogiem koniecznym wymogiem.
Zobaczmy, jak praktycznie w bardzo prosty sposób można wprowadzić pewne elementy wielowątkowości do naszych aplikacji. Przede wszystkim należy skonstruować własną odmianę typu TThreadFunc. Nic prostszego — wystarczy odpowiednio wykorzystać na przykład procedurę obsługi zdarzenia SendFileClick() opisaną w module rs_17.pas (patrz wydruk 5.15). Zawartość nowej funkcji, nazwijmy ją RS_SendFile(), wypełnimy po prostu tamtym kodem w sposób, który zaprezentowałem poniżej.
function RS_SendFile(P: Pointer): Integer;
var
i : Integer;
FileSizeHigh : DWORD;
begin
for i := 0 to cbOutQueue do
Buffer_O[i] := char(0); // czyści bufor wyjściowy
Form1.ProgressBar1.Max:=0;
if (hCommDev > 0) then
begin
if((_lopen(PChar(Form1.OpenDialog1.FileName), OF_READ)) <>
HFILE_ERROR) then
begin
hfile_s := _lopen(PChar(Form1.OpenDialog1.FileName),
OF_READ);
Form1.ProgressBar1.Max:=GetFileSize(hfile_s,
@FileSizeHigh);
while (_lread(hfile_s, @Buffer_O, 1) > 0) do
begin
Form1.Write_Comm(hCommDev, Buffer_O, 1);
Form1.ProgressBar1.StepIt();
end;
_lclose(hfile_s);
FlushFileBuffers(hCommDev);
end
else
Application.MessageBox('Nie wybrano pliku do'+
' transmisji ', 'Uwaga !' ,MB_OK);
end
else
Application.MessageBox('Niewłaściwa nazwa portu lub jest on'+
' aktywny ', 'Uwaga !',MB_OK);
Result := 0;
end;
Nie pozostaje nam już teraz nic innego, jak tylko zaprojektować procedurę obsługi nowego zdarzenia SendFileClick(), w którym uruchomimy nasz wątek:
procedure TForm1.SendFileClick(Sender: TObject);
begin
hThread_SF := BeginThread (NIL, 0, @RS_SendFile, NIL,
0, ThreadID_SF);
end;
Na rysunku 6.1 pokazano formularz projektu p_RS_21.dpr znajdującego się w katalogu \KODY\DELPHI\RS_21\, który jest nieznaczną modyfikacją przedstawionego wcześniej p_RS_17.dpr. Dodałem w nim jedynie nowe okno edycji w postaci drugiego komponentu TRichEdit. Główny moduł tej aplikacji RS_21.pas, którego kod zamieszczony jest na wydruku 6.1, został tylko w niewielkim stopniu zmieniony w porównaniu ze swoim poprzednikiem. Jedynymi uwzględnionymi w nim nowościami są właśnie opisane wcześniej funkcja: RS_SendFile() oraz procedura obsługi zdarzenia SendFileClick(). Program testowałem, nawiązując połączenie z innym komputerem, na którym uruchomiony był Terminal. Zastosowanie oddzielnego, drugorzędnego wątku dla części algorytmu realizującego wysyłanie plików zapewnia, że bez problemu można w trakcie transmisji danych równolegle przygotowywać w drugim oknie edycji jakiś własny komunikat do wysłania (ktoś może tak przeprojektować algorytm, by mieć możliwość wysłania w ten sposób pliku). Jeżeli wiadomość taką zechcemy nadać w trakcie transmitowanego już pliku, będzie miała rzecz jasna większy priorytet, ale tylko z tego powodu, że jest realizowana w wątku głównym. Tego typu właściwością powinny charakteryzować się wszystkie programy obsługujące urządzenia zewnętrzne. Możemy na przykład stanąć kiedyś przed koniecznością bardzo szybkiego wysłania komendy OFF, choćby i w trakcie pomiaru. Zwróćmy uwagę, że nawet w tej sytuacji struktura danych wysyłanych z pliku zostanie zachwiana tylko w ten sposób, że ostatnio wysyłany komunikat po prostu je uzupełni. Równie ciekawą cechą omawianego programu jest możliwość odebrania danych wysyłanych z innego komputera w czasie, gdy właśnie transmitujemy np. plik. Właściwość taką, poza wykonywaniem nadawania w oddzielnym wątku, może zapewnić umiejętne (jak zwykle to robimy) zastosowanie oddzielnych buforów dla danych transmitowanych i odbieranych. Testując przedstawioną aplikację, zauważymy też, że można ją swobodnie przesuwać po ekranie oraz zmieniać jej rozmiary zarówno w czasie transmisji jak i odbioru danych. Nie mamy również problemu z zakończeniem działania programu w momencie, który uznamy za stosowny. Jest to bardzo wygodna cecha tego typu aplikacji.
Rysunek 6.1. Wygląd formularza projektu p_RS_21.dpr
|
|
Wydruk 6.1. Kod modułu RS_21.pas aplikacji wykorzystującej elementy wielowątkowości
unit RS_21;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls, ComCtrls, FileCtrl, ToolWin, Buttons, Menus;
type
TForm1 = class(TForm)
CloseComm: TButton;
OpenComm: TButton;
SendFile: TButton;
Receive: TButton;
ProgressBar1: TProgressBar;
OpenDialog1: TOpenDialog;
SaveDialog1: TSaveDialog;
CoolBar1: TCoolBar;
CopyText: TSpeedButton;
PasteText: TSpeedButton;
CutText: TSpeedButton;
CheckBox1: TCheckBox;
CheckBox2: TCheckBox;
CheckBox3: TCheckBox;
CheckBox4: TCheckBox;
MainMenu1: TMainMenu;
RichEdit2: TRichEdit;
Label1: TLabel;
Label2: TLabel;
RichEdit1: TRichEdit;
procedure CloseCommClick(Sender: TObject);
procedure OpenCommClick(Sender: TObject);
procedure SendFileClick(Sender: TObject);
procedure ReceiveClick(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure CopyTextClick(Sender: TObject);
procedure PasteTextClick(Sender: TObject);
procedure CutTextClick(Sender: TObject);
procedure FileOpenClick(Sender: TObject);
procedure NewClick(Sender: TObject);
procedure SaveAs1Click(Sender: TObject);
procedure SendWrittenClick(Sender: TObject);
private
{ Private declarations }
sFile: string;
procedure FormCaption(const sFile_s: String);
procedure ShowFileOpen(const sFile_O: String);
function Write_Comm(hCommDev: THANDLE; lpBuffer: PChar;
nNumberOfBytesToWrite: DWORD): Integer;
function Read_Comm(hCommDev: THANDLE; Buf_Size: DWORD): Integer;
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.DFM}
const
// -- wartości znaczników sterujących portu szeregowego --
dcb_fBinary = $0001;
dcb_fParity = $0002;
dcb_fOutxCtsFlow = $0004;
dcb_fOutxDsrFlow = $0008;
// -- fDtrControl --
DTR_CONTROL_ENABLE = $0010;
DTR_CONTROL_HANDSHAKE = $0020;
dcb_fDsrSensitivity = $0040;
dcb_fTXContinueOnXoff = $0080;
dcb_fOutX = $0100;
dcb_fInX = $0200;
dcb_fErrorChar = $0400;
dcb_fNull = $0800;
// -- fRtsControl --
RTS_CONTROL_ENABLE = $1000;
RTS_CONTROL_HANDSHAKE = $2000;
RTS_CONTROL_TOGGLE = $3000;
dcb_fAbortOnError = $4000;
cbInQueue = 1024;
cbOutQueue = 1024;
var
hThread_SF : THANDLE; // pseudoidentyfikator wątku
ThreadID_SF : Cardinal; // zmienna identyfikująca wątek
hfile_s : HFILE; // identyfikator pliku źródłowego
Buffer_O : ARRAY[0..cbOutQueue] of Char; // bufor wyjściowy
Buffer_I : ARRAY[0..cbInQueue] of Char; // bufor wejściowy
Number_Bytes_Read : DWORD;
hCommDev : THANDLE;
lpFileName : LPCSTR;
fdwEvtMask : DWORD;
Stat : TCOMSTAT;
Errors : DWORD;
dcb : TDCB;
//--------------------------------------------------------------------
procedure TForm1.CloseCommClick(Sender: TObject);
var
iCheckSave: Integer;
begin
iCheckSave := MessageDlg(Format('Zamknięcie aplikacji ? %s ',
[sFile]),
mtConfirmation, mbYesNoCancel, 0);
case iCheckSave of
idYes:
begin
SuspendThread(ThreadID_SF);
CloseHandle(hCommDev);
Application.Terminate();
end;
idNo: {};
idCancel: Abort;
end;
end;
//--------------------------------------------------------------------
procedure TForm1.FormCreate(Sender: TObject);
begin
OpenDialog1.InitialDir := ExtractFilePath(ParamStr(0));
SaveDialog1.InitialDir := OpenDialog1.InitialDir;
ProgressBar1.Step := 1;
RichEdit1.ScrollBars := ssBoth;
end;
//--------------------------------------------------------------------
procedure TForm1.CopyTextClick(Sender: TObject);
begin
RichEdit1.CopyToClipboard;
end;
//--------------------------------------------------------------------
procedure TForm1.PasteTextClick(Sender: TObject);
begin
RichEdit1.PasteFromClipboard;
end;
//--------------------------------------------------------------------
procedure TForm1.CutTextClick(Sender: TObject);
begin
RichEdit1.CutToClipboard;
end;
//--------------------------------------------------------------------
procedure TForm1.FormCaption(const sFile_s: String);
begin
sFile := sFile_s;
Caption := Format('%s - %s', [ExtractFileName(sFile_s),
Application.Title]);
end;
//--------------------------------------------------------------------
procedure TForm1.FileOpenClick(Sender: TObject);
begin
if OpenDialog1.Execute then
begin
ShowFileOpen(OpenDialog1.FileName);
RichEdit1.ReadOnly := ofReadOnly in OpenDialog1.Options;
end;
end;
//--------------------------------------------------------------------
procedure TForm1.NewClick(Sender: TObject);
begin
FormCaption('Bez nazwy');
RichEdit1.Lines.Clear;
end;
//--------------------------------------------------------------------
procedure TForm1.SaveAs1Click(Sender: TObject);
begin
if SaveDialog1.Execute then
begin
if FileExists(SaveDialog1.FileName) then
if MessageDlg(Format('Plik zapisany ponownie %s',
[SaveDialog1.FileName]), mtConfirmation,
mbYesNoCancel, 0) <> idYes then Exit;
RichEdit1.Lines.SaveToFile(SaveDialog1.FileName);
FormCaption(SaveDialog1.FileName);
RichEdit1.Modified := FALSE;
end;
end;
//--------------------------------------------------------------------
procedure TForm1.ShowFileOpen(const sFile_O: string);
begin
RichEdit1.Lines.LoadFromFile(sFile_O);
FormCaption(sFile_O);
RichEdit1.SetFocus;
RichEdit1.Modified := FALSE;
end;
//--------------------------------------------------------------------
function TForm1.Write_Comm(hCommDev: THANDLE; lpBuffer: PChar;
nNumberOfBytesToWrite: DWORD): Integer;
var
NumberOfBytesWritten : DWORD;
begin
WriteFile(hCommDev, Buffer_O, nNumberOfBytesToWrite,
NumberOfBytesWritten, NIL);
if (WaitCommEvent(hCommDev, fdwEvtMask, NIL) = TRUE) then
Write_Comm := 1
else
Write_Comm := 0;
end;
//--------------------------------------------------------------------
function TForm1.Read_Comm(hCommDev: THANDLE;
Buf_Size: DWORD): Integer;
var
nNumberOfBytesToRead: DWORD;
begin
ClearCommError(hCommDev, Errors, @Stat);
if (Stat.cbInQue > 0) then
begin
if (Stat.cbInQue > Buf_Size) then
nNumberOfBytesToRead := Buf_Size
else
nNumberOfBytesToRead := Stat.cbInQue;
ReadFile(hCommDev, Buffer_I, nNumberOfBytesToRead,
Number_Bytes_Read, NIL);
Read_Comm := 1;
end
else
begin
Number_Bytes_Read := 0;
Read_Comm := 0;
end;
end;
//--------------------------------------------------------------------
procedure TForm1.OpenCommClick(Sender: TObject);
begin
if (CheckBox1.Checked = TRUE) then
lpFileName:='COM1';
if (CheckBox2.Checked = TRUE) then
lpFileName:='COM2';
hCommDev:= CreateFile(lpFileName, GENERIC_READ or GENERIC_WRITE, 0,
NIL, OPEN_EXISTING, 0, 0);
if (hCommDev <> INVALID_HANDLE_VALUE) then
begin
SetupComm(hCommDev, cbInQueue, cbOutQueue);
dcb.DCBlength := sizeof(dcb);
GetCommState(hCommDev, dcb);
if (CheckBox3.Checked = TRUE) then
dcb.BaudRate:=CBR_1200;
if (CheckBox4.Checked = TRUE) then
dcb.BaudRate:=CBR_19200;
//-przykładowe ustawienia flag sterujących DCB-
dcb.Flags := dcb_fParity;
dcb.Parity := ODDPARITY;
dcb.StopBits := TWOSTOPBITS;
dcb.ByteSize := 8;
SetCommState(hCommDev, dcb);
GetCommMask(hCommDev, fdwEvtMask);
SetCommMask(hCommDev, EV_TXEMPTY);
end
else
case hCommDev of
IE_BADID: MessageDlg('Niewłaściwa nazwa portu '+lpFileName+
' lub jest on aktywny ',
mtError, [mbOk], 0);
end;
end;
//--------------------------------------------------------------------
function RS_SendFile(P: Pointer): Integer;
var
i : Integer;
FileSizeHigh : DWORD;
begin
for i := 0 to cbOutQueue do
Buffer_O[i] := char(0); // czyści bufor wyjściowy
Form1.ProgressBar1.Max:=0;
if (hCommDev > 0) then
begin
if((_lopen(PChar(Form1.OpenDialog1.FileName), OF_READ)) <>
HFILE_ERROR) then
begin
hfile_s := _lopen(PChar(Form1.OpenDialog1.FileName),
OF_READ);
Form1.ProgressBar1.Max:=GetFileSize(hfile_s,
@FileSizeHigh);
while (_lread(hfile_s, @Buffer_O, 1) > 0) do
begin
Form1.Write_Comm(hCommDev, Buffer_O, 1);
Form1.ProgressBar1.StepIt();
end;
_lclose(hfile_s);
FlushFileBuffers(hCommDev);
end
else
Application.MessageBox('Nie wybrano pliku do'+
' transmisji ', 'Uwaga !' ,MB_OK);
end
else
Application.MessageBox('Niewłaściwa nazwa portu lub jest on'+
' aktywny ', 'Uwaga !',MB_OK);
Result := 0;
end;
//---------wysyłanie pliku w oddzielnym wątku ------------------------
procedure TForm1.SendFileClick(Sender: TObject);
begin
hThread_SF := BeginThread (NIL, 0, @RS_SendFile, NIL,
0, ThreadID_SF);
end;
//---------------odbiór danych ---------------------------------------
procedure TForm1.ReceiveClick(Sender: TObject);
begin
// Form1.ProgressBar1.Max := 0;
if (Form1.Read_Comm(hCommDev, SizeOf(Buffer_I)) > 0) then
begin
Form1.RichEdit2.Text := Buffer_I;
end
else
begin
Form1.RichEdit2.Text := 'Brak danych do odebrania';
Beep();
end;
end;
//--------transmisja danych ------------------------------------------
procedure TForm1.SendWrittenClick(Sender: TObject);
begin
if (hCommDev > 0) then
begin
StrCopy(Buffer_O, PChar(Form1.RichEdit2.Text));
Form1.Write_Comm(hCommDev, Buffer_O, StrLen(Buffer_O));
FlushFileBuffers(hCommDev);
end
else
Application.MessageBox('Niewłaściwa nazwa portu lub jest on'+
' aktywny ', 'Uwaga !',MB_OK);
end;
//--------------------------------------------------------------------
end.
Funkcji BeginThread() tworzącej wątek, w którym transmitowane są dane z pliku, został przydzielony identyfikator, zadeklarowany następująco:
var
hThread_SF : THANDLE;
Tak naprawdę, hThread_SF występuje w roli tak zwanego pseudoidentyfikatora, mimo że jest (ale nie musi być) typu THANDLE odnośnego wątku. Tego typu pseudoidentyfikator jest zazwyczaj odnośnikiem do normalnego identyfikatora, nie stanowiąc tym samym odrębnego obiektu. Użycie funkcji CloseHandle() nie wywoła żadnych skutków. Zakończenie tego typu wątków drugorzędnych nastąpi po zamknięciu wątku głównego i nie musimy się tym zbytnio przejmować, tym bardziej, że samodzielnie nie rezerwowaliśmy dla procesu związanego z tym wątkiem żadnych specjalnych obszarów pamięci. Na nasze potrzeby nie było to konieczne.
Przedstawionej aplikacji brakuje jeszcze opcji umożliwiającej wstrzymanie (zawieszenie) i przywrócenie w dowolnym momencie procesu transmisji danych. Owych cech w formularzu p_RS_21.dpr nie uwzględniłem tylko z tego powodu, by niepotrzebnie nie mnożyć przycisków lub innych opcji menu. Uwzględnienie ich wymaga użycia dwóch bardzo prostych funkcji Win32 API — pierwszej:
DWORD SuspendThread(HANDLE hThread);
za pomocą której możemy czasowo wstrzymać proces wykonywania wątku oraz następnej, wznawiającej jego wykonywanie:
DWORD ResumeThread(HANDLE hThread);
Wykorzystanie ich w naszym programie w najprostszym wypadku wymagałoby zbudowania dwóch nowych zdarzeń:
procedure TForm1.SuspendClick(Sender: TObject);
begin
SuspendThread(hThread_SF);
end;
procedure TForm1.ResumeClick(Sender: TObject);
begin
ResumeThread(hThread_SF);
end;
Myślę, że nie nikt nie będzie miał trudności z samodzielnym wkomponowaniem tych dwóch nowych zdarzeń do swojej aplikacji.
Konkurencja dla Timera
Pamiętamy wszystko, co powiedzieliśmy na temat wad i zalet obiektu TTimer. Aplikacje posługujące się nim również nie działały w sposób szczególnie elegancki. Regulując częstość odczytu danych z portu szeregowego czy to za pomocą komponentu TCSpinEdit, czy nawet za pomocą TTrackBar, nie byliśmy w stanie w sposób tak naprawdę płynny dostroić się do miernika. Wszystkie te operacje przebiegały w sposób „prawie” swobodny, zaś w czasie ich wykonywania pomiar był, niestety, wstrzymywany. Zobaczmy zatem, czym można zastąpić wymieniony komponent. Rysunek 6.2 przedstawia znany nam już formularz aplikacji obsługującej woltomierz cyfrowy, której projekt można znaleźć w katalogu \KODY\DELPHI\RS_22\p_RS_22.dpr. Formularz ten oraz jego kod zmodyfikowałem nieznacznie w ten sposób, by przedstawiał aplikację współpracującą z bardzo dokładną wagą cyfrową, wykorzystując przy tym zalety funkcji BeginThread(). Zachowałem tu wszystkie zastosowane wcześniej właściwości edytora IDE. Wydruk 6.2 pokazuje kompletny algorytm omawianego programu.
Rysunek 6.2. Wygląd formularza projektu p_RS_22.dpr
|
|
Wydruk 6.2. Kod modułu RS_22.pas aplikacji wykorzystującej elementy wielowątkowości przy obsłudze wagi cyfrowej
unit RS_22;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls, ComCtrls, ExtCtrls, Buttons;
type
TForm1 = class(TForm)
Start: TButton;
Suspend: TButton;
Resume: TButton;
CloseComm: TButton;
OpenComm: TButton;
Memo2: TMemo;
Edit1: TEdit;
TrackBar1: TTrackBar;
Label1: TLabel;
Label2: TLabel;
Label3: TLabel;
Panel1: TPanel;
CheckBox1: TCheckBox;
CheckBox2: TCheckBox;
Label4: TLabel;
Label5: TLabel;
SpeedButton1: TSpeedButton;
SpeedButton2: TSpeedButton;
Memo1: TMemo;
procedure StartClick(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure SuspendClick(Sender: TObject);
procedure ResumeClick(Sender: TObject);
procedure CloseCommClick(Sender: TObject);
procedure OpenCommClick(Sender: TObject);
procedure SpeedButton1Click(Sender: TObject);
procedure SpeedButton2Click(Sender: TObject);
procedure TrackBar1Change(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.DFM}
const
// -- wartości znaczników sterujących portu szeregowego --
dcb_fBinary = $0001;
dcb_fParity = $0002;
cbInQueue = 32; // rozmiary buforów danych
cbOutQueue = 32;
var
query : PChar = 'SI'+#13+#10; // rozkaz wysłania mierzonej wartości
Buffer_O : ARRAY[0..cbOutQueue] of Char; // bufor wyjściowy
Buffer_I : ARRAY[0..cbInQueue] of Char; // bufor wejściowy
Number_Bytes_Read : DWORD;
hCommDev : THANDLE;
lpFileName : PChar;
fdwEvtMask : DWORD;
Stat : TCOMSTAT;
Errors : DWORD;
dcb : TDCB;
intVar : LongWord; // licznik pomiarów
intVarSleep : Cardinal; // licznik późnienia
bResult : BOOL; // "niema" zmienna logiczna
hThread_SR : THANDLE; // pseudoidentyfikator wątku
ThreadID_SR: Cardinal;
//--------------------------------------------------------------------
procedure TForm1.CloseCommClick(Sender: TObject);
var
iCheckProcess: Integer;
begin
iCheckProcess := MessageDlg('Zakończenie pomiaru i zamknięcie'+
' aplikacji?', mtConfirmation, [mbYes, mbNo], 0);
case iCheckProcess of
idYes:
begin
CloseHandle(hCommDev);
Application.Terminate();
end;
idNo: Exit;
end;
end;
//--------------------------------------------------------------------
procedure TForm1.OpenCommClick(Sender: TObject);
begin
if (CheckBox1.Checked = TRUE) then
lpFileName:='COM1';
hCommDev:= CreateFile(lpFileName, GENERIC_READ or GENERIC_WRITE, 0,
NIL, OPEN_EXISTING, 0, 0);
if (hCommDev <> INVALID_HANDLE_VALUE) then
begin
SetupComm(hCommDev, cbInQueue, cbOutQueue);
dcb.DCBlength := sizeof(dcb);
GetCommState(hCommDev, dcb);
if (CheckBox2.Checked = TRUE) then
dcb.BaudRate:=CBR_4800;
//-przykładowe ustawienia znaczników sterujących DCB-
dcb.Flags := dcb_fParity;
dcb.Parity := NOPARITY;
dcb.StopBits :=ONESTOPBIT;
dcb.ByteSize :=8;
SetCommState(hCommDev, dcb);
GetCommMask(hCommDev, fdwEvtMask);
SetCommMask(hCommDev, EV_TXEMPTY);
end
else
case hCommDev of
IE_BADID:
begin
Application.MessageBox('Niewłaściwa nazwa portu lub'+
' jest on aktywny', 'Uwaga !',MB_OK);
lpFileName:='';
end;
end;
end;
//--------------------------------------------------------------------
function Write_Comm(hCommDev: THANDLE;
nNumberOfBytesToWrite: DWORD): Integer;
var
NumberOfBytesWritten : DWORD;
begin
WriteFile(hCommDev, Buffer_O, nNumberOfBytesToWrite,
NumberOfBytesWritten, NIL);
if (WaitCommEvent(hCommDev, fdwEvtMask, NIL) = TRUE) then
Write_Comm := 1
else
Write_Comm := 0;
end;
//--------------------------------------------------------------------
function Read_Comm(hCommDev: THANDLE;
Buf_Size: DWORD): Integer;
var
nNumberOfBytesToRead: DWORD;
begin
ClearCommError(hCommDev, Errors, @Stat);
if (Stat.cbInQue > 0) then
begin
if (Stat.cbInQue > Buf_Size) then
nNumberOfBytesToRead := Buf_Size
else
nNumberOfBytesToRead := Stat.cbInQue;
ReadFile(hCommDev, Buffer_I, nNumberOfBytesToRead,
Number_Bytes_Read, NIL);
Read_Comm := 1;
end
else
begin
Number_Bytes_Read := 0;
Read_Comm := 0;
end;
end;
//--------------------------------------------------------------------
function RS_Send_Receive(P: Pointer): Integer;
begin
REPEAT
repeat // transmisja komunikatu
FlushFileBuffers(hCommDev);
until (Write_Comm(hCommDev, StrLen(Buffer_O)) <> 0);
Form1.Memo1.Lines.Add('');
Sleep(intVarSleep);
//-------odczyt danych z portu--------
if ( Read_Comm(hCommDev, SizeOf(Buffer_I)) > 0) then
begin
Form1.Memo2.Lines.Add(AnsiString(Buffer_I));
Inc(intVar); // zliczanie kolejnych pomiarów
Form1.Memo1.Lines.Add(AnsiString(IntToStr(intVar)));
// Beep();
end
else
begin
Form1.Memo2.Lines.Add('x0'); // błędny odczyt
Beep();
Form1.Memo2.Lines.Add('');
end;
UNTIL( bResult = FALSE);
Result:=0;
end;
//--------------------------------------------------------------------
procedure TForm1.StartClick(Sender: TObject);
begin
if (hCommDev > 0) then
begin
OpenComm.Enabled := FALSE;
StrCopy(Buffer_O, query);
hThread_SR := BeginThread (NIL, 0, @RS_Send_Receive, NIL, 0,
ThreadID_SR);
Label1.Caption := 'Pomiar';
end
else
Application.MessageBox('Niewłaściwa nazwa portu lub jest on'+
' aktywny ', 'Uwaga !',MB_OK);
end;
//--------------------------------------------------------------------
procedure TForm1.FormCreate(Sender: TObject);
begin
TrackBar1.Position := 1000;
TrackBar1.Max := 2000;
TrackBar1.Min := 1;
TrackBar1.Frequency := 100;
OpenComm.Enabled := TRUE;
intVar := 0;
intVarSleep := 1000;
bResult := TRUE;
end;
//----------wstrzymanie pomiaru --------------------------------------
procedure TForm1.SuspendClick(Sender: TObject);
begin
SuspendThread(hThread_SR);
Label1.Caption := 'Wstrzymanie';
end;
//----------wznowienie pomiaru ---------------------------------------
procedure TForm1.ResumeClick(Sender: TObject);
begin
ResumeThread(hThread_SR);
Label1.Caption := 'Pomiar';
end;
//-----kopiowanie okna edycji Memo2 do schowka------------------------
procedure TForm1.SpeedButton1Click(Sender: TObject);
begin
Form1.Memo2.SelectAll;
Form1.Memo2.CopyToClipboard;
end;
//-----kopiowanie okna edycji Memo1 do schowka------------------------
procedure TForm1.SpeedButton2Click(Sender: TObject);
begin
Form1.Memo1.SelectAll;
Form1.Memo1.CopyToClipboard;
end;
//--------------------------------------------------------------------
procedure TForm1.TrackBar1Change(Sender: TObject);
begin
intVarSleep := TrackBar1.Position; // sterowanie późnieniem
Edit1.Text := IntToStr(TrackBar1.Position);
end;
//-------------------------------------------------------------------
end.
Śledząc powyższe zapisy, bez trudu zauważymy, że części programu wysyłające i odbierające komunikaty zostały połączone w jednej funkcji RS_Send_Receive(). Zastosowałem w niej główną instrukcję powtarzającą REPEAT...UNTIL, która, jak widzimy, nigdy nie może ulec zakończeniu (wartość bResult jest zawsze ustalona jako TRUE — patrz procedura FormCreate()). Jedynym sposobem wstrzymania pomiaru jest albo zamknięcie aplikacji, albo użycie przycisku Wstrzymaj, wywołującego procedurę obsługi zdarzenia SuspendClick(). Proces zbierania danych może zostać przywrócony w dowolnym momencie poprzez uruchomienie procedury obsługi zdarzenia ResumeClick(). Funkcja RS_Send_Receive() jest najważniejszym argumentem dla BeginThread()wywoływanej w treści procedury obsługi zdarzenia StartClick(), które uruchamiamy przyciskiem Rozpocznij pomiar. Testując aplikację, przekonamy się, że proces wyboru odpowiedniego przedziału czasu pomiędzy dwoma kolejnymi odczytami wcale nie zawiesza transmisji. Argumentem funkcji Sleep() sterującej opóźnieniem, z jakim dokonywane są kolejne odczyty danych, jest zmienna intVarSleep, która przybiera aktualną wartość cechy Position komponentu TrackBar1.
Należy oczywiście zauważyć, że konstrukcja funkcji RS_Send_Receive() również może również opierać się na wykorzystaniu prostszej w użyciu i trochę szybszej w działaniu instrukcji powtarzającej while...do, tak jak przedstawia to poniższy przykład:
function RS_Send_Receive(P: Pointer): Integer;
begin
while ((bResult = TRUE)) do
BEGIN
// --- wysyłanie zapytania ---
while(Write_Comm(hCommDev, StrLen(Buffer_O)) = 0) do
FlushFileBuffers(hCommDev);
Form1.Memo1.Lines.Add('');
Sleep(intVarSleep);
//-------odczyt danych z portu--------
if (Read_Comm(hCommDev, SizeOf(Buffer_I)) > 0) then
begin
Form1.Memo2.Lines.Add(AnsiString(Buffer_I));
Inc(intVar); // zliczanie kolejnych pomiarów
Form1.Memo1.Lines.Add(AnsiString(IntToStr(intVar)));
// Beep();
end
else
begin
Form1.Memo2.Lines.Add('x0');
Beep();
Form1.Memo2.Lines.Add('');
end;
END; // koniec zewnętrznego while
Result:=0;
end;
W „typowych” programach instrukcje powtarzające while...do oraz repeat...until działają z reguły nieco odmiennie. Pierwsza z nich powoduje cykliczne wykonywanie instrukcji warunkowej do czasu spełnienia określonego warunku, który jest sprawdzany przed wykonaniem danej instrukcji. Możliwa jest zatem sytuacja, gdy instrukcja ta nie zostanie wykonana ani razu. W naszym przypadku oznaczałoby to, że nie wysłaliśmy żadnego zapytania do urządzenia, czyli na pewno nie otrzymamy odpowiedzi. W tego typu programach nie może być dwuznaczności. Używając z kolei repeat...until będziemy pewni, że dany warunek będzie sprawdzany po wykonaniu bloku instrukcji, czyli dana instrukcja musi być wykonana przynajmniej raz. Jest to ważne, jeżeli chcemy być pewni wykonania np. funkcji FlushFileBuffers().
Testując przedstawiony algorytm, możemy wysnuć jeszcze jeden bardzo ważny wniosek, mianowicie: pseudoidentyfikator, np. hThread_SR, pełni odmienną rolę niż zmienna identyfikująca dany wątek, np. ThreadID_SR. Podstawowa różnica pomiędzy nimi jest taka, że instrukcja:
SuspendThread(ThreadID_SR);
po prostu nie będzie działać. Argumentami funkcji manipulujących wątkami mogą być tylko ich pseudoidentyfikatory. Jeszcze wyraźniej zauważymy to na przykładzie programu napisanego w C++Builderze.
Analizując zaprezentowane przykłady, śmiało stwierdzimy, że Timera z powodzeniem może zastąpić jakaś instrukcja powtarzająca, która jednak nigdy nie może być zakończona i nie może być tam żadnej zmiennej sterującej. Jeżeli algorytm zostanie jeszcze uzupełniony o elementy wielowątkowości, całość będzie działać naprawdę dobrze, a my przestaniemy martwić się o prawidłowość i cykliczność taktowania obiektu typu TTimer.
C++ Builder
Zastosowanie opisanych wcześniej sposobów wykorzystywania elementów wielowątkowości w programach obsługujących łącze szeregowe RS 232C, tworzonych w C++Builderze nie powinno sprawić nam większych kłopotów. Jedynym, z którym może zetknąć się mniej doświadczony programista, jest nieco odmienny sposób deklarowania BeginThread. Builder określa go za pomocą instrukcji extern jak typ int:
extern PACKAGE int __fastcall BeginThread(void * SecurityAttributes, unsigned StackSize, TThreadFunc ThreadFunc, void * Parameter, unsigned CreationFlags, unsigned &ThreadId);
Ponadto TThreadFunc już w swojej definicji jest zapisany z operatorem wyłuskiwania:
typedef Integer __fastcall (*TThreadFunc)(Pointer Parameter);
co sugeruje, że w najprostszym przypadku przy wywołaniu konkretnej funkcji reprezentującej ten typ jako parametr BeginThread(), nie będzie potrzeby posługiwania się operatorem adresowym, tak jak schematycznie prezentują to poniższe zapisy.
int hThread_SW; // pseudoidentyfikator wątku
unsigned uThreadID_SW; // zmienna identyfikująca wątek
...
int __fastcall RS_SendWritten(Pointer Parameter)
{
...
return TRUE;
}
...
void __fastcall TForm1::SendWrittenClick(TObject *Sender)
{
...
hThread_SW = BeginThread (NULL, 0, RS_SendWritten, NULL,
0, uThreadID_SW);
...
}
Zarówno w przykładzie pokazanym w Delphi jak i tym w Builderze wskaźnik będący parametrem funkcji RS_SendWritten() należy do typu Pointer. Nie wskazuje on na żadną konkretną zmienną, służy natomiast do przekazywania adresów innych zmiennych. Rozpatrzmy to na przykładzie projektu \KODY\BUILDER\RS_08\p_RS_08.bpr, który jest niczym innym, jak uzupełnioną o elementy wielowątkowości wersją p_RS_06.bpr. Cała modyfikacja polega na wydzieleniu trzech wątków drugorzędnych określonych pseudoidentyfikatorami hThread_SF (dla części wysyłającej pliki — Send File), hThread_SW (dla części transmitującej dane wpisywane z klawiatury lub plik zmodyfikowane tuż przed wysłaniem — Send Written) oraz hThread_Rec (część odbierająca dane z łącza szeregowego — Receive). Konieczność utworzenia trzech wątków wynika z faktu, iż aplikacja nasza posługuje się dwoma komponentami edycyjnym, które w dużym uproszczeniu można traktować jako swego rodzaju okna dialogowe stworzone za pomocą biblioteki VCL (ang. Visual Component Library). Być może w Object Pascalu nie było to tak oczywiste, jednak Win32 API wymaga dla każdego wątku osobnego okna edycji. Ktoś może mieć wątpliwości: trzy wątki i dwa okna... jednak po chwili namysłu dojdziemy do wniosku, że wszystko się zgadza. Przecież jeden z komponentów typu TRichEdit (w tym wypadku RichEdit2) pełni podwójną rolę: wyświetlamy tam zawartość pliku i możemy wpisywać własne komunikaty przeznaczone do transmisji. Rysunek 6.3 pokazuje wygląd omawianego formularza. Na wydruku 6.3 prezentowany jest jego kompletny kod źródłowy ze szczegółowym przedstawieniem kolejności użycia odpowiednich funkcji.
Rysunek 6.3. Wygląd formularza projektu p_RS_08.dpr
|
|
Wydruk 6.3. Kod modułu RS_08.cpp aplikacji wykorzystującej elementy wielowątkowości przy wysyłaniu plików
//--- kompilować z borlndmm.dll oraz cc3250mt.dll --------------
//--------RS_08.cpp-----------------------
#include <vcl.h>
#pragma hdrstop
#include "RS_08.h"
#pragma package(smart_init)
#pragma resource "*.dfm"
#define cbOutQueue 1024 //rozmiar bufora danych wyjściowych
#define cbInQueue 1024 //rozmiar bufora danych wejściowych
TForm1 *Form1;
AnsiString New_File; // przechowuje nazwę pliku
HFILE hfile_s; // identyfikator pliku
char Buffer_O[cbOutQueue]; // bufor danych wyjściowych
char Buffer_I[cbInQueue]; // bufor danych wejściowych
DWORD Number_Bytes_Read; // liczba bajtów do czytania
HANDLE hCommDev; // identyfikator portu
LPCTSTR lpFileName; // wskaźnik do nazwy portu
DCB dcb; // struktura kontroli portu szeregowego
DWORD fdwEvtMask;
COMSTAT Stat;
DWORD Errors;
BOOL bResult; // zmienna boolowska
int hThread_SF, hThread_SW, hThread_Rec; // pseudoidentyfikatory
// wątków
unsigned uThreadID_SF, uThreadID_SW, uThreadID_Rec;
//--------------------------------------------------------------------
int __fastcall Close_Comm(HANDLE hCommDev)
{
CloseHandle(hCommDev);
return TRUE;
}
//--------------------------------------------------------------------
int __fastcall Write_Comm(HANDLE hCommDev,
DWORD nNumberOfBytesToWrite)
{
DWORD NumberOfBytesWritten;
if (WriteFile(hCommDev, &Buffer_O[0],
nNumberOfBytesToWrite, &NumberOfBytesWritten, NULL) > 0)
{
WaitCommEvent(hCommDev, &fdwEvtMask, NULL);
return TRUE;
}
else
return FALSE;
}
//--------------------------------------------------------------------
int __fastcall Read_Comm(HANDLE hCommDev,
LPDWORD lpNumberOfBytesRead, DWORD Buf_Size)
{
DWORD nNumberOfBytesToRead;
ClearCommError(hCommDev, &Errors ,&Stat);
if (Stat.cbInQue > 0)
{
if (Stat.cbInQue > Buf_Size)
nNumberOfBytesToRead = Buf_Size;
else
nNumberOfBytesToRead = Stat.cbInQue;
ReadFile(hCommDev, &Buffer_I[0], nNumberOfBytesToRead,
lpNumberOfBytesRead, NULL);
}
else
*lpNumberOfBytesRead = 0;
return TRUE;
}
//--------------------------------------------------------------------
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
}
//--------------------------------------------------------------------
void __fastcall TForm1::CheckFileSave(void)
{
if (RichEdit1->Modified)
{
switch(MessageBox(NULL, "Zawartość pliku lub okna została"
" zmieniona. Zapisać zmiany?", "Uwaga!",
MB_YESNOCANCEL | MB_ICONQUESTION))
{
case ID_YES : FileSaveClick(this);
case ID_CANCEL : Abort();
};
}
}
//--------------------------------------------------------------------
void __fastcall TForm1::FormCreate(TObject *Sender)
{
OpenDialog1->InitialDir = ExtractFilePath(ParamStr(0));
OpenDialog1->Filter =
"*.dat , *.txt, *.cpp, *.c | *.dat; *.txt; *.cpp; *.c";
SaveDialog1->InitialDir = OpenDialog1->InitialDir;
SaveDialog1->Filter = "*.*|*.*";
FileOpen->Hint = "Otwórz plik";
FileOpen->ShowHint = TRUE;
FileSave->Hint = "Zapisz";
FileSave->ShowHint = TRUE;
CopyText->Hint = "Kopiuj";
CopyText->ShowHint = TRUE;
PasteText->Hint = "Wklej";
PasteText->ShowHint = TRUE;
CutText->Hint = "Wytnij";
CutText->ShowHint = TRUE;
CleanBuffers->Hint = "Wyczyść bufory";
CleanBuffers->ShowHint = TRUE;
ReceiveFileSave->Hint = "Zapisz otrzymane";
ReceiveFileSave->ShowHint = TRUE;
RichEdit1->ScrollBars = ssBoth;
}
//--------------------------------------------------------------------
void __fastcall TForm1::FileOpenClick(TObject *Sender)
{
if (OpenDialog1->Execute())
{
RichEdit1->Lines->LoadFromFile(OpenDialog1->FileName);
RichEdit1->Modified = FALSE;
RichEdit1->ReadOnly =
OpenDialog1->Options.Contains(ofReadOnly);
}
}
//--------------------------------------------------------------------
void __fastcall TForm1::CloseCommClick(TObject *Sender)
{
switch(MessageBox(NULL, " Działanie aplikacji zostanie"
" zakończone.", "Uwaga!", MB_YESNOCANCEL | MB_ICONQUESTION))
{
case ID_YES :
{
if (RichEdit1->Modified)
CheckFileSave();
Close_Comm(hCommDev);
Application->Terminate();
}
case ID_CANCEL : Abort();
};
}
//--------------------------------------------------------------------
void __fastcall TForm1::FileSaveClick(TObject *Sender)
{
if ( ! strcmp(New_File.c_str(),LoadStr(256).c_str()) )
SaveAs1Click(Sender);
else
{
RichEdit1->Lines->SaveToFile(New_File);
RichEdit1->Modified = FALSE;
}
}
//--------------------------------------------------------------------
void __fastcall TForm1::SaveAs1Click(TObject *Sender)
{
if (SaveDialog1->Execute()) // dane będą zapisywane w
// formacie Rich !
{
RichEdit1->Lines->SaveToFile(SaveDialog1->FileName);
RichEdit1->Modified = FALSE;
}
}
//--------------------------------------------------------------------
void __fastcall TForm1::ReceiveFileSaveClick(TObject *Sender)
{
if (SaveDialog1->Execute())
{
RichEdit2->Lines->SaveToFile(SaveDialog1->FileName);
RichEdit2->Modified = FALSE;
}
}
//---------------------------------------------------------------------
void __fastcall TForm1::NewClick(TObject *Sender)
{
RichEdit1->Lines->Clear();
RichEdit1->Modified = FALSE;
}
//--------------------------------------------------------------------
void __fastcall TForm1::CutTextClick(TObject *Sender)
{
RichEdit1->CutToClipboard();
}
//-------------------------------------------------------------------
void __fastcall TForm1::CopyTextClick(TObject *Sender)
{
RichEdit1->CopyToClipboard();
}
//--------------------------------------------------------------------
void __fastcall TForm1::PasteTextClick(TObject *Sender)
{
RichEdit1->PasteFromClipboard();
}
//--------------------------------------------------------------------
void __fastcall TForm1::SelectAllClick(TObject *Sender)
{
RichEdit1->SelectAll();
}
//--------------------------------------------------------------------
void __fastcall TForm1::UndoClick(TObject *Sender)
{
if (RichEdit1->HandleAllocated())
SendMessage(RichEdit1->Handle, EM_UNDO, 0, 0);
}
//--------------------------------------------------------------------
void __fastcall TForm1::CleanBuffersClick(TObject *Sender)
{
for (int i = 0; i <= cbInQueue - 1; i++)
{
Buffer_I[i] = NULL;
RichEdit1->Text = Buffer_I;
}
for (int i = 0; i <= cbOutQueue - 1; i ++)
{
Buffer_O[i] = NULL;
RichEdit2->Text = Buffer_O;
}
//memset(Buffer_O, 0, cbOutQueue);
//memset(Buffer_I, 0, cbInQueue);
ProgressBar1->Max = 0;
}
//--------------------------------------------------------------------
void __fastcall TForm1::OpenCommClick(TObject *Sender)
{
if (CheckBox1->Checked == TRUE) // wybór portu
lpFileName = "COM1";
if (CheckBox2->Checked == TRUE)
lpFileName = "COM2";
hCommDev = CreateFile(lpFileName, GENERIC_READ | GENERIC_WRITE,
0, NULL, OPEN_EXISTING, 0, NULL);
if (hCommDev != INVALID_HANDLE_VALUE) // sprawdza, czy port jest
// otwarty prawidłowo
{
SetupComm(hCommDev, cbInQueue, cbOutQueue);
dcb.DCBlength = sizeof(dcb);
GetCommState(hCommDev, &dcb);
if (CheckBox3->Checked == TRUE) // wybór prędkości
dcb.BaudRate = CBR_1200;
if (CheckBox4->Checked == TRUE)
dcb.BaudRate = CBR_19200;
dcb.Parity = NOPARITY; // ustawienie parzystości
dcb.StopBits = TWOSTOPBITS; // bity stopu
dcb.ByteSize = 8; // bity danych
//-przykładowe ustawienia znaczników sterujących DCB-
dcb.fParity = TRUE;
dcb.fDtrControl = DTR_CONTROL_DISABLE;
dcb.fRtsControl = RTS_CONTROL_DISABLE;
dcb.fOutxCtsFlow = FALSE;
dcb.fOutxDsrFlow = FALSE;
dcb.fDsrSensitivity = FALSE;
dcb.fAbortOnError = FALSE;
dcb.fOutX = FALSE;
dcb.fInX = FALSE;
dcb.fErrorChar = FALSE;
dcb.fNull = FALSE;
dcb.EofChar = FALSE;
SetCommState(hCommDev, &dcb);
GetCommMask(hCommDev, &fdwEvtMask);
SetCommMask(hCommDev, EV_TXEMPTY);
}
else
{
switch ((int)hCommDev)
{
case IE_BADID:
MessageBox(NULL, "Niewłaściwa nazwa portu lub port jest"
" aktywny.", "Błąd", MB_OK);
break;
};
}
}
//--------------------------------------------------------------------
int __fastcall RS_SendFile(Pointer Parameter)
{
DWORD FileSizeHigh;
Form1->ProgressBar1->Max = 0;
if ((_lopen(Form1->OpenDialog1->FileName.c_str(),OF_READ))!=
HFILE_ERROR)
{
hfile_s =_lopen(Form1->OpenDialog1->FileName.c_str(), OF_READ);
Form1->ProgressBar1->Max = GetFileSize((HANDLE)hfile_s,
&FileSizeHigh);
while (_lread(hfile_s, &Buffer_O[0], 1))
{
Write_Comm(hCommDev, 1); // transmisja 1 bajta
Form1->ProgressBar1->StepIt();
}
_lclose(hfile_s);
FlushFileBuffers(hCommDev);
}
else
MessageBox(NULL, "Nie wybrano pliku do transmisji.", "Błąd !",
MB_OK);
return TRUE;
}
//--------------------------------------------------------------------
void __fastcall TForm1::SendFileClick(TObject *Sender)
{
if (hCommDev > 0)
{
hThread_SF = BeginThread (NULL, 0, RS_SendFile, NULL,
0, uThreadID_SF);
}
else
MessageBox(NULL, "Port nie został otwarty do transmisji.",
"Błąd", MB_OK);
}
//--------------------------------------------------------------------
int __fastcall RS_Receive(Pointer Parameter)
{
bResult = Read_Comm(hCommDev, &Number_Bytes_Read,
sizeof(Buffer_I));
if(bResult && Number_Bytes_Read != 0)
Form1->RichEdit2->Text = Buffer_I;
else
MessageBox(NULL, "W buforze wejściowym nie ma danych do"
" odebrania ", " Uwaga ", MB_OK);
return TRUE;
}
//--------------------------------------------------------------------
void __fastcall TForm1::ReceiveClick(TObject *Sender)
{
if (hCommDev > 0)
{
hThread_Rec = BeginThread (NULL, 0, RS_Receive, NULL,
0, uThreadID_Rec);
}
else
MessageBox(NULL, "Port nie został otwarty do transmisji.",
"Błąd", MB_OK);
}
//--------------------------------------------------------------------
int __fastcall RS_SendWritten(Pointer Parameter)
{
strcpy(Buffer_O, Form1->RichEdit2->Lines->Text.c_str());
Write_Comm(hCommDev, strlen(Buffer_O));
FlushFileBuffers(hCommDev);
return TRUE;
}
//--------------------------------------------------------------------
void __fastcall TForm1::SendWrittenClick(TObject *Sender)
{
if (hCommDev > 0)
{
hThread_SW = BeginThread (NULL, 0, RS_SendWritten, NULL,
0, uThreadID_SW);
}
else
MessageBox(NULL, "Port nie został otwarty do transmisji.",
"Błąd", MB_OK);
}
//--------------------------------------------------------------------
Posługując się przedstawionymi do tej pory algorytmami, jesteśmy w stanie przetransmitować pliki znacznych rozmiarów, nie troszcząc się zbytnio o bufor wyjściowy. Jeżeli interesuje nas jedynie przesłanie pliku, rozmiar bufora wyjściowego możemy tak naprawdę ustalić na 1 bajt (cbOutQueue = 1) — tyle ile pobieramy z dysku za pomocą funkcji _lread(). Jej użycie wydłuża oczywiście czas transmisji, niemniej jednak będziemy mieli pewność, że przeznaczony do wysłania zbiór danych dojdzie do adresata w całości. Należy dodać, że istnieje znacznie szybszy sposób przetransferowania porcji danych. W tym celu można od razu załadować zawartość okna edycji do bufora wyjściowego w sposób identyczny, jak w przypadku danych wysyłanych z klawiatury. Przykładowa funkcja mogłaby przybrać następującą postać:
int __fastcall RS_SendFile(Pointer Parameter)
{
...
strcpy(Buffer_O, Form1->RichEdit1->Lines->Text.c_str());
Write_Comm(hCommDev, strlen(Buffer_O));
return TRUE;
}
Niemniej jednak postępując w ten sposób, musimy już pamiętać o zadeklarowaniu odpowiedniego bufora dla danych wyjściowych. Ponadto należy zdawać sobie sprawę, że istnieje zawsze niebezpieczeństwo bardzo szybkiego przepełnienia bufora wejściowego odbiornika, jeżeli oczywiście nie używa on specjalnych protokołów transmisji.
Dalsze omawianie zwartości powyższego wydruku nie będzie już chyba na naszym etapie rozważań wnosić nic nowego. Są jeszcze pewne subtelności związane z zastosowanym przez nas sposobem wykorzystania wątków w programach komunikacyjnych. Omówimy je już za chwilę.
Zamiast Timera
Kłopoty z tytułowym bohaterem tego podrozdziału nie są oczywiście własnością jedynie Delphi. Testując napisany wcześniej program (projekt p_RS_07.bpr), na pewno zauważyliśmy, że nie pracował on w sposób wybitnie elegancki. Działo się to również za sprawą użycia w nim komponentu TCSpinEdit, którego działanie bez szeregu uprzednich zabezpieczeń może być nieco zdradliwe. Zapewne większość Czytelników już się z tym zapoznała, testując tamtą aplikację. Lekarstwem na te zmartwienia może okazać się użycie w odpowiednim miejscu programu dwóch instrukcji do...while (lub oczywiście samych while), przy czym zewnętrzna, z logicznego punktu widzenia, nigdy nie może ulec zakończeniu. Funkcja RS_Send_Receive(), będąca integralną częścią naszej nowej aplikacji, której projekt widzimy na rysunku 6.4 jest właśnie tak zbudowana. Kolejną modyfikacją, jaką zastosowałem, jest użycie komponentu TUpDown. Ma on taką ciekawą własność, że wartość jego cechy Position może być wyświetlana tylko pośrednio, np. w polu edycji TEdit. Wówczas nawet złośliwe skasowanie zawartości danego pola edycji w żadnym razie nie wpłynie na sposób funkcjonowania programu, gdyż aktualna wartość jego wymienionej cechy będzie zawsze argumentem funkcji opóźniającej Sleep(). Całość została uzupełniona o dobrze znaną nam funkcję tworzącą wątek, którą wywołujemy w funkcji obsługi zdarzenia MeasureONClick().
Rysunek 6.4. Formularz projektu p_RS_09.bpr
|
|
Wydruk 6.4. Kod modułu RS_09.cpp aplikacji wykorzystującej elementy wielowątkowości przy obsłudze woltomierza cyfrowego
//--- kompilować z borlndmm.dll oraz cc3250mt.dll --------------
//----RS_09.cpp-------------
#include <vcl.h>
#pragma hdrstop
#include "RS_09.h"
#pragma package(smart_init)
#pragma resource "*.dfm"
#define cbOutQueue 16 //rozmiar bufora danych wyjściowych
#define cbInQueue 16 //rozmiar bufora danych wejściowych
TForm1 *Form1;
LPCTSTR query = "CDAT?\r\n"; // zapytanie o mierzone napięcie
char Buffer_O[cbOutQueue]; // bufor danych wyjściowych
char Buffer_I[cbInQueue]; // bufor danych wejściowych
DWORD Number_Bytes_Read; // liczba bajtów do czytania
HANDLE hCommDev; // identyfikator portu
LPCTSTR lpFileName;
DCB dcb;
DWORD fdwEvtMask;
COMSTAT Stat;
DWORD Errors;
BOOL bResult = TRUE;
int hThread_SR;
unsigned ThreadID_SR;
Cardinal intVar; // licznik pomiaru
//--------------------------------------------------------------------
int __fastcall Close_Comm(HANDLE hCommDev)
{
CloseHandle(hCommDev);
return TRUE;
}
//--------------------------------------------------------------------
int __fastcall Write_Comm(HANDLE hCommDev,
DWORD nNumberOfBytesToWrite)
{
DWORD NumberOfBytesWritten;
if (WriteFile(hCommDev, &Buffer_O[0], nNumberOfBytesToWrite,
&NumberOfBytesWritten , NULL) > 0)
{
WaitCommEvent(hCommDev, &fdwEvtMask, NULL);
return TRUE;
}
else
return FALSE;
}
//--------------------------------------------------------------------
int __fastcall Read_Comm(HANDLE hCommDev,
LPDWORD lpNumberOfBytesRead, DWORD Buf_Size)
{
DWORD nNumberOfBytesToRead;
ClearCommError(hCommDev, &Errors ,&Stat);
if (Stat.cbInQue > 0)
{
if (Stat.cbInQue > Buf_Size)
nNumberOfBytesToRead = Buf_Size;
else
nNumberOfBytesToRead = Stat.cbInQue;
ReadFile(hCommDev, &Buffer_I[0], nNumberOfBytesToRead,
lpNumberOfBytesRead, NULL);
}
else
*lpNumberOfBytesRead = 0;
return TRUE;
}
//--------------------------------------------------------------------
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
}
//--------------------------------------------------------------------
void __fastcall TForm1::CloseCommClick(TObject *Sender)
{
switch(MessageBox(NULL, " Działanie aplikacji zostanie"
" zakończone.", "Uwaga!",
MB_YESNOCANCEL | MB_ICONQUESTION))
{
case ID_YES :
{
SuspendThread((HANDLE)hThread_SR);
Close_Comm(hCommDev);
Application->Terminate();
}
case ID_CANCEL : Abort();
}
}
//--------------------------------------------------------------------
void __fastcall TForm1::FormCreate(TObject *Sender)
{
OpenComm->Enabled = TRUE;
UpDown1->Position = 1000;
Edit1->Text = "1000";
intVar = 0;
}
//--------------------------------------------------------------------
void __fastcall TForm1::OpenCommClick(TObject *Sender)
{
if (CheckBox1->Checked == TRUE) // wybór portu
lpFileName = "COM1";
if (CheckBox2->Checked == TRUE)
lpFileName = "COM2";
hCommDev = CreateFile(lpFileName, GENERIC_READ | GENERIC_WRITE,
0, NULL, OPEN_EXISTING, 0, NULL);
if (hCommDev != INVALID_HANDLE_VALUE) // sprawdza, czy port jest
// otwarty prawidłowo
{
SetupComm(hCommDev, cbInQueue, cbOutQueue);
dcb.DCBlength = sizeof(dcb);
GetCommState(hCommDev, &dcb);
if (CheckBox3->Checked == TRUE) // wybór prędkości transmisji
dcb.BaudRate = CBR_300;
if (CheckBox4->Checked == TRUE)
dcb.BaudRate = CBR_1200;
if (CheckBox5->Checked == TRUE)
dcb.BaudRate = CBR_9600;
dcb.Parity = ODDPARITY; // ustawienie parzystości
dcb.StopBits = ONESTOPBIT; // bity stopu
dcb.ByteSize = 7; // bity danych
//-przykładowe ustawienia znaczników sterujących DCB-
dcb.fParity = TRUE; // sprawdzanie parzystości
dcb.fDtrControl = DTR_CONTROL_DISABLE;
dcb.fRtsControl = RTS_CONTROL_DISABLE;
dcb.fOutxCtsFlow = FALSE;
dcb.fOutxDsrFlow = FALSE;
dcb.fDsrSensitivity = FALSE;
dcb.fAbortOnError = FALSE;
dcb.fOutX = FALSE;
dcb.fInX = FALSE;
dcb.fErrorChar = FALSE;
dcb.fNull = FALSE;
SetCommState(hCommDev, &dcb);
GetCommMask(hCommDev, &fdwEvtMask);
SetCommMask(hCommDev, EV_TXEMPTY);
}
else
{
switch ((int)hCommDev)
{
case IE_BADID:
MessageBox(NULL, "Niewłaściwa nazwa portu lub port jest"
" aktywny.", "Błąd", MB_OK);
break;
};
}
}
//--------wysłanie zapytania i odbiór danych--------------------------
int __fastcall RS_Send_Receive(Pointer Parameter)
{
do {
do { //-- wysyłanie zapytania
//Beep();
FlushFileBuffers(hCommDev);
} while (Write_Comm(hCommDev, strlen(Buffer_O)) == 0);
Sleep(Form1->UpDown1->Position);
//-- odbiór danych
Read_Comm(hCommDev, &Number_Bytes_Read, sizeof(Buffer_I));
if (Number_Bytes_Read >0)
{
Form1->RichEdit2->Text = IntToStr(intVar++);
Form1->RichEdit1->Text = Buffer_I;
}
else
{
Beep();
Form1->RichEdit1->Text = "0x"; // błędna wartość pomiaru
}
} while (bResult); // koniec nadrzędnego DO
return TRUE;
}
//----------------pomiar----------------------------------------------
void __fastcall TForm1::MeasureONClick(TObject *Sender)
{
if (hCommDev > 0) // powtórnie sprawdza, czy port jest otwarty
{
OpenComm->Enabled = FALSE;
strcpy(Buffer_O, query);
hThread_SR = BeginThread (NULL, 0, RS_Send_Receive, NULL, 0,
ThreadID_SR);
}
else
MessageBox(NULL, "Port nie został otwarty do transmisji.",
"Błąd", MB_OK);
}
//--------------wznowienie pomiaru------------------------------------
void __fastcall TForm1::MeasureResumeClick(TObject *Sender)
{
ResumeThread((HANDLE)hThread_SR);
}
//---------------wstrzymanie pomiaru----------------------------------
void __fastcall TForm1::MeasureSuspendClick(TObject *Sender)
{
SuspendThread((HANDLE)hThread_SR);
}
//---------------synchronizacja---------------------------------------
void __fastcall TForm1::UpDown1Click(TObject *Sender, TUDBtnType
Button)
{
Edit1->Text = IntToStr(UpDown1->Position); // sterowanie
// opóźnieniem
}
//--------------------------------------------------------------------
Uważnie śledząc zapisy niektórych algorytmów zamieszczonych w naszej książce, na pewno wielu z Czytelników zauważyło, iż możliwym jest alternatywny sposób odwołania się do Write_Comm(), który w funkcji RS_Send_Receive() może przybrać formę:
BOOL bResult_Write = FALSE;
...
//-- wysyłanie zapytania
do {
FlushFileBuffers(hCommDev);
bResult_Write = Write_Comm(hCommDev, strlen(Buffer_O));
} while ( ! bResult_Write );
Pokazana metoda wysyłania zapytań i rozkazów w pętli do...while jest na pewno bardziej przejrzysta, niemniej jednak nie powinniśmy zauważyć większych różnic w tempie działania algorytmu, posługując się konstrukcją taką jak na wydruku 6.4 oraz zaprezentowaną powyżej. Inaczej będzie podczas odbierania komunikatów. Można by pomyśleć, iż równie dobrze funkcja Read_Comm() będzie działać w pętli skonstruowanej według następującego przepisu:
BOOL bResult_Read = FALSE;
...
//-- odbiór danych
do {
bResult_Read = Read_Comm(hCommDev, &Number_Bytes_Read,
sizeof(Buffer_I));
Form1->RichEdit2->Text = IntToStr(intVar++);
Form1->RichEdit1->Text = Buffer_I;
} while ( ! bResult_Read );
if ( ! bResult_Read )
Form1->RichEdit1->Text = "0x"; // błędna wartość pomiaru
Niewątpliwie, sam proces odczytu danych z portu przebiegać będzie prawidłowo. Niemniej jednak będziemy mieli problem ze zdiagnozowaniem ewentualnych błędów w trakcie odbioru komunikatów. Wynika to z prostego faktu — nigdy nie wiemy, kiedy tak naprawdę urządzenie zacznie przesyłać kompletną odpowiedź, zaś nasza funkcja Read_Comm() wywoływana jest w pętli cyklicznie, przez co w buforze wejściowym może nieustannie pojawiać się jakiś znak, co spowoduje, że warunek instrukcji if nie będzie spełniony.
Podobnie jak uczyniliśmy w Delphi, również w przedstawionym na wydruku 6.4 przykładzie funkcję RS_Send_Receive() można zapisać w nieco odmienny, ale równie poprawny sposób za pomocą samych instrukcji while. Musi być tu zawsze spełniony warunek kontynuacji:
int __fastcall RS_Send_Receive(Pointer Parameter)
{
while (bResult)
{
while (Write_Comm(hCommDev, strlen(Buffer_O)) == 0)
{
//Beep();
FlushFileBuffers(hCommDev);
}
Sleep(Form1->UpDown1->Position);
//-- odbiór danych
Read_Comm(hCommDev, &Number_Bytes_Read, sizeof(Buffer_I));
if (Number_Bytes_Read > 0)
{
Form1->RichEdit2->Text = IntToStr(intVar++);
Form1->RichEdit1->Text = Buffer_I;
}
else
{
Beep();
Form1->RichEdit1->Text = "0x"; // błędna wartość pomiaru
}
} // koniec nadrzędnego while
return TRUE;
}
Jest jeszcze jeden, na pozór drobny szczegół w wykorzystaniu w Builderze pseudoidentyfikatora wątku. W naszym przykładzie hThread_SR zadeklarowany został jako typ całkowity int. Deklaracja ta zgodna była z obowiązującymi w C++Builderze regułami implementacji funkcji BeginThread(). Pewną trudność napotkamy jednak, gdy zechcemy manipulować własnościami takiego pseudoidentyfikatora. Bardzo dobrym przykładem mogą być funkcje SuspendThread() oraz ResumeThread(). Testując nasz ostatni program, łatwo się przekonamy, że argumentami ich nie mogą być po prostu liczby całkowite. Jeżeli ktoś zechciałby dokładnie to sprawdzić, wystarczy w jakimś miejscu kodu wywołać jedną z tych funkcji w sposób następujący:
SuspendThread(hThread_SR);
Podane przez kompilator komunikaty o błędzie nie powinny nas zaskoczyć:
[C++ Error] RS_09.cpp(234): E2034 Cannot convert 'int' to 'void *'
[C++ Error] RS_09.cpp(234): E2342 Type mismatch in parameter
'hThread' (wanted 'void *', got 'int')
Okazuje się, że argumentami tych funkcji muszą być dane typu void * (LPVOID), zdolne do lokalizowania pewnych obszarów pamięci operacyjnej. Jeżeli przypomnimy sobie, co powiedzieliśmy na początku rozdziału piątego o danych typu HANDLE, będziemy mieli pełny obraz sytuacji. Jeżeli dla tego przypadku zadeklarowaliśmy zmienną typu int, to w celu zapewnienia poprawności wykonania omówionych funkcji należy wykorzystać po prostu metodę rzutowania typów, tak jak zostało to wykonane w funkcjach obsługi zdarzeń MeasureResumeClick() oraz MeasureSuspendClick(), które z powodzeniem mogą przyjąć równoważną postać:
//---------------wznowienie pomiaru----------------------------------
void __fastcall TForm1::MeasureResumeClick(TObject *Sender)
{
ResumeThread((LPVOID)hThread_SR);
}
//---------------wstrzymanie pomiaru----------------------------------
void __fastcall TForm1::MeasureSuspendClick(TObject *Sender)
{
SuspendThread((LPVOID)hThread_SR);
}
Bez trudu zauważymy, że w pewnych przypadkach kompilator nie rozróżnia danych typu LPVOID oraz HANDLE. Jeżeli ktoś zechce być do końca dociekliwy, na pewno spróbuje identyfikator portu zadeklarować w sposób następujący:
LPVOID hCommDev;
Rezultat działania programu z tak określonym identyfikatorem portu powinien skłonić do pewnej refleksji osoby nadal uważające, że „uchwyt” HANDLE jest numerem nadanym określonemu portowi komunikacyjnemu (plikowi, oknu w programie lub innemu obiektowi Win32).
Podsumowanie
W niniejszym rozdziale zostały przedstawione informacje na temat rzadziej spotykanych w literaturze metod wykorzystania elementów techniki programowania wielowątkowego w aplikacjach realizujących transmisję szeregową. Chociaż wykorzystaliśmy bardzo prostą funkcję BeginThread() zdolną do generowania nowych wątków, jednak już samo jej odpowiednie użycie sprawiło, że posługujące się nią aplikacje komunikacyjne są bardziej funkcjonalne. Istnieje oczywiście możliwość pełnej implementacji zarówno w Delphi jak i Builderze klasy TThread. Trzeba jednak stwierdzić, że technika programowego wykorzystania wymienionej klasy jest szeroko opisana w dostępnych publikacjach, więc samodzielne próby jej użycia nie powinny sprawić Czytelnikom większych kłopotów. Opisane sposoby posługiwania się funkcją BeginThread() mają jeszcze jedna zaletę — nie potrzebujemy martwić się skomplikowanym ustalaniem priorytetów wątków jak również różnymi metodami ich synchronizacji. Możliwość samodzielnego ustalania priorytetu wątku jest jedną z głównych zalet techniki programowania współbieżnego. --> Jednak w aplikacji sterującej konkretnym przyrządem pomiarowym trudno byłoby wprowadzać tego typu zależności. Na jakiej zasadzie można zaprogramować wyższy (lub niższy) priorytet np. dla funkcji wysyłającej komendy do urządzenia oraz funkcji czytającej jego wskazania? [Author:kdh]
Przy prezentowanym sposobie deklaracji funkcji RS_SendFile(P: Pointer) użycie operatora @ nie jest wymagane, natomiast gdyby była ona bezparametrowa, należy go wykorzystać.
Użycie ich w Win NT wymaga, aby identyfikator nowego wątku został przydzielony wraz z rodzajem dostępu THREAD_SUSPEND_RESUME.
Pisząc tego rodzaju aplikacje, należy zawsze wybierać komponenty umożliwiające pośrednie wyświetlanie wybranej wartości liczbowej w trakcie działania programu. Unikamy w ten sposób konsekwencji związanych z przypadkowym skasowaniem zawartości komponentu edycyjnego.
2 Część I ♦ Podstawy obsługi systemu WhizBang (Nagłówek strony)
H:\Książki\!Lukasz\RS 232. Praktyczne programowanie\4 po jezykowej\R_6.doc
229
brak bezpośredniego nawiązania do postawionego pytania — proponuję usunięcie zaznaczonego fragmentu (bez szkody dla kompozycji podsumowania!)