ADO的并行处理能力是否很弱?在线程中使用ADO并行查询经常出错,请教。 (200分)

用Synchronize那还用什么线程啊?先不考虑使用Synchronize。
CSDN中weekendW写道:
我增加了一个ADOConnection以保证每个线程都有一个自己连接(会话),从而避免出现资源冲突,我的问题是不是解决了呢?是的,这个问题已经解决了
疑问:仅仅通过“增加一个ADOConnection”就可以“保证每个线程都有一个自己连接(会话)”,他是怎么做到的?
而 http://www.delphibbs.com/delphibbs/DispQ.asp?LID=389081 中讨论的结果就是要在每个线程中增加自己的TADOConnection,因为ADO是线程不安全的。
似乎就是这样,但我的疑问是,为什么使用了clUseServer游标,出错的概率就大大降低,今天测试了几次,一下子开了200个线程,都一直没有出错过,以前虽然也是出过错,但是在其他(比如TMy1Thread)测试出错后,没有关闭程序,直接继续测试出现的,另外,我到是没有做长时间的连续测试(今天下班不关机了,测试到周一试试)。
而使用clUseClient游标,在SQL7上测试,几乎每个线程都出错了,而在SQL2000中,每50个线程会有3、4个出错,而Oracle则更少,这个很奇怪,和dbserver也有关?
 
贴一下主要代码:
TMy4Thread = class(TThread){SetConn前使用server游标}
private
{ Private declarations }
FADOQ1:TADOQuery;
FADOQ2:TADOQuery;
protected
procedure Execute;
override;
public
constructor Create;
destructor Destroy;override;
end;

