ADO是Active Data Objects的缩写,想必很多朋友对它都有所了解,在这里我就不详细展开说了。而“线程”——thread我个人认为是一个相当专业的词汇,再学过了操作系统这门课后,对它才有了一些真正的认识。在介绍ADO.NET的线程技术之前,我先来简单阐述一下线程的含义。
线程是允许程序的一部分独立于其他部分运行。线程可以在单个线程执行的同时运行多个操作,让用户感到像同时发生的一样,即使其中的某些线程出现错误,相互间的操作也不会直接受到影响。一个多线程功能的典型范例是Office中的Word拼写检查程序。在程序开始时,执行指针位于该程序的顶部,然后移动至开始读入代码的位置。不过Word同时还将开始另一个线程并创建另一个执行指针。当键入文本时,这个新线程将检查在文档中输入的文本,并给有拼写错误的地方置于红色的波浪线标记,这个大家在打英文的时候,会常见到。说到线程就不能不谈“进程”这个词,这两个词几乎总是同时出现。Windows可以将许多程序同时保存在内存中,并允许用户在程序之间来回切换。这种能够同时运行多个程序的能力称作多任务。一个进程中可以包含许多单独的线程。所以要注意,多任务和多线程并不是一回事。既然可能有许多线程,便可以对不同的线程指定不同的优先级。
聊了半天线程,下面我们把它和ADO.NET结合起来谈。在数据库访问领域,线程可以建立具有大量数据的控件而不阻止用户与其他控件交互。.NET Framework提高了运行多个执行线程的可编程性。在深入介绍ADO.NET的异步操作之前,要说明几点。
1. | NET Framework用System .Threading名称空间简化生成线程的工作,但这同样是危险的。线程可能造成很难查错的异常行为,我想大家在Windows操作系统下使用多种软件时出现的令人费解的错误已经很多了。 |
2. | 线程应用程序很难调试,但调试是非常重要的,如果使用在银行系统,医疗系统,出现异常错误,损失会很大。 |
3. | 需要认真管理线程,多线程应用程序中的线程实际上共享相同的内存空间,在同一进程中的线程间有可能覆盖对方的重要数据。 |
对上面的内容有所认识后,当然,我们的实例还不牵扯到上面说到得那么复杂的情况,作为初学者可以先不管那么多,我们首先介绍System .Threading名称空间。
System .Threading名称空间放置建立多线程应用程序的主要组件。
ThreadStart代理
使用ThreadStart线程代理可以指定生成线程时要执行的方法名。ThreadStart代理并不实际运行线程。需要等调用Start()方法时,线程才用线程代理中指定的方法开始执行。可以把ThreadStart看成线程的进入点。生成ThreadStart对象时,指定线程开始执行时要运行的方法的指针。可以用重载构造函数指定这个值:
Dim tsFill As System . Threading . ThreadStart = New System . Threading . ThreadStart(AddressOf MyMethod) |
指定为线程代理的方法不能接受任何参数,即MyMethod方法是无参函数,否则会出现错误。
如果要指定特定的输入条件,可以把方法已到另一个类中,然后在运行时设置这个类的属性,传入作为方法参数的信息。此外,方法应为子程序,而不是返回数值的函数。如果线程代理返回数值,则会限制在同步操作,不能利用异步线程的好处。子程序执行完毕时,不返回数值,而是发出一个事件。这样就可以告诉调用代码返回的信息。
设置ThreadStart代理之后,可以将其传入Thread对象的新实例。
Thread对象
System . Threading . Thread对象是应用程序中生成不同执行线程的基础类。可以用System . Threading . Thread对象并传入ThreadStart代理到构造函数中,生成一个线程:
Dim thdFill As System.Threading . Thread thdFill = New Thread(tsFill) |
在上面的代码中,我们传入类型为ThreadStart代理的对象到Thread对象的构造函数中。还有一种省略生成ThreadStart对象的方法,直接将指针传入方法代理:
Dim thdFill As New System . Threading . Thread(AddressOf MyMethod) |
Thread对象的构造函数需要一个参数,是线程代理,可以用ThreadStart对象或AddressOf运算符传递。
Start()方法
Start()方法是System . Threading名称空间中最重要的方法。这个方法负责实际派生线程。它利用ThreadStart()对象中指定的线程代理确定线程执行开始的具体方法。Start()方法和其他线程操作方法一起使用。调用这个方法之后,可以用ThreadStart属性监视线程的状态。注意:线程只能启动一次。如果多次调用这个方法,则会产生异常。
CurrentThread属性
使用多个线程时,可能要在特定线程执行时进行修改,这是要使用CurrentThread属性。
管理线程
派生线程和随其自己运行有些时候是无法满足需求的,可能要根据特定逻辑暂停和恢复线程执行。可能要在发现某些地方出错使用某种线程安全控件中止线程执行。Thread对象提供了一些方法可以密切控制线程行为。
1. Start()方法 | 上面已经介绍过 |
2. Abort()方法 | Thread对象的Abort()方法终止特定线程执行。这个方法通常和ThreadState与IsAlive属性一起使用,确定特定线程的状态。调用Abort()方法时,线程并不自动死亡。实际上还要调用Join()方法完成终止过程。即使如此,线程关闭之前要执行Try块中的所有Finally从句。对没有启动的线程调用Abort()方法,它启动并停止。对暂停的线程调用Abort()方法,它恢复并停止。如果线程处于等待状态,受阻或休眠,则调用Abort()方法时首先中断线程,然后中止线程。 |
3. Join()方法 | Join()方法用超时参数,等待线程死亡或超时。Join()方法返回一个布尔值。如果线程已经终止,则这个方法返回True。如果发生超时,则这个方法返回False。 |
4. Sleep()方法 | Sleep()方法在一定时间内暂停线程进行的任何活动。将线程置于休眠方式时要小心选择。不要把使用外部资源的线程置于休眠方式,例如数据库连接,否则会异常锁住资源。此外,不要对控件之类的Windows窗体对象将线程置于休眠方式,因为Windwos窗体使用single-threaded apartment (STA)。 |
5. Suspend()方法 | Suspend()方法推迟线程对任何活动的处理。如果调用Resume()方法,则处理继续。和Sleep()方法一样,不要暂停使用数据库连接的线程,Windows 窗体和控件。最好不是强制线程暂停和恢复,而是用线程状态属性改变线程的行为。因为处理多个线程需要占用大量处理器资源。线程暂停和恢复是很费资源的。多个线程暂停和恢复成为情景切换。 |
6. Resume()方法 | Resume()方法继续处理暂停的线程。 |
7. Interrupt()方法 | Interrupt()方法请求线程在离开等待、休眠或连接状态之后停止工作。 Interrupt()方法不会像Abort()方法那样产生无法捕获的ThreadAbortException |
ThreadState线程状态
下表是ThreadState属性的枚举值:
数值
|
说明
|
Aborted | 线程处于停止状态 |
AbortRequested | Thread.Abort方法已经被调用,但线程还未收到该信息,System .Threading . ThreadAbortException将终止该线程 |
Background | 线程作为后台线程执行,Thread . IsBackground属性决定线程为后台线程。 |
Running | 线程正在执行 |
Stopped | 线程已经停止 |
StopRequested | 线程正在被请求停止 |
Suspended | 线程被暂停 |
SuspendRequested | 线程正在被请求暂停 |
Unstarted | Thread . Start方法还未被线程调用 |
WaitSleepJoin | 线程处于等待、休眠或连接状态 |
下面我们来实现一个非常简单的ADO . NET线程应用程序:
首先,打开Microsoft Visual Studio . NET我们建立一个新的Windows应用程序,命名为ADO Threading,如图:
建立双搜索引擎
建立应用程序后,要构造两个搜索引擎。将下列控件添加到窗体上并排列好:2个TextBox,2个Button,2个DataGrid,如图:
清空2个TextBox的Text属性;2个Button的Text属性分别为:
Search for Customers By Country;Search for Orders By Customer |
将第一个搜索引擎配制成根据客户所在国家搜索客户的引擎。首先拖动一个SqlDataAdapter控件到窗体上,SqlDataAdapter控件在Toolbox中的Data部分中。然后右键点击SqlDataAdapter选中弹出的Configure Data Adapter,接着会弹出Data Adapter Configuration Wizard
点Next后选择要连接的数据库,在这个实验中,我们选择SQL Server2000已建好的Northwind数据库,想必大家在初学数据库时这个数据库的名称会频繁出现。
Next后选择Using existing stored procedures(用已存在的存储过程),接着在 Bind Commands to Existing Stored Procedures中的Select菜单中选择GetCustomersByCountry存储过程
然后选择Finish即可。GetCustomersByCountry存储过程,Northwind数据库里没有是新编写的,内容如下:
ALTER PROCEDURE GetCustomersByCountry @CountryName varchar(15) AS SELECT * FROM Customers WHERE Country=@CountryName |
然后用这个DataAdapter生成DataSet,如图
将DataSet命名为DsCustomersByCountry1。然后将第一个DataGrid的DataSource属性设置为新建的DsCustomersByCountry1 DataSet
这样第一个搜索引擎就配置好了。下面来配置第二个搜索引擎,步骤基本上和配置第一个搜索引擎相同这里就不再细说了。其中将存储过程选为SelectOrdersByCustomer,内容如下:
ALTER PROCEDURE SelectOrdersByCustomer @CustomerID char(5) AS SET NOCOUNT ON; SELECT OrderID,CustomerID,OrderDate,ShippedDate,ShipVia,Freight FROM Orders WHERE customerID=@customerID |
生成的DataSet命名为DsOrdersByCustomer1,然后配置第二个DataGrid的DataSource属性为DsOrdersByCustomer1 DataSet。数据库最终配置完,窗体下部显示内容如下图:
接下来进入最重要的编码阶段。首先要将TextBox控件中的搜索条件传递到每个SelectCommand的Parameter对象的Value属性中。然后为每个按钮的单击事件添加代码逻辑。
将国家名查找条件与SelectCommand的Parameter相联系
Private Sub Button1_Click _ 'Populate customers by country name Try |
将CustomerID查找条件与SelectCommand的Parameter相联系
Private Sub Button2_Click _ 'Populate orders by customer Try |
FillOrders()子程序
Private Sub FillOrders() Try DsOrdersByCustomer1 . Clear() Me . SqlDataAdapter2 . Fill(DsOrdersByCustomer1) Catch excFill As SqlClient . SqlException Console . WriteLine(excFill . Message) Catch excGeneral As System . Exception Console . WriteLine(excGeneral . Message) End Try End Sub |
FillCustomers()子程序
Private Sub FillCustomers() Try DsOrdersByCustomer1 . Clear() Me . SqlDataAdapter1 . Fill(DsCustomersByCountry1) Catch excFill As SqlClient . SqlException Console . WriteLine(excFill . Message) Catch excGeneral As System . Exception Console . WriteLine(excGeneral . Message) End Try End Sub |
好啦!大家可以先运行并试验一下每个搜索引擎。第一个搜索引擎接受国家名作为查找条件,提供属于指定国家的客户名单。第二个搜索引擎接受CustomerID作为查找条件,提供属于指定客户的订单。不过大家要注意的是要等第一个搜索完成之后才能进行新的搜索,在实验的时候大家手可要快,因为现在的电脑配置都很高,搜索需要的时间很短,大家可以在按下查找按钮后快速将光标移到第二个TextBox上,光标是无法放在文本框中的。这时就需要线程来解决这个问题。
生成线程代理
下面处理第一个搜索引擎,按国家取得客户。首先要导入System .Threading名称空间,以便直接引用Threading类成员。在定义类form1前增加Imports System .Threading语句。然后要生成线程代理,代替直接调用的FillCustomers()方法。修改第二个Try块,用下列代码代替FillCustomers()方法。
Dim tsFill As ThreadStart = New ThreadStart(AddressOf clsFiller . FillCustomers) |
这行代码生成ThreadStart对象,将线程代理作为构造函数输入参数传递到FillCustomers()。
生成新线程
用下列语句声明线程对象:
Dim thdFill As Thread |
然后实例化这个线程:
thdFill = New Thread(tsFill) |
其利用重载构造函数,传入线程代理作为新线程的跳转点。最后用Thread对象的Start()方法开始执行线程:
thdFill . Start() |
生成对象包装属性
.NET Framework采用应用程序域(AppDomains)提供的逻辑隔离补充物理进程隔离。应用程序中的线程在AppDomains的逻辑限制中运行。这个主线程是应用程序进程中的主执行逻辑。
但我们派生出填充DataSet的新进程,它在主应用程序线程之外运行,这样,一个线程专用的对象、属性和方法就无法被另一个线程访问。
thdFill线程首先调用FillCustomers()方法的线程代理。FillCustomers()方法操纵本地窗体对象,SqlDataAdapter1与DsCustomersByCountry1对象。这些对象隐藏在窗体的AppDomains中,外部thdFill线程无法访问。
要解决这个问题我们可以在线程中生成每个对象的包装属性。要对线程生成属性,就要把线程逻辑移到单独的类中。右键单击Solution Explorer中的ADO Threading选择Add->Add New Item。
我们添加一个类,名称为Filler.vb。将FillCustomers()方法移到这个类中。在类代码开头增加Imports System . Data . SqlClient,以便使用SqlClient . NET数据提供者对象。然后包装对象,需要包装的有DataAdapter,DataSet,DataGrid三个对象。这是我们将第一个DataGrid的DataSource属性中置为none,我们在线程运行是通过程序进行数据关联。
下面是Filler类的代码:
Imports System.Data . SqlClient Public Class Filler Public Sub FillCustomers() Public Property CustDataSet() As DataSet Public Property CustDataAdapter() As SqlDataAdapter Public Property CustDataGrid() As DataGrid |
最后我们来修改Button1_Click事件,首先要生成表示Filler类的新变量:
Dim clsFiller As New Filler() |
然后我们设置DataAdapter与DataSet属性:
clsFiller . CustDataAdapter = Me . SqlDataAdapter1 clsFiller . CustDataSet = Me . DsCustomersByCountry1 |
下面是完整的Button1_Click事件:
Private Sub Button1_Click _ 'Populate customers by country name Try |
按F5执行程序,与上次不同的是,开始进行第一个搜索时,可以继续使用应用程序。可以在进行第一个搜索时在第二个搜索条件框中输入新数据。在实际运行中会出现异常,因为这里没有牵扯到更高级的管理线程的代码部分,这样会出现在窗体对象的线程中同时写入相同的内存空间的情况。
真正编写一个好的多线程程序是非常困难的,我们在这里只是编一个非常简单的“残品”,希望大家对线程程序有所了解。