當你有兩個或者兩個以上的線程同時運行,並且他們共同操作的同一塊數據時,我們必須要對其進行保護,否則因為時間切片或者一些其他原因造成的延時都會讓你的程序產生錯誤的計算結果。即使做兩個線程訪問共享整數變量這樣簡單的事情也可能導致完全的災難
線程之間共享什麼數據
首先,值得確切知道每個進程和每個線程存儲的狀態。每個線程都有自己的程序計數器和處理器狀態。這意味着線程通過代碼獨立進行。每個線程也有自己的堆棧,因此局部變量本身就是每個線程的本地變量,並且這些變量不存在同步問題。
程序中的全局數據可以在線程之間自由共享,因此這些變量可能存在同步問題。當然,如果變量是全局可訪問的,但只有一個線程使用它,則沒有問題。
Delphi提供了threadvar關鍵字。這允許聲明全局變量,其中為每個線程創建變量的副本。此功能使用不多,因為將這些變量放在TThread類中通常更方便,因此為每個創建的TThread後代創建一個變量實例。
共享數據的原子性
為了理解如何使線程協同工作,有必要理解原子性的概念。
所謂的原子性是指某一組動作是一個整體,它不可分割,要麼一起成功,要麼一起失敗。就好比銀行轉賬,轉賬這個東西由兩步完成取出、存入,必須兩個都成功才可以算成功,不允許出現一半成功一半失敗
當線程執行原子操作時,這意味着所有其他線程將操作視為尚未啟動或已完成。一個線程不可能在「行為」中捕獲另一個線程。如果線程之間沒有執行同步,那麼幾乎所有操作都是非原子的。我們舉一個簡單的例子。考慮 這段代碼
var
a: integer;
begin
a := a + 1;
end;
如果兩個單獨的線程使用它來遞增共享變量A,即使是這些微不足道的代碼也會導致問題。這個單個pascal語句在彙編程序級別分解為三個操作。
- 從存儲器讀取A到處理器寄存器。
- 將1添加到處理器寄存器。
- 將處理器寄存器的內容寫入內存中的A.
即使在單個處理器機器上,多個線程執行此代碼也可能導致問題。之所以這樣做是因為調度操作。當只存在一個處理器時,實際上只有一個線程 一次執行,但Win32調度程序在它們之間以每秒約18次的速度切換。
調度程序可以在任何時候停止一個線程運行並啟動另一個線程調度是先發制人的。在掛起一個線程並啟動另一個線程之前,操作系統不會等待權限交換機可能隨時發生。由於切換可以在任何兩個處理器指令之間發生,因此它可能發生在函數中間的臨界點,甚至是執行一個特定程序語句的一半。
讓我們假設兩個線程正在單處理器機器(X和Y)上執行示例代碼。在一個很好的情況下,程序可能正在運行,並且調度操作可能會錯過這個臨界點,給出預期結果:A增加2。但是,這絕不是保證,而是盲目的機會如果共享變量碰巧是一個指針,那結果可能會讓人崩潰。
說了一大堆理論,下面以代碼的方式來看看上述的情況會不會出現,為了便於觀查我沒有使用視頻中案例,而是採用了控制台應用
uses
System.SysUtils, System.Classes;
type
TWorkThread = class(TThread)
protected
procedure Execute; override;
end;
var
// 定義全局變量,充當共享數據
Num: Integer = 0;
{ TWorkThread }
procedure TWorkThread.Execute;
begin
// 循環的方式自增Num
while True do begin
//為了效果更為明顯加入了延時
TThread.Sleep(100);
// 當Num的值大於10則終止線程
if (Num > 10) then
Exit;
Writeln(Num);
Inc(Num);
end;
end;
begin
//啟動3個線程
TWorkThread.Create(False);
TWorkThread.Create(False);
TWorkThread.Create(False);
Readln;
end.
執行結果如下

