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,如圖:

1.gif

建立雙搜索引擎


建立應用程序後,要構造兩個搜索引擎。將下列控件添加到窗體上並排列好:2個TextBox,2個Button,2個DataGrid,如圖:

2_01.gif

2_02.gif

清空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

3.gif

點Next後選擇要連接的數據庫,在這個實驗中,我們選擇SQL Server2000已建好的Northwind數據庫,想必大家在初學數據庫時這個數據庫的名稱會頻繁出現。

4.gif

Next後選擇Using existing stored procedures(用已存在的存儲過程),接着在 Bind Commands to Existing Stored Procedures中的Select菜單中選擇GetCustomersByCountry存儲過程

5.gif

然後選擇Finish即可。GetCustomersByCountry存儲過程,Northwind數據庫裏沒有是新編寫的,內容如下:

ALTER PROCEDURE GetCustomersByCountry
@CountryName varchar(15)
AS
SELECT * FROM Customers
WHERE Country=@CountryName

然後用這個DataAdapter生成DataSet,如圖

6.gif

將DataSet命名爲DsCustomersByCountry1。然後將第一個DataGrid的DataSource屬性設置爲新建的DsCustomersByCountry1 DataSet

7.gif

這樣第一個搜索引擎就配置好了。下面來配置第二個搜索引擎,步驟基本上和配置第一個搜索引擎相同這裏就不再細說了。其中將存儲過程選爲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。數據庫最終配置完,窗體下部顯示內容如下圖:

8.gif

接下來進入最重要的編碼階段。首先要將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。

9.gif

我們添加一個類,名稱爲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執行程序,與上次不同的是,開始進行第一個搜索時,可以繼續使用應用程序。可以在進行第一個搜索時在第二個搜索條件框中輸入新數據。在實際運行中會出現異常,因爲這裏沒有牽扯到更高級的管理線程的代碼部分,這樣會出現在窗體對象的線程中同時寫入相同的內存空間的情況。

真正編寫一個好的多線程程序是非常困難的,我們在這裏只是編一個非常簡單的“殘品”,希望大家對線程程序有所瞭解。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章