当你有两个或两个以上的线程同时运行,并且它们在同一条数据上协同工作时,我们必须保护它,否则由于时间切片或其他一些原因导致的延迟会导致你的程序产生错误的计算结果。即使做一些简单的事情,比如两个线程访问一个共享的整数变量,也会导致完全的灾难。
线程之间共享什么数据
首先,确切知道每个进程和每个线程的存储状态是值得的。每个线程都有自己的程序计数器和处理器状态。这意味着线程在代码中独立运行。每个线程也有自己的栈,所以局部变量本身就是每个线程的局部变量,与这些变量不存在同步问题。
程序中的全局数据可以在线程之间自由共享,因此这些变量可能会有同步问题。当然,如果变量是全局可访问的,但是只有一个线程使用它,那就没有问题了。
Delphi提供了threadvar关键字。这允许声明全局变量,其中为每个线程创建一个变量副本。这个函数很少使用,因为将这些变量放在TThread类中通常更方便,所以为TThread的每个创建的后代创建一个变量实例。
共享数据的原子性
为了理解线程如何协同工作,有必要理解原子性的概念。
所谓原子性,是指某一组动作是一个整体,不能分开,要么一起成功,要么一起失败。就像银行转账一样,取出和存入分两步完成转账。两者都成功了才算成功,不允许半成功半失败。
当一个线程执行原子操作时,这意味着所有其他线程都将该操作视为尚未开始或完成。一个线程不可能在行为中捕获另一个线程。如果线程之间没有同步,几乎所有的操作都是非原子的。让我们举一个简单的例子。考虑一下这段代码。
定义变量
a:整数;
开始
a :=a 1;
结束;
如果两个单独的线程使用它来增加共享变量A,即使这些琐碎的代码也会导致问题。这个pascal语句在汇编级被分解成三个操作。
从存储器读取a到处理器寄存器。
向处理器寄存器添加1。
将处理器寄存器的内容写入内存中的。
即使在单处理器机器上,多个线程执行该代码也可能导致问题。这是因为调度操作。当只有一个处理器时,实际上只有一个线程执行一次,但是Win32调度程序以大约每秒18次的速度在它们之间切换。
调度程序可以随时停止一个线程的运行并启动另一个线程。排班是先发制人的。在挂起一个线程并启动另一个线程之前,操作系统不会等待任何时候发生权限切换。由于切换可以发生在任意两条处理器指令之间,因此它可能发生在函数中间的临界点,甚至是特定程序语句执行的一半。
让我们假设两个线程正在单处理器机器(X和Y)上执行示例代码。在一个好的情况下,程序可能正在运行,调度操作可能会错过这个临界点,给出预期的结果:A增加2。然而,这绝不是一个保证,而是一个盲目的机会。如果共享变量碰巧是指针,结果可能会导致人们崩溃。
说了很多理论之后,我们以代码的形式来看看上面的情况。为了方便观察,我没有用视频里的案例,而是用了控制台应用。
使用
系统。系统,系统。班级;
类型
twokthread=class(TThread)
保护
执行程序;覆盖;
结束;
定义变量
//将全局变量定义为共享数据。
Num:整数=0;
{ TWorkThread }
过程网络。执行;
开始
//循环方式自动增加Num。
当真开始时
//为了让效果更明显,增加了延时。
TThread。睡眠(100);
//当Num大于10时终止线程。
如果(Num 10),那么
退出;
writeln(Num);
Inc(Num);
结束;
结束;
开始
//开始3个线程
TWorkThread。创建(假);
TWorkThread。创建(假);
TWorkThread。创建(假);
Readln
结束。
实施结果如下
因为篇幅的关系,我截取了其中一个结果,但也足以说明问题。
解决
方案对于我们自己来说解决起来好像确实很麻烦,好在我们的前辈已经提供了解决方案。不学不知道,一学吓一跳,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图文版的最后一篇文章了,至此第一季相关的内容全部更新完成