高效处理在主线程中显示后台线程处理进度
先看经典的处理方法:
TThread.CreateAnonymousThread( procedure var AHint: TZProgressNotify; ACount: Integer; ATime: Cardinal; const PassCount=1000000; begin ACount := 0; ATime := TThread.GetTickCount; while (not Application.Terminated) and (ACount < PassCount) do begin AHint.Progress := ACount*100 div PassCount ; AHint.HintText := ACount.ToString; Inc(ACount); TThread.Synchronize(nil, procedure begin FProgress.Update(AHint); end ); FProgress.Update(AHint); end; AHint.HintText := (TThread.GetTickCount - ATime).ToString + 'ms'; FProgress.Update(AHint); end).Start;
我们需要更新进度时,将其切换到主线程,并更新进度显示。我们测试显示用了32735ms,也就是说100万次进度更新,用了约33秒。
接下来我们来看下优化后的代码:
TThread.CreateAnonymousThread( procedure var AHint: TZProgressNotify; ACount: Integer; ATime: Cardinal; const PassCount=1000000; begin ACount := 0; ATime := TThread.GetTickCount; while (not Application.Terminated) and (ACount < PassCount) do begin AHint.Progress := ACount*100 div PassCount ; AHint.HintText := ACount.ToString; Inc(ACount); FProgress.Update(AHint); end; AHint.HintText := (TThread.GetTickCount - ATime).ToString + 'ms'; FProgress.Update(AHint); end).Start;
对的,你没看错,我们将 FProgress.Update 直接在后台线程调用了。我们对其代码进行了逻辑隔离,实测 100 万次进度更新,用时 78 毫秒。
我们来看一下具体的实现:
type TZProgressNotify = record Progress: Integer; HintText: String; end; TZMainThreadUpdator < T: record >= record private const FLAG_READING = Integer($80000000); var FBuffers: array [0 .. 1] of T; FActiveIndex, FUpdateRefCount: Integer; public procedure Update(const AValue: T); function GetData: T;overload; procedure GetData(var AValue: T);overload; property Data: T read GetData; end; { TZMainThreadUpdator<T> } procedure TZMainThreadUpdator<T>.GetData(var AValue: T); // 值复制可能会引起冲突,我们需要避免在值复制时,外部更新,所以将 FActiveIndex 加入标志位 var ABufferIndex: Integer; begin Assert(MainThreadId = TThread.Current.ThreadID, 'GetData must invoke in main thread'); //设置读取中标记位,设置后,Update不会更新 FActiveIndex 的值 repeat ABufferIndex := FActiveIndex; until AtomicCmpExchange(FActiveIndex, ABufferIndex or FLAG_READING, ABufferIndex) = ABufferIndex; AValue := FBuffers[ABufferIndex]; //允许后续的 Update 更新 FActiveIndex 的值以体现最新的进度 AtomicExchange(FActiveIndex, ABufferIndex); end; function TZMainThreadUpdator<T>.GetData: T; begin GetData(Result); end; procedure TZMainThreadUpdator<T>.Update(const AValue: T); var ABufferIndex: Integer; begin ABufferIndex := AtomicIncrement(FUpdateRefCount); try // 增加计数,如果有多个同时提交更新,只有第一个会保留,剩下的会丢弃 if ABufferIndex = 1 then begin // 多个线程同时更新,只保留第一个更新线程的结果,避免使用锁 ABufferIndex := (FActiveIndex + 1) and $1; FBuffers[ABufferIndex] := AValue; // 如果 FActiveIndex 不处于读状态,则更新,否则忽略更新,可以增加额外的标记来记录这个情况,然后在读取的时候清楚这一标记,本版本不做处理 AtomicCmpExchange(FActiveIndex, ABufferIndex, FActiveIndex and $1); end; finally AtomicDecrement(FUpdateRefCount); end; end;