全國最多中醫師線上諮詢網站-台灣中醫網
發文 回覆 瀏覽次數:2555
推到 Plurk!
推到 Facebook!

TImage 在多執行緒下使用是否有 bug ?

尚未結案
CA
一般會員


發表:1
回覆:10
積分:22
註冊:2007-04-01

發送簡訊給我
#1 引用回覆 回覆 發表時間:2010-05-01 21:02:26 IP:114.25.xxx.xxx 未訂閱
大家好:

使用 BCB5, 請問 TImage 如果有兩個執行緒同時存取它, 是不是會造成畫布掛掉?


就我查網頁所知, TImage::TCanvas 等於 TImage::TPicture::TBitmap::TCanvas 是一個記憶體畫布
另外它自 TGraphicControl 繼承了一個視窗畫布 (參考到一個 TWinControl 也就是 TImage 的 Parent)

現在如果有一個取像的程式, 可以註冊一個 callback 函式, 每次取完像它就會呼叫 callback
然後在 callback 內對 Image1 畫上擷取到的影像 (使用 API BitBlt 之類的函式直接對 Image1->Canvas->Handle 操作)
然後再使用 TCanvas::LineTo 等函式在上面一些簡單的線條, 例如一個十字線. 在畫圖前會先 Lock 畫完再 Unlock 像這樣:

Image1->Canvas->Lock();
// ... paint image
// ... paint cross hair
Image1->Canvas->Unlock();

我目前知道的是 TCanvas 畫圖函式在繪畫後會引發 OnChange 事件, 所以在畫完之後 (如果 Parent 是 TPanel)
會對 Panel1 發送出一個 WM_PAINT 訊息, 待主執行序有空時, 會處理這個訊息, 將 Image1->Canvas 的內容搬到 Panel1 上
在搬移之前, 會先對 Panel1 的畫布 Lock (由 TGraphicControl.WMPaint 執行) 但是好像沒有對 Image1 的畫布 Lock

我現在因為發現畫面更新比較慢, 所以在劃圖後 Unlock 前加上 Refresh 呼叫, 畫面就可以更新比較快
但是如果在 Unlock 後才 Refresh 畫布幾乎會立刻掛掉, 但如果我適當的加上 Sleep 就比較不會掛, 我不了解為什麼有這樣的差別?
因為如果主執行緒沒有對 Image1->Canvas 做 Lock 那它在搬移資料的時候, 如果剛好 callback 對 Image1->Canvas 寫入資料
應該就很有可能造成畫布掛掉, 不論 Refresh 在 Unlock 之前或之後, 掛掉機會應該是差不多一樣的, 甚至不呼叫 Refresh 也應該會有機會掛掉
但現在的結果是不呼叫 Refresh 沒遇過掛掉情形(印象中), 在 Unlock 之前 Refresh 幾乎不太會掛 (因為印象中好像發生過又好像沒有)


註: 我有查看過 TGraphicControl.WMPaint 它只對 TGraphicControl.Canvas.Lock
而 TImage.Paint 只有在呼叫 StretchDraw 前設定 FDrawing 的 Flag 而已, 並沒有對 TImage.Canvas.Lock
不知道是否看漏了


網路上找好久都找不到, 望各位大大解惑


daldal
高階會員


發表:6
回覆:102
積分:226
註冊:2007-06-18

發送簡訊給我
#2 引用回覆 回覆 發表時間:2010-05-03 13:58:05 IP:61.219.xxx.xxx 訂閱
就裝置的角度來看
螢幕更新的頻率在 60Hz 附近
換算每次更新的間隔大概在 16.6 ms 附近
一般會大於Canvas花費的時間 (如果不是更新非常非常多Pixel)

至於Canvas底層
Refresh 使用的是同步的 api
Invalidate 使用的是異步的 api
若是要讓 Canvas thread-safe
最好使用 invalidate
避免 Refresh 同步鎖定更新時
另一個執行緒剛好也發生同步互鎖的一些問題

PS: refresh 不是完全不能用在 thread, 只是要自己處理好一些同步問題
若是想要系統幫你處理,那就用異步的 invalidate 吧~
CA
一般會員


發表:1
回覆:10
積分:22
註冊:2007-04-01

