有谁能解决 Delphi 的这个 Bug?(200分)

  • 主题发起人 主题发起人 andin
  • 开始时间 开始时间
已经一个小时了,还是没问题啊?
不过,我带着sleep(10)的时候,确实出了一次错误。你去掉sleep(10)试试,如果还出错的
话,看看把 FCurrentBitmap.Canvas.TextOut(0,0,IntToStr(number));
放到循坏外面还出错吗?如果还出,就不是delphi的问题。
 
这个问题确实存在,当程序运行后,访问它生成的ret.bmp文件(比如打开它)就会出现得更快。启动程序时GDI计数为28,运行线程后到31,出错后变为30。
 
我不太懂的。
 
估计跟GDI对象泄漏没有太大关系,可能要进行IO的同步。
改成如下,好象不容易出错一些:

var
mrew: TMultiReadExclusiveWriteSynchronizer;

implementation
constructor TTest.Create(CreateSuspended: Boolean);
begin
FCurrentBitmap := TBitmap.Create;
FCurrentBitmap.LoadFromFile('c:/test.bmp');
mrew := TMultiReadExclusiveWriteSynchronizer.Create;
Inherited;
end;

destructor TTest.Destroy;
begin
inherited;
FreeAndNil(FCurrentBitmap);
mrew.Free;
end;

procedure TTest.Execute;
var
number : integer;
begin
number := 0;
while not Terminated do
begin
mrew.BeginWrite;
try
FCurrentBitmap.Canvas.TextOut(0,0,IntToStr(number));
FCurrentBitmap.SaveToFile('c:/ret.bmp');
finally
mrew.EndWrite;
end;
Inc(number);

sleep(10);
end;
end;
DFW的高手们都不在了,不然应该有正确答案的。
 
经过多次测试发现,只要不去访问那个线程生成的ret.bmp文件,错误就不会出现,只要一访问就出错。
测试结果:

第一次
GDI对象31,I/O读取 4,I/O写入 35264
于I/O写入到34272时访问ret.bmp, 出错!

第二次
GDI对象31,I/O读取 4,I/O写入 65432
于I/O写入到63279时访问ret.bmp,出错!

第三次
GDI对象31,I/O读取 4,I/O写入 1870227
未访问该文件,没有出错,还在继续运行

注: test.bmp文件尺寸为384KB,我估计这个文件越大,错误出现的频率也会越大。可以初步肯定是文件IO的问题,非DELPHI之罪。
诚盼最权威答案。
 
真的不出错了!打开ret.bmp出错是因为,ACDSee打开锁住了它,造成测试程序写入时出的错,与楼主说的错误不是一回事。
 
搞定了这个 Bug !!!!让我们欢呼吧!!!!

问题的解决办法:
在 Create 函数里,LoadFromFile 后面增加一句话:FCurrentBitmap.Canvas.Lock;
在 Destory 函数里,FreeAndNIl 前面增加一句话:FCurrentBitmap.Canvas.Unlock;
问题从此不再出现!为什么呢?

千万不要相信 Delphi 的帮助文档!Delphi 说:VCL 不是线程安全的,但是图像对象,例如:TBitmap、TPen 等是线程安全的!这简直是一句谎言,TBitmap 根本就不是线程安全的,相反,与其他的对象相比,更不安全!

Delphi 在主线程里,每当处理完一个消息时,就会“自作主张”删除内存设备句柄,这就是引起这个问题的罪魁祸首!当我们调用 TBitmap.SaveToFile、TextOut 等函数时,在执行的过程中,刚申请到一个内存设备句柄,主线程可能就将这个句柄给释放掉,当再继续执行工作线程时,句柄已经无效了,从而引起了线程安全问题!
所以,如果在工作线程里需要处理 TBitmap 对象时,一定要将 Canvas 给锁定,否则主线程就会偷偷摸摸的给删除掉!
 
这个 Bug 困扰了我好长时间,一直没有找到问题的原因,也没有任何的文档资料给出说明,现在也不能完全说是 Delphi 的 Bug,但也不能说不是 Delphi 的 Bug。

总而言之,大家还是要多看 VCL 的源程序为妙,Delphi 的技术文档并不可靠,在这方面,Borland 公司作得没有 MS 好。

现在,我对 Delphi 的信心,渐渐的恢复了一些;
 
补充一下,Create 函数应当这么写:
constructor TTest.Create(CreateSuspended: Boolean);
begin
FCurrentBitmap := TBitmap.Create;
FCurrentBitmap.LoadFromFile('c:/test.bmp');
FCurrentBitmap.Canvas.Handle;
FCurrentBitmap.Canvas.Lock;
Inherited;
end;
 
解决了好啊,我一直怀疑tbitmap不是线程安全的。其实,在你前面的讨论中我好像提过,
但一直没有证据。
我看了一下,在我这里确实运行的时候不出错,但运行一段时间,线程就挂起了。
另外问一下: FCurrentBitmap.Canvas.Handle;有什么作用?
 
>constructor TTest.Create(CreateSuspended: Boolean);
>begin
> FCurrentBitmap := TBitmap.Create;>
> FCurrentBitmap.LoadFromFile('c:/test.bmp');
> FCurrentBitmap.Canvas.Handle; //这一行是什么意思?
> FCurrentBitmap.Canvas.Lock;
> Inherited;
>end;
 
