Delphi 线程
不是原创,只是看到好的内容复制了保存下来,留着学习。
CreadteThred参考,同步参考,WaitForSingleObject参考,互斥参考,
一、在 Delphi 中使用多线程有两种方法: 调用 API、使用 TThread 类; 使用 API 的代码更简单.
1、调用 API:CreateThread()
function CreateThread(
lpThreadAttributes: Pointer; {安全设置}
dwStackSize: DWORD; {堆栈大小}
lpStartAddress: TFNThreadStartRoutine; {入口函数}
lpParameter: Pointer; {函数参数}
dwCreationFlags: DWORD; {启动选项}
var lpThreadId: DWORD {输出线程 ID }
): THandle; stdcall; {返回线程句柄}
这首先这可以让我们知道: 每个线程都有自己独立的堆栈(也拥有自己的消息队列)
那主线程的栈有多大? 这个值是可以设定的: Project -> Options -> linker -> memory size
0: 线程建立后立即执行入口函数;
CREATE_SUSPENDED: 线程建立后会挂起等待.
可用 ResumeThread 函数是恢复线程的运行; 可用 SuspendThread 再次挂起线程.
这两个函数的参数都是线程句柄, 返回值是执行前的挂起计数.
什么是挂起计数?
SuspendThread 会给这个数 +1; ResumeThread 会给这个数 -1; 但这个数最小是 0.
当这个数 = 0 时, 线程会运行; > 0 时会挂起.
如果被 SuspendThread 多次, 同样需要 ResumeThread 多次才能恢复线程的运行.
2、ID 比句柄更轻便.
unit Unit1; interface uses Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, Dialogs; type TForm1 = class(TForm) procedure FormMouseUp(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); procedure Button2Click(Sender: TObject); end; var Form1: TForm1; implementation {$R *.dfm} var pt: TPoint; {这个坐标点将会已指针的方式传递给线程, 它应该是全局的} hThread : THandlde; {生成的线程} function MyThreadFun(p: Pointer): Integer; stdcall; var i: Integer; pt2: TPoint; {因为指针参数给的点随时都在变, 需用线程的局部变量存起来} begin pt2 := PPoint(p)^; {转换} for i := 0 to 1000000 do begin with Form1.Canvas do begin Lock; TextOut(pt2.X, pt2.Y, IntToStr(i)); Unlock; end; end; Result := 0; end; procedure TForm1.FormMouseUp(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); var ID: DWORD; begin pt := Point(X, Y); hThread := CreateThread(nil, 0, @MyThreadFun, @pt, 0, ID); {下面这种写法更好理解, 其实不必, 因为 PPoint 会自动转换为 Pointer 的} //CreateThread(nil, 0, @MyThreadFun, Pointer(@pt), 0, ID); end; {获取线程的退出代码, 并判断线程是否退出} procedure TForm1.Button2Click(Sender: TObject); var ExitCode: DWORD; begin GetExitCodeThread(hThread, ExitCode); if hThread = 0 then begin Text := '线程还未启动'; Exit; end; if ExitCode = STILL_ACTIVE then Text := Format('线程退出代码是: %d, 表示线程还未退出', [ExitCode]) else Text := Format('线程已退出, 退出代码是: %d', [ExitCode]); end; end.
procedure TForm1.Button1Click(Sender: TObject); var MyThread: TMyThread; begin MyThread := TMyThread.Create(False); end;
OnTerminate属性:表示在线程执行完Execute之后,还没有被释放之前,要紧接着执行的方法。
procedure TTestThread.Execute; var i: Integer; begin OnTerminate:= Form1.ThreadDone; //在这里设置OnTerminate属性的值为Form1的ThreadDone方法, //表示在线程执行完Execute之后,还没有被释放之前,要紧接着执行Form1的ThreadDone方法。 EnterCriticalSection(CS); for i:= 1 to MaxSize do begin GlobalArray[i]:= GetNextNumber; Sleep(5); end; LeaveCriticalSection(CS); end;
二、同步
1、临界区
"临界区"(CriticalSection): 当把一段代码放入一个临界区, 线程执行到临界区时就独占了, 让其他也要执行此代码的线程先等等;
var CS: TRTLCriticalSection; {声明一个 TRTLCriticalSection 结构类型变量; 它应该是全局的} InitializeCriticalSection(CS); {初始化} EnterCriticalSection(CS); {开始: 轮到我了其他线程走开} LeaveCriticalSection(CS); {结束: 其他线程可以来了} DeleteCriticalSection(CS); {删除: 注意不能过早删除} var CS: TRTLCriticalSection; function MyThreadFun(p: Pointer): DWORD; stdcall; var i: Integer; begin EnterCriticalSection(CS); for i := 0 to 99 do Form1.ListBox1.Items.Add(IntToStr(i)); LeaveCriticalSection(CS); Result := 0; end; procedure TForm1.Button1Click(Sender: TObject); var ID: DWORD; begin CreateThread(nil, 0, @MyThreadFun, nil, 0, ID); CreateThread(nil, 0, @MyThreadFun, nil, 0, ID); CreateThread(nil, 0, @MyThreadFun, nil, 0, ID); end; procedure TForm1.FormCreate(Sender: TObject); begin ListBox1.Align := alLeft; InitializeCriticalSection(CS); end; procedure TForm1.FormDestroy(Sender: TObject); begin DeleteCriticalSection(CS); end;
Delphi 在 SyncObjs 单元给封装了一个 TCriticalSection 类, 用法差不多, 代码如下:
uses SyncObjs; var CS: TCriticalSection; function MyThreadFun(p: Pointer): DWORD; stdcall; var i: Integer; begin CS.Enter; for i := 0 to 99 do Form1.ListBox1.Items.Add(IntToStr(i)); CS.Leave; Result := 0; end; procedure TForm1.Button1Click(Sender: TObject); var ID: DWORD; begin CreateThread(nil, 0, @MyThreadFun, nil, 0, ID); CreateThread(nil, 0, @MyThreadFun, nil, 0, ID); CreateThread(nil, 0, @MyThreadFun, nil, 0, ID); end; procedure TForm1.FormCreate(Sender: TObject); begin ListBox1.Align := alLeft; CS := TCriticalSection.Create; end; procedure TForm1.FormDestroy(Sender: TObject); begin CS.Free; end;
2、互斥
互斥量(原文链接)是系统内核对象,谁拥有就谁执行。它与临界区工作很类似。不同处在于:1、互斥量可以跨进程边界同步线程。2、可以给互斥量取个名字,通过引用互斥量的名字来使用一个已知的互斥量对象。
使用互斥量之类的对象需要反复调用系统内核,期间需要进行进程上下文转换和控制级别转换,大概需要耗费400到600个时间周期。
又是图书馆的比喻,现在是搞一个锁,把钥匙(互斥量句柄)交给管理员(操作系统),每一个人(线程)想要借书的时候,都要向管理员拿钥匙。当有人在使用的时候,另一人必须等待,等到钥匙有空的时候(互斥量进入信号状态),才能拿到钥匙(拥有了句柄)办理借书业务(此时互斥量进入非信号状态直到办完业务)。
使用互斥量的步骤:
1、声明一个全局的互斥量句柄变量(var hMutex: THandle;);
2、创建互斥量:CreateMutex(
lpMutexAttributes: PSecurityAttributes;
bInitialOwner: BOOL;
lpName: PWideChar ): THandle;
(lpMutexAttributes参数:指向TSecurityAttributes的指针,安全属性,一般用缺省安全属性nil;
bInitialOwer参数:表示创建的互斥量线程是否是互斥量的属主,如果该参数为False互斥量就没属主,一般来讲应设为False,否则如果设为True的话,要当主线程结束其他线程才成为它的属主才能运行;
lpName参数:是互斥量的名字,若打算取名的话,则传入nil。)
hMutex:= CreateMutex(nil, False, nil);
3、用等待函数控制线程进入同步代码块:
if WaitForSingleObject(hMutex, INFINITE) = WAIT_OBJECT_0 then begin //执行语句 end;
4、执行线程运行代码。
5、线程运行完后释放互斥量的拥有权:ReleaseMutex(hMutex: THandle);
6、最后关闭互斥量:CloseHandle(hMutex: THandle);
3、信号量
信号量(原文链接)是建立在互斥量的基础之上,同时加入重要特性:提供了资源计数功能,因此预定义数量的线程同时可以进入同步的代码块中。
信号量是维护0到指定最大值之间的计数器的同步对象,当线程完成一次信号量的等待时,计数器自减1,当线程释放信号量对象时,计数器自增1。
借用上面的图书馆例子,信号量好像是多设几把管理钥匙。每次可以设定N把钥匙同时工作,那就有N个人员可以同时办理业务。
信号量使用的一般步骤:
1、声明一个全局的信号量名柄,如:hSem:THandle;
2、创建信号量:CreateSemphore(
lpSemaphoreAttributes:PSecurityAttributes;
lInitialCount,lMaximumCount:LongInt;
lpName:PChar):THandle;stdcall;
(lpSemaphoreAttributes参数,指向TSecurityAttributes记录的指针,一般可以缺省填入nil值;
lInitialCount参数,是信号量对象的初始计数,是0~lMaximumCount之间的数。当它大于0时,信号量就进入了信号状态,当WaiForSingleObject函数释放了一个线程,信号量计数就减1。使用ReleaseSemphore函数可以增加信号量计数;
lMaximumCount参数,是信号量对象计数的最大值;
lpName参数,指定信号量的名字。)
hSem:=CreateSemaphore(nil,2,3,nil);
3、用等待函数WaiForSingleObject协调线程。
4、当一个线程用完一个信号,释放。使用ReleaseSemphore(
hSemaphore:THandle;
lReleaseCount:LongInt;
lpPreviousCount:Pointer):BOOL;StdCall;
(hSemphore参数,是信号量对象句柄;
lReleaseCount参数,要增加的信号量计数的数量;
lpPreviousCount参数,当前资源数量的原始值,一般为nil。)
ReleaseSemaphore(hSem,1,nil);
5、最后关闭信号量句柄,CloseHandle(hSem)。
CloseHandle(hSem);
如果最大信号量计数为1,那么就相当于Mutex。
三、WaitForSingleObject
DWORD WaitForSingleObject( HANDLE hHandle, DWORDdwMilliseconds);
有两个参数,分别是THandle和Timeout(毫秒单位)。
如果想要等待一条线程,那么你需要指定线程的Handle,以及相应的Timeout时间。当然,如果你想无限等待下去,Timeout参数可以指定系统常量INFINITE。
2. 使用对象
它可以等待如下几种类型的对象:
Event,Mutex,Semaphore,Process,Thread
3. 返回类型
有三种返回类型:
WAIT_OBJECT_0, 表示等待的对象有信号。(对线程来说,表示执行结束;对互斥量对象来说,指定的对象进入信号状态,可以执行)
WAIT_TIMEOUT, 表示等待指定时间内,对象一直没有信号。(线程没执行完;对互斥量来说,等到时间已过,对象依然是无信号状态);
WAIT_ABANDONED 表示对象有信号,但还是不能执行 一般是因为未获取到锁或其他原因(对于互斥量对象,拥有这个互斥量对象的线程在没有释放互斥量之前就已经终止,称作废弃互斥量,此时该互斥量归调用线程所拥有,并把这个互斥量设为非信号状态)
function WaitForSingleObject( hHandle: THandle; {要等待的对象句柄} dwMilliseconds: DWORD {等待的时间, 单位是毫秒} ): DWORD; stdcall; {返回值如下:} WAIT_OBJECT_0 {等着了, 本例中是: 等的那个进程终于结束了} WAIT_TIMEOUT {等过了点(你指定的时间), 也没等着} WAIT_ABANDONED {好不容易等着了, 但人家还是不让咱执行; 这一般是互斥对象} //WaitForSingleObject 的第二个参数一般给常数值 INFINITE, 表示一直等下去, 死等
//WaitForSingleObject的示例代码文件: unit Unit1; interface uses Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls; type TForm1 = class(TForm) Button1: TButton; procedure Button1Click(Sender: TObject); end; var Form1: TForm1; implementation {$R *.dfm} var hProcess: THandle; {进程句柄} {等待一个指定句柄的进程什么时候结束} function MyThreadFun(p: Pointer): DWORD; stdcall; begin if WaitForSingleObject(hProcess, INFINITE) = WAIT_OBJECT_0 then Form1.Text := Format('进程 %d 已关闭', [hProcess]); Result := 0; end; {启动一个进程, 并建立新线程等待它的结束} procedure TForm1.Button1Click(Sender: TObject); var pInfo: TProcessInformation; sInfo: TStartupInfo; Path: array[0..MAX_PATH-1] of Char; ThreadID: DWORD; begin {先获取记事本的路径} GetSystemDirectory(Path, MAX_PATH); StrCat(Path, '\notepad.exe'); {用 CreateProcess 打开记事本并获取其进程句柄, 然后建立线程监视} FillChar(sInfo, SizeOf(sInfo), 0); if CreateProcess(Path, nil, nil, nil, False, 0, nil, nil, sInfo, pInfo) then begin hProcess := pInfo.hProcess; {获取进程句柄} Text := Format('进程 %d 已启动', [hProcess]); CreateThread(nil, 0, @MyThreadFun, nil, 0, ThreadID); {建立线程监视} end; end;