發送簡訊給我
#3 引用回覆 回覆 發表時間:2010-05-03 17:34:21 IP:203.204.xxx.xxx 未訂閱
因為用 Invalidate 送 WM_PAINT 給主執行緒更新, 不是立即的
它的更新會等到主執行緒有空才做, 比較慢, 所以才想說用 Repaint 比較快


如下測試碼

[code cpp]
//---------------------------------------------------------------------------

#include
#pragma hdrstop

volatile BOOL g_fExit = FALSE;

#include "Unit1.h"
//---------------------------------------------------------------------------
#pragma package(smart_init)
#pragma resource "*.dfm"
TForm1 *Form1;
//---------------------------------------------------------------------------
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
}

//---------------------------------------------------------------------------
void __fastcall TForm1::FormClose(TObject *Sender, TCloseAction &Action)
{
g_fExit = TRUE;
}

//---------------------------------------------------------------------------

DWORD WINAPI ThreadProc(LPVOID)
{
TCanvas* pCanvas = Form1->Image1->Canvas;

int x = 5;
int y = 5;

while (!g_fExit)
{
pCanvas->Lock();
pCanvas->FillRect(Rect(0, 0, Form1->Image1->Width, Form1->Image1->Height));
pCanvas->TextOut(x, y, "Hello");

//Form1->Image1->Repaint();

//Sleep(50);
x = 20; if (x > 105) x = 5;
y = 20; if (y > 105) y = 5;

pCanvas->Unlock();
}

return 0;
}

//---------------------------------------------------------------------------
void __fastcall TForm1::btnThreadClick(TObject *Sender)
{
HANDLE hThread = CreateThread(NULL, 0, ThreadProc, NULL, 0, NULL);
CloseHandle(hThread);
//btnHalt->Click();
}

//---------------------------------------------------------------------------
void __fastcall TForm1::btnHaltClick(TObject *Sender)
{
HANDLE hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
WaitForSingleObject(hEvent, 10000);
CloseHandle(hEvent);
}

//---------------------------------------------------------------------------
[/code]

TextOut 會引發 TBitmapCanvas.OnChange
最後跑到 TImage.PictureChanged 送出 Invalidate (非同步) 給 Form1
所以主執行緒一有空就會來搬資料, 把記憶體內容搬到 Form1 上

所以我 發現如果在 Unlock 之後 Repaint 會出問題, 本來不知道為什麼
後來查詢後是說 TWinControl.MainWndProc 在處理訊息後會清理畫布
呼叫這兩個函式 FreeDeviceContexts 及 FreeMemoryContexts
變成如果不把 Repaint 寫在 Lock/Unlock 之內, 可能會有競爭情況
最後在 TCanvas.RequiredState 跳出 "Canvas does not allow drawing"
(因為 CreateHandle 跟 if FHandle = 0 then 之間有漏洞會出問題)

而把 Repaint 寫在 Unlock 之內, 印象中以前畫布掛掉過
不過我這兩天測試一次都沒有發生, 所以可能寫在 Lock/Unlock 就安全了吧 (搞不好是以前記錯)

但我覺得奇怪的是, 照理說應該是不安全的, 主執行緒因為 PictureChanged 收到 Invalidate
等它有空時, 去處理 WM_PAINT 時, 沒有鎖定 TImage.Canvas 只有產生的執行緒去鎖定它而已
應該有機會 FillRect 或 TextOut 到一半, 主執行緒去搬資料, 造成問題才對 (不論有沒有 Repaint 皆同)
MSDN 有寫 Note that the handle to the DC can only be used by a single thread at any one time.
不是很瞭解它的意思, 怎麼測都沒問題 ..



編輯記錄
CA 重新編輯於 2010-05-03 17:41:32, 註解 無‧
CA 重新編輯於 2010-05-03 17:43:19, 註解 無‧
daldal
高階會員


發表:6
回覆:102
積分:226
註冊:2007-06-18

發送簡訊給我
#4 引用回覆 回覆 發表時間:2010-05-04 10:51:26 IP:61.219.xxx.xxx 訂閱

===================引 用 CA 文 章===================
因為用 Invalidate 送 WM_PAINT 給主執行緒更新, 不是立即的
它的更新會等到主執行緒有空才做, 比較慢, 所以才想說用 Repaint 比較快
>> draw functions, repaint 要寫在 Lock / unlock 內,不然單一 thread 還 ok ,多 thread 會隨機死
>> 之前遇到畫布會掛掉,也許是有自己去取 getdc 放到新的TCanvas,
>> 不然正常下使用應該不會遇到問題 (我是用 5 thread 更新同一個 PaintBox)

