ADO.NET与线程操作

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 _
  (ByVal sender As System.Object, ByVal e As System . EventArgs) _
  Handles Button1 . Click

  'Populate customers by country name
  Try
    SqlSelectCommand1 . Parameters("@CountryName") . Value() = TextBox1 . Text
  Catch excParam As System . Exception
    Console . WriteLine("Error at populating parameter " & excParam . Message)
  End Try

  Try
    FillCustomers()
  Catch excFill As SqlClient . SqlException
    Console . WriteLine(excFill . Message)
  Catch excGeneral As System . Exception
    Console . WriteLine(excGeneral . Message)
  End Try
End Sub

将CustomerID查找条件与SelectCommand的Parameter相联系

Private Sub Button2_Click _
  (ByVal sender As System . Object, ByVal e As System.EventArgs) _
  Handles Button2 . Click

  'Populate orders by customer
  Try
    SqlSelectCommand2 . Parameters("@CustomerID") . Value() = TextBox2 . Text
  Catch excParam As System . Exception
    Console . WriteLine("Error at populating parameter " & excParam . Message)
  End Try

  Try
    FillOrders()
  Catch excFill As SqlClient . SqlException
    Console . WriteLine(excFill . Message)
  Catch excGeneral As System . Exception
    Console . WriteLine(excGeneral . Message)
  End Try
End Sub

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
  Private m_dsCustomer As DataSet
  Private m_daCustomer As SqlDataAdapter
  Private m_dgCustomer As DataGrid

  Public Sub FillCustomers()
    Try
      m_dsCustomer . Clear()
      m_dgCustomer . DataSource = m_dsCustomer
      m_daCustomer . Fill(m_dsCustomer)
    Catch excFill As SqlClient . SqlException
      Console . WriteLine(excFill . Message)
    Catch excGeneral As System . Exception
      Console . WriteLine(excGeneral . Message)
    End Try
  End Sub

  Public Property CustDataSet() As DataSet
    Get
      CustDataSet = m_dsCustomer
    End Get
    Set(ByVal dsInput As DataSet)
      m_dsCustomer = dsInput
    End Set
  End Property

  Public Property CustDataAdapter() As SqlDataAdapter
    Get
      CustDataAdapter() = m_daCustomer
    End Get
    Set(ByVal daInput As SqlDataAdapter)
      m_daCustomer = daInput
    End Set
  End Property

  Public Property CustDataGrid() As DataGrid
    Get
      CustDataGrid = m_dgCustomer
    End Get
    Set(ByVal dgInput As DataGrid)
      m_dgCustomer = dgInput
    End Set
  End Property
End Class

最后我们来修改Button1_Click事件,首先要生成表示Filler类的新变量:

Dim clsFiller As New Filler()

然后我们设置DataAdapter与DataSet属性:

clsFiller . CustDataAdapter = Me . SqlDataAdapter1
clsFiller . CustDataSet = Me . DsCustomersByCountry1

下面是完整的Button1_Click事件:

Private Sub Button1_Click _
  (ByVal sender As System . Object, ByVal e As System . EventArgs) _
  Handles Button1.Click

    'Populate customers by country name
    Dim thdFill As Thread
    Dim clsFiller As New Filler()
    DataGrid1 . Refresh()
    Try
      SqlSelectCommand1.Parameters("@CountryName") . Value() = TextBox1 . Text
    Catch excParam As System . Exception
      Console . WriteLine("Error at populating parameter " & excParam . Message)
    End Try

    Try
      clsFiller . CustDataAdapter = Me . SqlDataAdapter1
      clsFiller . CustDataSet = Me . DsCustomersByCountry1
      clsFiller . CustDataGrid = Me . DataGrid1
      Dim tsFill As ThreadStart = New ThreadStart(AddressOf clsFiller . FillCustomers)
      thdFill = New Thread(tsFill)
      thdFill . Start()
    Catch excFill As SqlClient . SqlException
      Console . WriteLine(excFill . Message)
    Catch excGeneral As System . Exception
      Console . WriteLine(excGeneral . Message)
    End Try
End Sub

按F5执行程序,与上次不同的是,开始进行第一个搜索时,可以继续使用应用程序。可以在进行第一个搜索时在第二个搜索条件框中输入新数据。在实际运行中会出现异常,因为这里没有牵扯到更高级的管理线程的代码部分,这样会出现在窗体对象的线程中同时写入相同的内存空间的情况。

真正编写一个好的多线程程序是非常困难的,我们在这里只是编一个非常简单的“残品”,希望大家对线程程序有所了解。

你可能感兴趣的:(.net技巧)