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 [email protected] |
然後用這個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 [email protected] |
生成的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執行程式,與上次不同的是,開始進行第一個搜尋時,可以繼續使用應用程式。可以在進行第一個搜尋時在第二個搜尋條件框中輸入新資料。在實際運作中會出現異常,因為這裡沒有牽扯到更進階的管理線程的代碼部分,這樣會出現在窗體對象的線程中同時寫入相同的記憶體空間的情況。
真正編寫一個好的多線程程式是非常困難的,我們在這裡隻是編一個非常簡單的“殘品”,希望大家對線程程式有所了解。