但我覺得奇怪的是, 照理說應該是不安全的, 主執行緒因為 PictureChanged 收到 Invalidate
等它有空時, 去處理 WM_PAINT 時, 沒有鎖定 TImage.Canvas 只有產生的執行緒去鎖定它而已
應該有機會 FillRect 或 TextOut 到一半, 主執行緒去搬資料, 造成問題才對 (不論有沒有 Repaint 皆同)
MSDN 有寫 Note that the handle to the DC can only be used by a single thread at any one time.
不是很瞭解它的意思, 怎麼測都沒問題 ..
>> 印象中、Canvas 本身是 Thread-safe, (依靠 lock, unlock來實做)
>> 而MSDN 指的是還沒封裝過的 DC 畫布, 所以沒有 thread-safe
>> 兩者應該是沒有衝突的



CA
一般會員


發表:1
回覆:10
積分:22
註冊:2007-04-01

發送簡訊給我
#5 引用回覆 回覆 發表時間:2010-05-04 14:00:36 IP:61.218.xxx.xxx 未訂閱
我的問題就是在這裡, 因為 TPaintBox 跟 TImage 不同
對 TImage.Canvas 畫圖會導致最後呼叫到 TImage.PictureChanged

(我在網路上查到, 把 Packages/Build with runtime packages 取消勾選,
Linker/Use debug libraries 勾選, 就可以追入 VCL 源碼)

導致它呼叫 Invalidate 送出 WM_PAINT 給主執行緒, 主執行緒會不定時來搬資料, 從
TWinControl.MainWndProc -> TWinControl.WMPaint -> TWinControl.PaintHandler ->
TWinControl.PaintControls -> TGraphicControl.WMPaint -> TImage.Paint 一路追下來
只有在 TGraphicControl.WMPaint 的時候發現它對 TGraphicControl.Canvas 鎖定, 似乎
沒有對 TImage.Canvas 鎖定, 變成它從 TImage.Canvas 搬資料的時候 如果我的執行緒
同時對 TImage 做 FillRect/TextOut 應該很有可能出問題才是 (不論有無 Repaint 都一樣)

但是我測了好幾天, 確實沒有問題, 可能真的是我記錯了, 不然就是某些地方沒看清楚 ^^
(不過上面程式碼如果有 Repaint 的話, 在 Win7 切換佈景主題的時候會不明原因死結 = =)
至於 GetDC 應該是沒有, 我是在改別人程式的時候測的, 它沒呼叫那個函式然後再放到 Canvas
可能有其它地方沒注意到, 當時重點不在這裡, 沒花太多心思

TPaintBox 是不會觸發事件, 它是直接用 TGraphicControl.Canvas 所以也不會在繪圖之後
呼叫 Invalidate 送出 WM_PAINT 給主執行緒, 跟 TImage (有兩個畫布) 情況不太一樣

我後來也有持續針對 Canvas does not allow drawing 查得更詳細, 官方論壇有說到
FreeDeviceContexts 跟 FreeMemoryContexts 是 VCL 自己實作的垃圾回收機制
它會在每次主執行緒處理完一個訊息之後被呼叫, 內容是會掃瞄一個全域的 CanvasList / BitmapCanvasList
把目前沒有鎖定的畫布回收, 導致的結果就是, 如果今天在主執行緒下使用畫布
若是沒有 Lock / Unlock 即使那個副函式只有一個執行緒去執行, 仍然會出問題
因為主執行緒發現他沒有鎖定就會去釋放它 (除非主執行緒忙碌中沒時間釋放)
所以在非主執行緒下畫圖, 對畫布 Lock 是必須的不是可選的 ..

謝謝指教囉 ^^
編輯記錄
CA 重新編輯於 2010-05-04 14:02:33, 註解 無‧
系統時間:2024-03-29 7:20:40
聯絡我們 | Delphi K.Top討論版
本站聲明
1. 本論壇為無營利行為之開放平台,所有文章都是由網友自行張貼,如牽涉到法律糾紛一切與本站無關。
2. 假如網友發表之內容涉及侵權,而損及您的利益,請立即通知版主刪除。
3. 請勿批評中華民國元首及政府或批評各政黨,是藍是綠本站無權干涉,但這裡不是政治性論壇!