implementation
procedure OpenSQL(var ADOQ:TADOQuery;FSQL:string);
begin
ADOQ.Close ;
ADOQ.SQL.Clear ;
ADOQ.SQL.Add(FSQL);
try
ADOQ.Open ;//错误就出在这里,似乎并行处理很容易出错
except
on E:Exceptiondo
begin
Application.MessageBox(pchar('引发错误的SQL语句:'+#10+FSQL+'。'+#10#10+'数据库返回错误:'+#10+E.Message),'出错消息',MB_ICONERROR);
end;
end;
end;

constructor TMy4Thread.Create;
begin
inherited Create(true);
FreeOnTerminate:=true;
Priority :=tpLower;
Resume ;
end;

destructor TMy4Thread.Destroy;
begin
inherited Destroy;
end;

procedure TMy4Thread.Execute;
begin
FADOQ1:=TADOQuery.Create(nil);
FADOQ2:=TADOQuery.Create(nil);
FADOQ1.CursorLocation :=clUseServer;//TMy1Thread线程就少了这两句,
FADOQ2.CursorLocation :=clUseServer;//默认是clUseClient,这个关系似乎很大,why?
FADOQ1.Connection :=ADOC;
FADOQ2.Connection :=ADOC;
OpenSQL(FADOQ1,SQL1);
while not FADOQ1.Eofdo
begin
FADOQ1.Next ;
end;

OpenSQL(FADOQ2,SQL2);
while not FADOQ2.Eofdo
begin
FADOQ2.Next ;
end;

FADOQ1.Close ;
FADOQ2.Close ;
FADOQ1.Free ;
FADOQ2.Free ;
end;

另外,连红星的“大富翁论坛离线浏览器2.1.3.449”中创建TADOQuery的代码是这样的,虽然没有使用过他的离线浏览器,但我想既然是公开发布的,应该不会出现这么明显的错误才对,不过他是用ACCESS:
FQuery:=TADOQuery.Create(nil);
FNewQuery:=TAdoQuery.Create(nil);
FQuery.Name:='Query'+IntToHex(Fbegin
ID,8)+IntToHex(FEndID,8);
FNewQuery.Name:='NQuery'+IntToHex(Fbegin
ID,8)+IntToHex(FEndID,8);
FQuery.CacheSize:=1;
FQuery.CursorType:=ctDynamic;
FQuery.CursorLocation:=clUseServer;
FQuery.Connection:=Connection;
FNewQuery.Connection:=Connection;
FNewQuery.MaxRecords:=1;
 
用transaction
 
喊一嗓子再说,不知对不对
我感觉一个adoconnection应该对应一个session,多个线程公用的话可能会有问题,一般我都在线程中创建adoconnection好像没出过问题。
至于clUseClient容易出错,可能是因为clUseClient会将所有数据下载到客户端缓存,session占用的时间较长,多线程冲突的几率较大。clUseServer只传输cachesize设定的纪录数,所以几率较小。你可以将adoquery设为clUseServer,cachesize设大一点,看是不是容易出错?
-----没有那么大的数据,不好测试,乱说的
 
来自:hfghfghfg, 时间:2004-2-5 8:19:00, ID:2436816 | 编辑
to qince;
我 认为 server 写 到 这样 就 可以 了。{考虑 到 还有 网络 上 的 瓶颈}
我 算 过, 一秒 中 产生 的 数据 有 一兆。
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, DB, ADODB, Provider, DBClient, StdCtrls, Spin;
const
MaxRecordCount = 300;
type
TDoThread = class(TThread)
private
FDataSet: TDataSet;
FMemo: TMemo;
FDoCount: integer;
FFilter: string;
FMemoName: string;
procedure getData;
protected
procedure Execute;
override;
public
constructor Create(DataSet: TDataSet;
DoCount: Integer;
m: TMemo;
MemoName: string);
end;

type
TForm1 = class(TForm)
ADODataSet1: TADODataSet;
ClientDataSet1: TClientDataSet;
DataSetProvider1: TDataSetProvider;
Button1: TButton;
GroupBox1: TGroupBox;
Memo1: TMemo;
GroupBox2: TGroupBox;
Memo2: TMemo;
Button2: TButton;
SpinEdit1: TSpinEdit;
SpinEdit2: TSpinEdit;
Label1: TLabel;
Label2: TLabel;
procedure FormCreate(Sender: TObject);
procedure Button1Click(Sender: TObject);
procedure Button2Click(Sender: TObject);
private
public
{ Public declarations }
end;

var
Form1: TForm1;
implementation
{$R *.dfm}
{ TDoThread }
constructor TDoThread.Create(DataSet: TDataSet;
DoCount: Integer;
m: TMemo;
MemoName: string);
begin
FDataSet := DataSet;
FMemo := m;
FMemoName := MemoName;
FDoCount :=do
Count;
FreeOnTerminate := True;
inherited Create(False);
Priority := tpLower;
end;

procedure TDoThread.Execute;
var
i: integer;
begin
for i := 1 to FDoCountdo
begin
FFilter := 'F1 <> ''999'' ';
Synchronize(GetData);
end;
end;

procedure TDoThread.getData;
var
ss: TMemoryStream;
strData, SendData: string;
nowCount, i, j: integer;
begin
//
nowCount := random(MaxRecordCount);
FDataSet.First;
ss := TMemoryStream.Create;
try
FDataSet.Filter := FFilter;
FDataSet.Filtered := true;
with FDataSetdo
begin
for i := 1 to nowCountdo
if eof then
break
else
begin
for j := 0 to 9do
begin
strData := Fields[j].AsString;
ss.Write(strData[1], length(strData));
end;
next;
end;
end;
except
;
end;
setlength(SendData, ss.size);
ss.Read(SendData[1], ss.Size);
if Fmemo.Lines.Count > 10000 then
Fmemo.Lines.Clear;
Fmemo.Lines.Add(format('%s 线称%s: 记录数%d length(SendData)%d', [formatdatetime('hh:nn:ss', now), FMemoName, nowCount, length(SendData)]));
freeandnil(ss);
SendData := '';
strData := '';
Application.ProcessMessages;
end;

procedure TForm1.FormCreate(Sender: TObject);
var
i: integer;
begin
Randomize;
for i := 1 to 10do
ADODataSet1.FieldDefs.Add('F' + inttostr(i), ftstring, 50);
ADODataSet1.CreateDataSet;
ADODataSet1.Open;
for i := 1 to 10000do
ADODataSet1.AppendRecord([i, 'aaaaaaaaaaaaaaaaaaaaaa', 'aaaaaaaaaaaaaaaaaaaaaa', 'aaaaaaaaaaaaaaaaaaaaaa', 'aaaaaaaaaaaaaaaaaaaaaa', 'aaaaaaaaaaaaaaaaaaaaaa', 'aaaaaaaaaaaaaaaaaaaaaa', 'aaaaaaaaaaaaaaaaaaaaaa', 'aaaaaaaaaaaaaaaaaaaaaa', 'aaaaaaaaaaaaaaaa']);
ADODataSet1.First;
ClientDataSet1.Open;
// ADODataSet1.Close;
// showmessage(inttostr(ClientDataSet1.RecordCount));
end;

procedure TForm1.Button1Click(Sender: TObject);
var
i: integer;
begin
for i := 1 to SpinEdit1.Valuedo
begin
TDoThread.Create(ClientDataSet1, { random(10000) +} SpinEdit2.Value, memo1, inttostr(i));
end;
end;

procedure TForm1.Button2Click(Sender: TObject);
var
i: integer;
begin
for i := 1 to SpinEdit1.Valuedo
begin
TDoThread.Create(ADODataSet1, { random(10000) +} SpinEdit2.Value, memo2, inttostr(i));
end;

end;

end.

object Form1: TForm1
Left = 99
Top = 210
BorderStyle = bsSingle
Caption = 'Form1'
ClientHeight = 453
ClientWidth = 816
Color = clBtnFace
Font.Charset = GB2312_CHARSET
Font.Color = clWindowText
Font.Height = -12
Font.Name = '宋体'
Font.Style = []
OldCreateOrder = False
Position = poScreenCenter
OnCreate = FormCreate
PixelsPerInch = 96
TextHeight = 12
object Label1: TLabel
Left = 351
Top = 8
Width = 36
Height = 12
Caption = '线称数'
end
object Label2: TLabel
Left = 351
Top = 136
Width = 96
Height = 12
Caption = '每个线称的工作量'
end
object Button1: TButton
Left = 375
Top = 216
Width = 75
Height = 25
Caption = 'c'
TabOrder = 0
OnClick = Button1Click
end
object GroupBox1: TGroupBox
Left = 0
Top = 0
Width = 340
Height = 453
Align = alLeft
Caption = 'ClientDataSet1'
TabOrder = 1
object Memo1: TMemo
Left = 2
Top = 14
Width = 336
Height = 437
Align = alClient
TabOrder = 0
end
end
object GroupBox2: TGroupBox
Left = 476
Top = 0
Width = 340
Height = 453
Align = alRight
Caption = 'ADODataSet1'
TabOrder = 2
object Memo2: TMemo
Left = 2
Top = 14
Width = 336
Height = 437
Align = alClient
TabOrder = 0
end
end
object Button2: TButton
Left = 375
Top = 272
Width = 75
Height = 25
Caption = 'A'
TabOrder = 3
OnClick = Button2Click
end
object SpinEdit1: TSpinEdit
Left = 351
Top = 24
Width = 121
Height = 21
MaxValue = 0
MinValue = 0
TabOrder = 4
Value = 100
end
object SpinEdit2: TSpinEdit
Left = 351
Top = 152
Width = 121
Height = 21
MaxValue = 0
MinValue = 0
TabOrder = 5
Value = 100
end
object ADODataSet1: TADODataSet
Parameters = <>
Left = 472
Top = 56
end
object ClientDataSet1: TClientDataSet
Aggregates = <>
Params = <>
ProviderName = 'DataSetProvider1'
ReadOnly = True
Left = 480
Top = 184
end
object DataSetProvider1: TDataSetProvider
DataSet = ADODataSet1
Constraints = True
Left = 480
Top = 112
end
end


来自:hfghfghfg, 时间:2004-2-5 8:24:00,
 
http://www.efile.com.cn/efile/dfw@97546/time.exe
 
俺对数据库不熟,看了 Dirk 的例子,确实是一个很不错的测试,描述了很多的情况。
俺的疑问是:
在使用同一个 TADOConnection 时,在 TADOQuery.Create 时会发生什么事情?
TADOConnection 是一个 ADO 数据连接对象的容器,储存有连接对象、连接事件、数据集。
使用同一个 TADOConnection 对象的其它 ADO 控件,都会在 TADOConnection 里登记自己
吧?登记,其实就是往 TList 里 Add ,TList 的 Add 是不是线程安全的呢?
这是个问题,先不考虑 ADO Library 是否线程安全。能否说说这个问题?
 
我采用什么都不做的线程模式,当使用Oracle提供的驱动时(Provider=OraOLEDB.Oracle.1;Password=111111;Persist Security Info=True;User ID=topman;Data Source=alarm),三条线程立刻产生错误。Oracle版本是9.2.0.1.0
但如果使用微软的ADO(Provider=MSDAORA.1;Password=111111;User ID=topman;Data Source=alarm;Persist Security Info=True),十条线程运行两个多小时都没有问题。版本是2.7
 
嗯。在我提出上面的问题时,思路是不明确的,因为不知道 TADOConnection 为什么要使用
以及如何去使用。和 Dirk 讨论后,我觉得是否应该这样来看待 TADOConnection 这个控件:
就是,如果您在多线程中使用 ADO 控件,一定记着不要把 TADOConnection 引进来。没有
更多的有关这个控件的设计说明,看源代码可以看到这个控件确实是使用 TList 做为容器
的,没有使用 TThreadList ,我认为原作者是认为 TADOConnection 不会被多线程使用,
因为使用 TADOQuery 这样的控件可以调用原生的 ADO 对象来进行连接,放一个 Connection
属性只是为了 VCL 的完整性或者说 RAD 设计的方便考虑。我对这组控件不熟,这是我的个
人看法,也许还有更多的问题有待讨论。
 
好贴 收藏
 
我写网络游戏,DELPHI + ADO + MSSQL,多线程 + SOCKET 阻塞式。
目前游戏每日保持200以上的同时在线,也就是说至少有200个以上的线程在同时跑.
类似 Access violation at address 00135770. Write of address 005D8B78 的错误通常会无任何规律的出现,我只在主线程中使用了一个ADOConnection,其他的线程中全部 ADOQuery := TADOQuery.Create(nil);
ADOQuery.ConnectionString := 'xxxx';
我愚蠢的认为 TADOQuery的建立会伴随着一个新的ADOConnection建立。
实际上它们全部在用主线程的 ADOConnection……
 
嗯。使用了 ADOQuery.ConnectionString 来定义连接,就不会在 TADOQuery 里新建一个
TADOConnection 来建立连接,而是直接使用 ADO 的原生连接对象来完成连接的。只有在
没有定义 ADOQuery.ConnectionString 属性并且 TADOQuery.Connection 属性不为 nil
时,连接才使用 TADOConnection 控件,如果连 TADOQuery.Connection 属性都没有设置,
代码返回错误。明显的,ConnectionString 会被优先考虑进行连接尝试,只有它没有被
设置的情况下,才会检查有没有设置 Connection 属性。两者间不是并列的关系。所以我
认为这个 Connection 属性完全是贴上去为了 VCL 完整和 RAD 目的。
 
[?]
加个记号加个记号加个记号加个记号加个记号加个记号加个记号加个记号
 
这样改一下试试,行不行:
procedure OpenSQL(var ADOQ:TADOQuery;FSQL:string);
begin
ADOQ.Close ;
ADOQ.SQL.Clear ;
ADOQ.SQL.Add(FSQL);
try[red]
while ADOQ.Connection.tag = -1do
sleep(1);
//用个死循环,让它等一下,别和别人挤死在门口
ADOQ.Connection.tag := -1 //加锁[/red]
ADOQ.Open ;//错误就出在这里,似乎并行处理很容易出错
[red] ADOQ.Connection.tag := 0 //解锁[/red]
except
on E:Exceptiondo
begin
Application.MessageBox(pchar('引发错误的SQL语句:'+#10+FSQL+'。'+#10#10+'数据库返回错误:'+#10+E.Message),'出错消息',MB_ICONERROR);
end;
end;
end;

如果错误不是由多个query并行open时造成的,就要找出真正产生错误的地方,加把锁。
 
pyzfl,那天和小雨哥讨论了后,我还是觉得是ADO并行处理能力的问题,因为我后来把OpenSQL修改成
EnterCriticalSection(CS);
ADOQ.Open ;
LeaveCriticalSection(CS);
就不出错了,显然,这样ADO就不会出现并行执行的问题了,就不出错了,你说的加把锁,其实也就是避免了并行执行的情况。
但可能还是有其他方面的问题,比如,小雨哥认为和TList的线程不安全有关,但改成这样:
EnterCriticalSection(CS);
FADOQ1.Connection :=ADOC;
FADOQ2.Connection :=ADOC;
LeaveCriticalSection(CS);
仍然出错,但改成这样,出错的几率又大大下降,
EnterCriticalSection(CS);
FADOQ1.Connection :=ADOC;
LeaveCriticalSection(CS);
EnterCriticalSection(CS);
FADOQ2.Connection :=ADOC;
LeaveCriticalSection(CS);
我跟踪了FADOQ2.Connection :=ADOC;的执行过程,这两段程序在我看来没有什么区别,但结果却出乎意料,看来还要好好看看源码。
 
  我怀疑,在ADOQ.Open 时,是否真在公用Connection里的一个 TList 中执行 Add 操作?如果是这样的话,就好比是一个帐簿上的内容,虽然可供多个人阅读,但同一时间只由一个人来写入,因为伴随着还有内存的申请、TList.count的维护等工作,所以这时的并行操作,就象两只手共同操纵一支笔一般,应该会出错的。符合现实的话,还是要一个一个的来,当然如果Connection处理的好的话,应该把这个处理封装到内部,以避免这个错误的发生。
 
没有。事实上如果直接使用 ADOQuery.ConnectionString 来进行连接,所有的同步机制由
ADO 来完成,如果这时候存取的是 Access 的话,就涉及到了上面所说的 ADO Library 是
否是线程安全的。在上面 Dirk 的一个测试中,对 ADOQuery 的 Connection 属性赋值时进
行了临界保护,这个每次的赋值保护,目的就是避免 TADOConnection 的线程不安全,但是
虽然进行了每次的赋值保护,却无法做到在线程结束时对 TADOConnection 访问的保护。线
程结束时同样要访问 TADOConnection ,目的是从 TADOConnection 里移走一个已经登记了
的 ADO 控件。假如使用了 TADOConnection 做为公共的数据连接控件,一个 Add 和一个
Remove 动作是肯定需要执行的,而这 2 个动作的执行,在多线程中引起冲突是随机而且必
然的。有了这个观点后,看来一定要在多线程中共用 TADOConnection 的唯一做法就是使用
Synchronize 方法或者改写 TADOConnection 控件。如此一来还不如直接使用 ADOQuery 的
ConnectionString 来进行多线程中的连接岂不更好?所以,如果使用多线程的操作,撇开
TADOConnection 直接使用 ADOQuery 自己的 ConnectionString 属性应该是个好方法。
 
在FADOQ1.Connection :=ADOC;执行过程中,最终通过调用TCustomConnectiond的RegisterClient方法将FADOQ1记录在FDataSets: TList;属性,FDataSets是线程不安全的,但在代码:
EnterCriticalSection(CS);
FADOQ1.Connection :=ADOC;
LeaveCriticalSection(CS);
EnterCriticalSection(CS);
FADOQ2.Connection :=ADOC;
LeaveCriticalSection(CS);
中,已经是线程安全了(参见TThreadList的实现过程),但线程并行执行时仍然出错,可见,不是这里的线程安全问题,而当FADOQ1.Free时,会最终调用TCustomConnectiond的UnRegisterClient过程将FADOQ1从FDataSets中移除(FDataSets.Remove(Client);),虽然这里也是线程不安全的,但也很容易将其用临界区保护起来,但错误并不出现在这里,而是出在OpenSQL过程的Open方法执行的时候,SQLServer返回的错误是:“连接占线导致另一个命令”,Oracle返回的错误是:“对象被打开。”,我认为这个返回的错误提示说明了一定问题,从词义上看是TADOConnection在并行处理时应付不过来了,而这个错误在连续执行时似乎没有看到过,而将Open用临界区保护起来,错误就消失了。
TDOQuery的ConnectionString属性没有仔细看,明天再看看。
楼上的几个朋友的回答和代码也很有参考价值,hfghfghfg的代码因为我没有d6,暂时无法执行,但我看到其中还是使用了Synchronize,那么,实际是单线执行的。
hardware007说道transaction这个没注意过,暂时不知是干嘛的,看看。
fishjam的情况到是没见过。
Orber的说法还不是很清楚,是不是说线程中的TADOQuery即使不用主线程的ADOConnection仍然会通过主线程的ADOConnection?
 
再说两句,小雨哥你可千万别怪我啊。
  根据“如果使用多线程的操作,撇开TADOConnection 直接使用 ADOQuery 自己的 ConnectionString 属性”这个方法,我建立一个程序,其中有7个adoquery,并执行了么个过程:
procedure TForm1.Button3Click(Sender: TObject);
begin
ADOQuery1.Open;
ADOQuery2.Open;
ADOQuery3.Open;
ADOQuery4.Open;
ADOQuery5.Open;
ADOQuery6.Open;
ADOQuery7.Open;
end;

  打开MSSQL的企业管理器,那么在“管理-当前活动-锁/进程”节点下,马上会多出七个进程(当然要在管理节点上按一下快捷键“F5”刷新)。每个进程,应该是由服务器端和客户端共同维护的,如果真要每个QUERY都建立一下连接(进程)的话,对服务器及网络,是多大的浪费啊。
  其实不仅仅“ADO Library 不是线程安全的”,所有的程序,都有可能是不安全的,因为谁也没有能力把程序做到百分百的安全,如果用绝对安全来衡量的话,没有一个程序是合格的。我们只能充分利用它安全的部分,比如数据读取部分;然后针对不安全部分做一些补救措施。
我的计算机基础理论不是很扎实,只能从类比来分析:不管多线程的并行说得怎么好,在一个CPU上,还是由时段片来一段一段的运行,说到底还是串行的。就是有多个CPU,但硬盘上数据的读取还是一段一段的,还是串行;如果是网络环境的话,无论怎么搞,还是一根网线,传输还是一个字节一个字节来,还是串行。说到并行,只不过是从人机交流上,给人以并行运行的感觉,在计算机内部,还是在分段串行的。
 
>>不管多线程的并行说得怎么好,在一个CPU上,还是由时段片来一段一段的运行,说到底还是串行的。
这点是没错,但是多线程的确能提高程序运行的效率,比方:有5个任务(每任务一线程)在运行,如果平均分配时间,每分钟每任务获得12秒运行时间,这时某个任务多开了个线程,则每分钟每线程就只有10秒运行时间,但有两个线程的任务就获得了20秒的运行时间,这样效率就提高了,硬盘、网络也同样,多线程其实是在和其他程序在争时间,而不是和CPU在争时间。
另外,我也不是要把程序做到绝对安全,而是这些只是一个很小的应用,出现这样的错误,应该是程序的错误或者是某些组件的某些能力的缺陷,任何应用都起码应该避开这些问题。
还是把ADOQ.Open ;保护在临界区比较好些,这样可以避免这些错误(不管是设置或是ADO并行处理能力引起的),而且,这样处理,效率的损失也不会太大,毕竟还是遍历记录集的时间更长些,还是能发挥线程的后台执行的效力。
因为是要找到问题原因,所以使用了数目较多的线程进行测试,但实际使用中,我会控制在20个线程以内(甚至10),用户也不太可能要求同时打开这么多报表模块(看不过来)。
另外,我也考虑到资源的问题,每个线程一个TADOConnection也未尝不可,因为毕竟数据库服务器是面向大量用户并发应用设计的,不应该太低估这些软件的处理能力(即使有广告吹嘘成份,充分使用服务器资源也是好的风格,比如我机器上的SQLServer7,多台机器同时测试,服务起了6百多个线程,都还在正常运作)。
 
顶部