第五節:多個線程同時執行相同的任務
1.鎖
設,有一個房間 X ,X為全局變量,它有兩個函數 X.Lock 與 X.UnLock;
有如下代碼:
X
.
Lock;
訪問資源 P;
X
.
Unlock;
現在有A,B兩個線程時空都要執行此段代碼。
當線程A執行了 X.Lock 之后,在沒有執行完 X.Unlock 之前,第二個線程B此時也來執行 X.Lock ,
線程B就會阻塞在 X.Lock 這句代碼上。我們可以認為,此時,線程A進入房間,其它線程不准再進入房間。
只能在外面等,直到線程A執行完 X.Unlock 后,線程A退出了房間,此時線程B才可以進入。
線程B進入了房間后,其它線程此時同樣不准再進入。
即:
多個線程用本段代碼“訪問資源P”的操作是排隊執行的。
2. TMonitor
在 delphi XE2 及以后的版本中,提供了一個方便的鎖功能。TMonitor。
它是一個Record, TMonitor.Enter(X); 與 TMoniter.Exit(X); 等效於上面 lock 與 unlock;
X 可以是任何一個 TObject 實例。
本例源碼下載(delphi XE8版本):
FooMuliThread.zip
unit
uCountThread;
interface
uses
uFooThread;
type
TCountThread =
class
;
TOnGetNum =
function
(Sender: TCountThread):
boolean
of
object
;
//獲取 Num 事件。
TOnCounted =
procedure
(Sender: TCountThread)
of
object
;
TCountThread =
class
(TFooThread)
private
procedure
Count;
procedure
DoOnCounted;
function
DoOnGetNum:
boolean
;
public
procedure
StartThread; override;
public
Num:
integer
;
Total:
integer
;
OnCounted: TOnCounted;
OnGetNum: TOnGetNum;
ThreadName:
string
;
end
;
implementation
{ TCountThread }
procedure
TCountThread
.
Count;
var
i:
integer
;
begin
// 注意多線程不適合打斷點調試。
// 因為一旦在 IDE 中斷后,狀態全亂了。
// 可以寫 Log 或用腦袋想,哈哈。
if
DoOnGetNum
then
// 獲取參數 Num
begin
Total :=
0
;
if
Num >
0
then
for
i :=
1
to
Num
do
begin
Total := Total + i;
sleep(
5
);
//嫌慢就刪掉此句。
end
;
DoOnCounted;
// 引發 OnCounted 事件,告知調用者。
ExecProcInThread(Count);
// 上節說到在線程時空里執行本句。
end
;
end
;
procedure
TCountThread
.
DoOnCounted;
begin
if
Assigned(OnCounted)
then
OnCounted(self);
end
;
function
TCountThread
.
DoOnGetNum:
boolean
;
begin
result :=
false
;
if
Assigned(OnGetNum)
then
result := OnGetNum(self);
end
;
procedure
TCountThread
.
StartThread;
begin
inherited
;
ExecProcInThread(Count);
// 把 Count 過程塞到線程中運行。
end
;
end
.
unit
uFrmMain;
interface
uses
Winapi
.
Windows, Winapi
.
Messages, System
.
SysUtils, System
.
Variants, System
.
Classes,
Graphics,
Vcl
.
Controls, Vcl
.
Forms, Vcl
.
Dialogs, Vcl
.
StdCtrls, uCountThread;
type
TFrmMain =
class
(TForm)
memMsg: TMemo;
edtNum: TEdit;
btnWork: TButton;
lblInfo: TLabel;
procedure
FormCreate(Sender: TObject);
procedure
FormDestroy(Sender: TObject);
procedure
btnWorkClick(Sender: TObject);
procedure
FormCloseQuery(Sender: TObject;
var
CanClose:
Boolean
);
private
{ Private declarations }
FCo1, FCo2, FCo3: TCountThread;
// 定義了3個線程實例
// 以后章節將講解采用 List 容器來裝線程實例。
FBuff: TStringList;
FBuffIndex:
integer
;
FBuffMaxIndex:
integer
;
FWorkedCount:
integer
;
procedure
DispMsg(AMsg:
string
);
procedure
OnThreadMsg(AMsg:
string
);
function
OnGetNum(Sender: TCountThread):
Boolean
;
procedure
OnCounted(Sender: TCountThread);
procedure
LockBuffer;
procedure
UnlockBuffer;
procedure
LockCount;
procedure
UnlockCount;
public
{ Public declarations }
end
;
var
FrmMain: TFrmMain;
implementation
{
$R
*.dfm}
{ TFrmMain }
procedure
TFrmMain
.
btnWorkClick(Sender: TObject);
var
s:
string
;
begin
btnWork
.
Enabled :=
false
;
FWorkedCount :=
0
;
FBuffIndex :=
0
;
FBuffMaxIndex := FBuff
.
Count -
1
;
s :=
'共'
+ IntToStr(FBuffMaxIndex +
1
) +
'個任務,已完成:'
+ IntToStr(FWorkedCount);
lblInfo
.
Caption := s;
FCo1
.
StartThread;
FCo2
.
StartThread;
FCo3
.
StartThread;
end
;
procedure
TFrmMain
.
DispMsg(AMsg:
string
);
begin
memMsg
.
Lines
.
Add(AMsg);
end
;
procedure
TFrmMain
.
FormCloseQuery(Sender: TObject;
var
CanClose:
Boolean
);
begin
// 防止計算期間退出
LockCount;
// 請思考,這里為什么要用 LockCount;
CanClose := btnWork
.
Enabled;
if
not
btnWork
.
Enabled
then
DispMsg(
'正在計算,不准退出!'
);
UnlockCount;
end
;
procedure
TFrmMain
.
FormCreate(Sender: TObject);
begin
FCo1 := TCountThread
.
Create(
false
);
FCo1
.
OnStatusMsg := self
.
OnThreadMsg;
FCo1
.
OnGetNum := self
.
OnGetNum;
FCo1
.
OnCounted := self
.
OnCounted;
FCo1
.
ThreadName :=
'線程1'
;
FCo2 := TCountThread
.
Create(
false
);
FCo2
.
OnStatusMsg := self
.
OnThreadMsg;
FCo2
.
OnGetNum := self
.
OnGetNum;
FCo2
.
OnCounted := self
.
OnCounted;
FCo2
.
ThreadName :=
'線程2'
;
FCo3 := TCountThread
.
Create(
false
);
FCo3
.
OnStatusMsg := self
.
OnThreadMsg;
FCo3
.
OnGetNum := self
.
OnGetNum;
FCo3
.
OnCounted := self
.
OnCounted;
FCo3
.
ThreadName :=
'線程3'
;
FBuff := TStringList
.
Create;
// 構造一組數據用來測試
FBuff
.
Add(
'100'
);
FBuff
.
Add(
'136'
);
FBuff
.
Add(
'306'
);
FBuff
.
Add(
'156'
);
FBuff
.
Add(
'152'
);
FBuff
.
Add(
'106'
);
FBuff
.
Add(
'306'
);
FBuff
.
Add(
'156'
);
FBuff
.
Add(
'655'
);
FBuff
.
Add(
'53'
);
FBuff
.
Add(
'99'
);
FBuff
.
Add(
'157'
);
end
;
procedure
TFrmMain
.
FormDestroy(Sender: TObject);
begin
FCo1
.
Free;
FCo2
.
Free;
FCo3
.
Free;
end
;
procedure
TFrmMain
.
LockBuffer;
begin
System
.
TMonitor
.
Enter(FBuff);
// System 是單元名。因為 TMonitor 在 Forms 中也有一個相同的名字。
// 同名的類與函數,就要在前面加單元名稱以示區別。
end
;
procedure
TFrmMain
.
LockCount;
begin
// 任意一個 TObject 就行,所以我用了 btnWork
System
.
TMonitor
.
Enter(btnWork);
end
;
procedure
TFrmMain
.
OnCounted(Sender: TCountThread);
var
s:
string
;
begin
LockCount;
// 此處亦可以用 LockBuffer
// 但是,鎖不同的對象,宜用不同的鎖。
// 每把鎖的功能要單一,鎖的粒度要最小化。才能提高效率。
s := Sender
.
ThreadName +
':'
+ IntToStr(Sender
.
Num) +
'累加和為:'
;
s := s + IntToStr(Sender
.
Total);
OnThreadMsg(s);
inc(FWorkedCount);
s :=
'共'
+ IntToStr(FBuffMaxIndex +
1
) +
'個任務,已完成:'
+ IntToStr(FWorkedCount);
TThread
.
Synchronize(
nil
,
procedure
begin
lblInfo
.
Caption := s;
end
);
if
FWorkedCount >= FBuffMaxIndex +
1
then
begin
TThread
.
Synchronize(
nil
,
procedure
begin
DispMsg(
'已計算完成'
);
btnWork
.
Enabled :=
true
;
// 恢復按鈕狀態。
end
);
end
;
UnlockCount;
end
;
function
TFrmMain
.
OnGetNum(Sender: TCountThread):
Boolean
;
begin
LockBuffer;
// 將多個線程訪問 FBuff 排隊。
try
if
FBuffIndex > FBuffMaxIndex
then
begin
result :=
false
;
end
else
begin
Sender
.
Num := StrToInt(FBuff[FBuffIndex]);
result :=
true
;
inc(FBuffIndex);
end
;
finally
UnlockBuffer;
end
;
end
;
procedure
TFrmMain
.
OnThreadMsg(AMsg:
string
);
begin
TThread
.
Synchronize(
nil
,
procedure
begin
DispMsg(AMsg);
end
);
end
;
procedure
TFrmMain
.
UnlockBuffer;
begin
System
.
TMonitor
.
Exit(FBuff);
end
;
procedure
TFrmMain
.
UnlockCount;
begin
System
.
TMonitor
.
Exit(btnWork);
end
;
end
.
下一節,我們將學習 List 與泛型。為以后設計其它的更高級與靈活的線程做准備。