To:特尔斐,wfzha
找准了 Delphi 的问题原因,我们就好回避这个问题了。以上有很多朋友发现,需要运行好长时间,甚至一两天才能发现这个奇怪的异常,这很好解释,因为问题的本质原因就是:TBitmap 不是线程安全对象。
Create 函数是在主线程里执行的,调用 FCurrentBitmap.Canvas.Handle,就是让 delphi 创建一个内存设备句柄,马上调用 FCurrentBitmap.Canvas.Lock,就是让 Delphi 锁定这个内存设备句柄,从而,在工作线程里就可以放心的调用 Canvas 方法,对 TCurrentBitmap 进行操作;如果不锁住内存设备句柄,主线程就会偷偷摸摸的将内存设备句柄给释放掉,这样,工作线程就会出现异常,尤其是 SaveToStream 函数,因为这个函数代码较长,很容易暴露线程安全问题。
 
问题确实如你所说,我根踪到TBitmap的WriteStream中的以下行时出上面所述的错误:
Save := GDICheck(SelectObject(FCanvas.FHandle, FDIBHandle));
DELPHI在调用GDICheck时发现SelectObject函数已调用失败。
但“调用 FCurrentBitmap.Canvas.Handle,就是让 delphi 创建一个内存设备句柄”这里我觉得是没有必要的,因为你载入位图到FCurrentBitmap时,其Canvas就已建立了,为证明这点,我改了你的代码如下,运行一个上午也没问题:
procedure TTest.Execute;
var
number : integer;
begin
number := 0;
FCurrentBitmap := TBitmap.Create;
FCurrentBitmap.LoadFromFile('c:/test.bmp');
FCurrentBitmap.Canvas.TryLock;
while not Terminated do
begin
FCurrentBitmap.Canvas.TextOut(0,0,IntToStr(number));
FCurrentBitmap.SaveToFile('c:/ret.bmp');
Inc(number);
sleep(10);
end;
FCurrentBitmap.Canvas.Unlock;
FreeAndNil(FCurrentBitmap);
end;
 
谁都会碰到不会的问题,不要动不动就Bug.
我见到说开发工具有Bug的人,十有八九都是自己水平的原因.
 
To:特尔斐
首先,我要说的是,你的程序没有任何问题,运行一年也不会出现错误!
但是,你的说法有一些错误之处:“因为你载入位图到FCurrentBitmap时,其Canvas就已建立了”,虽然 Delphi 已经创建了Canvas 对象,但是,还没有创建内存设备句柄,如果一个 Canvas 对象不拥有内存设备句柄,这个 Canvas 对象毫无用处的,Delphi 的解决办法是:需要时(例如调用Canvas.TextOut),创建内存设备句柄,可是 Delhi 用一种非常怪异的做法去释放内存设备句柄:在主线程内,每处理完一个消息,就释放内存设备句柄,根本就告诉开发人员,没有任何的文档讲述这一点!

虽然你的程序没有错误,但是执行效率很低,原因就在:内存设备句柄反复的创建、释放、创建、释放。。。,更让人可气的时,这种低效率,不是因为我们导致的,是因为 Delphi 的 VCL 不太明智的处理办法决定的;

最后,我推荐我的程序的写法,这样效率能高出不少。
 
To:wr960204
是不是Bug,你将这里的贴子都看完,不就知道了吗?
我要奉劝你一句:不要盲目迷信 Delphi !如果你不同意我的观点,那请回答一个问题:国内外,有那个大型软件,是用 Delphi 开发成功的?
 
和我逗气是不是?
给你个例子:美国航天局的项目
 
国外的游戏——奇迹时代
国内的游戏——帝国在线(EO)
开发工具——Delphi 3~7
 
其实Delphi的帮助中有说明:

Prevents other threads from drawing on the canvas.

procedure Lock;

Description

Call Lock in a multithreaded application to prevent other threads from drawing on the canvas. Lock prevents other cooperating threads in the application from executing until the Unlock method is called. Nested calls to Lock increment the LockCount property so that the canvas is not unlocked until the last lock is released.

In multi-threaded applications that use Lock to protect a canvas, all calls that use the canvas must be protected by a call to Lock. Any thread that does not lock the canvas before using it will introduce potential bugs.

Because Lock prevents other threads from executing, it can adversely affect performance. Do not call Lock unless there is a danger that another thread might interfere with drawing on the canvas.

就是说,多线程并发访问Canvas的时候需要lock和unlock。另外,你代码中所暴露的问题并不属于TBitMap,而是属于TCanvas。
 
来自:wr960204, 时间:2003-8-11 12:01:00, ID:2099960
谁都会碰到不会的问题,不要动不动就Bug.
我见到说开发工具有Bug的人,十有八九都是自己水平的原因.


说这些没什么意义。


我挺欣赏andin追根刨底的精神。
 

Similar threads

S
回复
0
查看
1K
SUNSTONE的Delphi笔记
S
S
回复
0
查看
897
SUNSTONE的Delphi笔记
S
后退
顶部