天天看點

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

ADO.NET與線程操作

建立雙搜尋引擎

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

ADO.NET與線程操作
ADO.NET與線程操作

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

ADO.NET與線程操作

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

ADO.NET與線程操作

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

ADO.NET與線程操作

然後選擇Finish即可。GetCustomersByCountry存儲過程,Northwind資料庫裡沒有是新編寫的,内容如下:

ALTER PROCEDURE GetCustomersByCountry

@CountryName varchar(15)

AS

SELECT * FROM Customers

WHERE [email protected]

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

ADO.NET與線程操作

将DataSet命名為DsCustomersByCountry1。然後将第一個DataGrid的DataSource屬性設定為建立的DsCustomersByCountry1 DataSet

ADO.NET與線程操作

這樣第一個搜尋引擎就配置好了。下面來配置第二個搜尋引擎,步驟基本上和配置第一個搜尋引擎相同這裡就不再細說了。其中将存儲過程選為SelectOrdersByCustomer,内容如下:

ALTER PROCEDURE SelectOrdersByCustomer

@CustomerID char(5)

AS

SET NOCOUNT ON;

SELECT OrderID,CustomerID,OrderDate,ShippedDate,ShipVia,Freight

FROM Orders

WHERE [email protected]

生成的DataSet命名為DsOrdersByCustomer1,然後配置第二個DataGrid的DataSource屬性為DsOrdersByCustomer1 DataSet。資料庫最終配置完,窗體下部顯示内容如下圖:

ADO.NET與線程操作

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

ADO.NET與線程操作

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

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

繼續閱讀