篇幅的原因,我截取其中的一段結果,但是已經足以說明問題所在
解決方案
對於我們自己來說解決起來好像確實很麻煩,好在我們的前輩已經提供了解決方案。不學不知道,一學嚇一跳,Delphi針對線程安全問題提供了不止一種解決方案。
臨界區
臨界區是一種最直接的線程同步方式。所謂臨界區,簡單的說就是有一塊區域,而在該區域內的代碼只能有一個線程在執行。針對臨界區的使用Delphi中有兩種方式使用EnterCriticalSection( ) 和LeaveCriticalSection( ) API 函數,另外一種是使用 TCriticalSection 類,我個人推薦使用TCriticalSection 因為該類對API進行了封裝使用更為便捷。所在的單元為「SyncObjs」
在理清臨界區的概念之後我們改組上述代碼
program Project1;
{$APPTYPE CONSOLE}
{$R *.res}
uses
SyncObjs, System.SysUtils, System.Classes;
type
TWorkThread = class(TThread)
protected
procedure Execute; override;
public
end;
var
// 定義全局變量,充當共享數據
Num: Integer = 0;
var
{ 聲明臨界 }
CS: TCriticalSection;
{ TWorkThread }
procedure TWorkThread.Execute;
begin
// 循環的方式自增Num
while True do begin
TThread.Sleep(100);
// 臨界區開始
CS.Enter;
// 當Num的值大於10則終止線程
if (Num > 10) then
Exit;
Writeln(TThread.CurrentThread.ThreadID.ToString + ':' + Num.ToString);
Inc(Num);
// 臨界區結束
CS.Leave;
end;
end;
begin
//初始化臨界區
CS := TCriticalSection.Create;
TWorkThread.Create(False);
TWorkThread.Create(False);
TWorkThread.Create(False);
Readln;
end.
互斥對象
uses SyncObjs;用TMutex類的方法處理(把釋放語句放在循環內外可以決定執行順序)
例:互斥輸出三個0~2000的數字到窗體在不同位置。
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls;
type
TMyThread = class(TThread)
private
{ Private declarations }
protected
procedure Execute; override; {執行}
procedure Run; {運行}
end;
TForm1 = class(TForm)
btn1: TButton;
procedure FormDestroy(Sender: TObject);
procedure btn1Click(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
uses SyncObjs;
var
MyThread:TMyThread; {聲明線程}
Mutex:TMutex; {聲明互斥體}
f:integer;
procedure TMyThread.Execute;
begin
{ Place thread code here }
FreeOnTerminate:=True; {加上這句線程用完了會自動注釋}
Run; {運行}
end;
procedure TMyThread.Run;
var
i,y:integer;
begin
Inc(f);
y:=20*f;
for i := 0 to 2000 do
begin
if Mutex.WaitFor(INFINITE)=wrSignaled then {判斷函數,能用時就用}
begin
Form1.Canvas.Lock;
Form1.Canvas.TextOut(10,y,IntToStr(i));
Form1.Canvas.Unlock;
Sleep(1);
Mutex.Release; {釋放,誰來接下去用}
end;
end;
end;
procedure TForm1.btn1Click(Sender: TObject);
begin
f:=0;
Repaint;
Mutex:=TMutex.Create(False); {參數為是否讓創建者擁有該互斥體,一般為False}
MyThread:=TMyThread.Create(False);
MyThread:=TMyThread.Create(False);
MyThread:=TMyThread.Create(False);
end;
procedure TForm1.FormDestroy(Sender: TObject);
begin
Mutex.Free;{釋放互斥體}
end;
end.
Semaphore(信號或叫信號量)
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls;
type
TForm1 = class(TForm)
Button1: TButton;
Edit1: TEdit;
procedure Button1Click(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
procedure Edit1KeyPress(Sender: TObject; var Key: Char);
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
uses SyncObjs;
var
f: Integer;
MySemaphore: TSemaphore;
function MyThreadFun(p: Pointer): DWORD; stdcall;
var
i,y: Integer;
begin
Inc(f);
y := 20 * f;
if MySemaphore.WaitFor(INFINITE) = wrSignaled then
begin
for i := 0 to 1000 do
begin
Form1.Canvas.Lock;
Form1.Canvas.TextOut(20, y, IntToStr(i));
Form1.Canvas.Unlock;
Sleep(1);
end;
end;
MySemaphore.Release;
Result := 0;
end;
procedure TForm1.Button1Click(Sender: TObject);
var
ThreadID: DWORD;
begin
if Assigned(MySemaphore) then MySemaphore.Free;
MySemaphore := TSemaphore.Create(nil, StrToInt(Edit1.Text), 5, ''); {創建,參數一為安全默認為nil,參數2可以填寫運行多少線程,參數3是運行總數,參數4可命名用於多進程}
Self.Repaint;
f := 0;
CreateThread(nil, 0, @MyThreadFun, nil, 0, ThreadID);
CreateThread(nil, 0, @MyThreadFun, nil, 0, ThreadID);
CreateThread(nil, 0, @MyThreadFun, nil, 0, ThreadID);
CreateThread(nil, 0, @MyThreadFun, nil, 0, ThreadID);
CreateThread(nil, 0, @MyThreadFun, nil, 0, ThreadID);
end;
{讓 Edit 只接受 1 2 3 4 5 五個數}
procedure TForm1.Edit1KeyPress(Sender: TObject; var Key: Char);
begin
if not CharInSet(Key, ['1'..'5']) then Key := #0;
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
Edit1.Text := '1';
end;
procedure TForm1.FormDestroy(Sender: TObject);
begin
if Assigned(MySemaphore) then MySemaphore.Free;
end;
end.
Event (事件對象)
註:相比API的處理方式,此類沒有啟動步進一次後暫停的方法。
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls;
type
TMyThread = class(TThread)
private
{ Private declarations }
protected
procedure Execute; override;
procedure Run;
end;
TForm1 = class(TForm)
btn1: TButton;
btn2: TButton;
btn3: TButton;
btn4: TButton;
procedure btn1Click(Sender: TObject);
procedure FormDestroy(Sender: TObject);
procedure btn2Click(Sender: TObject);
procedure btn3Click(Sender: TObject);
procedure btn4Click(Sender: TObject);
procedure FormCreate(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
uses SyncObjs;
var
f:integer;
MyEvent:TEvent;
MyThread:TMyThread;
{ TMyThread }
procedure TMyThread.Execute;
begin
inherited;
FreeOnTerminate:=True; {線程使用完自己註銷}
Run;
end;
procedure TMyThread.Run;
var
i,y:integer;
begin
Inc(f);
y:=20*f;
for i := 0 to 20000 do
begin
if MyEvent.WaitFor(INFINITE)=wrSignaled then {判斷事件在用沒,配合事件的啟動和暫停,對事件相關線程起統一控制}
begin
Form1.Canvas.lock;
Form1.Canvas.TextOut(10,y,IntToStr(i));
Form1.Canvas.Unlock;
Sleep(1);
end;
end;
end;
procedure TForm1.btn1Click(Sender: TObject);
begin
Repaint;
f:=0;
if Assigned(MyEvent) then MyEvent.Free; {如果有,就先銷毀}
{參數1安全設置,一般為空;參數2為True時可手動控制暫停,為Flase時對象控制一次後立即暫停
參數3為True時對象建立後即可運行,為false時對象建立後控制為暫停狀態,參數4為對象名稱,用於跨進程,不用時默認''}
MyEvent:=TEvent.Create(nil,True,True,''); {創建事件}
end;
procedure TForm1.btn2Click(Sender: TObject);
var
ID:DWORD;
begin
MyThread:=TMyThread.Create(False); {創建線程}
end;
procedure TForm1.btn3Click(Sender: TObject);
begin
MyEvent.SetEvent; {啟動} {事件類沒有PulseEvent啟動一次後輕描談寫}
end;
procedure TForm1.btn4Click(Sender: TObject);
begin
MyEvent.ResetEvent; {暫停}
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
btn1.Caption:='創建事件';
btn2.Caption:='創建線程';
btn3.Caption:='啟動';
btn4.Caption:='暫停';
end;
procedure TForm1.FormDestroy(Sender: TObject);
begin
MyEvent.Free; {釋放}
end;
end.
Synchronize
最後來聊聊這個 Synchronize 函數,至於原因是將該線程的代碼放到主線程中運行,並非實際意義的線程同步。RAD Studio VCL Reference 中也有描述
Executes a method call within the main thread,Synchronize causes the call specified by AMethod() to be executed using the main thread,,thereby avoiding multi-thread conflicts。
谷歌譯文:在主線程中執行方法調用,同步導致指定的呼叫用於使用主線程執行的可用於執行的次數,從而避免多線程衝突
另外一個原因是個人感覺它不夠靈活,比如我只需要同步核心運算部分的代碼,其他部分並不需要同步的情況,所以我不太推薦。可能是我的姿勢不對,在控制台應用下無法使用,只能回到VCL中
//開啟控制台的指令
{$APPTYPE CONSOLE}
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants,
System.Classes, Vcl.Graphics,
Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls;
type
TForm1 = class(TForm)
Button1: TButton;
procedure Button1Click(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
type
TSyncThread = class(TTHread)
procedure Execute; override;
public
procedure Work();
end;
var
Num: Integer = 0;
var
Form1: TForm1;
implementation
{$R *.dfm}
{ TSyncThread }
procedure TSyncThread.Execute;
begin
inherited;
Synchronize(Work);
end;
procedure TSyncThread.Work;
begin
// 循環的方式自增Num
while True do begin
TTHread.Sleep(100);
// 當Num的值大於10則終止線程
if (Num > 10) then
Exit;
Writeln(TTHread.CurrentThread.ThreadID.ToString + ':' + Num.ToString);
Inc(Num);
end;
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
TSyncThread.Create(false);
end;
end.
至此Delphi多線程已知的同步方案結束了。通常Delphi中會提供兩種方案一是原生API方式二是Delphi本身封裝的
這篇文章也是Delphi圖文版的最後一篇文章了,至此第一季相關的內容全部更新完成
原創文章,作者:投稿專員,如若轉載,請註明出處:https://www.506064.com/zh-hk/n/258775.html