曾經剛接觸時多線程,認為甚是簡單,短短的幾行代碼就可以借助封裝好的方式實現自己的多線程。 這幾天學習在做C/S小項目時間碰見一個調試異常,才讓我真正沉下去認識線程,幸虧還有當年所學操作系統的知識,線程的調度知識,認識才更深一步。在這里我將以前的調試異常截圖,這篇文章就從此處入手,解釋我對線程的膚淺認識。提示信息如下圖所示:
仔細看代碼,發現項目中的列表框(listBox)是在UI主線程中的,而引起異常操作的地方發生在邏輯處理的又一個線程之中,在非UI線程調用UI線程,修改其屬性造成了這樣的異常:“線程間的操作無效”,我知道只要在UI線程的構造函數關閉線程調用異常檢測就可以避免這個提示,程序繼續正常走。可以這樣實現:
1: System.Windows.Forms.Control.CheckForIllegalCrossThreadCalls = false;
回頭想起這段代碼,總覺得這樣子做並不是最好的辦法。我回去查了資料,發現原先的.Net版本是沒有引入線程調用異常的檢測的,為了安全性微軟在線程的調用時引入了安全檢測。一種很糟糕的情況:如果有兩個或多個線程操作某一控件的狀態,則可能會迫使該控件進入一種不一致的狀態。還可能出現其他與線程相關的 bug,包括爭用情況和死鎖。確保以線程安全方式訪問控件非常重要。
既然微軟引入了安全線程的監測,那我們就應該盡量構建自己的安全代碼,而不是在此處為了圖方便設置去除安全機制,其實解決此類問題並不是很難,關鍵是真正的理解多線程的操作。多線程的使用看似只有那三四行代碼,但是要想真正的明白調度的過程,使用的技巧卻不是三言兩語就可以明白的。這里我從自己的理解角度出發,聯系自己看過的書籍對多線程的使用做出自己的解釋。
1)線程:
線程是程序中一個單一的順序控制流程。在單個程序中同時運行多個線程完成不同的工作,稱為多線程。多線程處理一個常見的例子就是用戶界面。利用線程,用戶可按下一個按鈕,然后程序會立即作出響應,而不是讓用戶等待程序完成了當前任務以后才開始響應。從程序設計語言的角度看,多線程操作最有價值的特性之一就是程序員不必關心到底使用了多少個處理器。程序在運行中就會虛擬有N個線程同時進行,相互中間沒有影響,相當於N個處理器一樣的工作效率,程序的處理速度自然加快。這時間就不得不注意一個問題-----沒錯,就是“資源共享”,面對資源共享的問題,我們知道兩個線程是不允許同時同步操作一個資源的,例如:一台打印機在某時刻只允許一個線程在操作。面對這樣的問題,多線程采用Lock方法解決。當完成此任務的時候解鎖釋放資源。(此時有沒有想到大學讀的操作系統的資源的調度哦)
再次必須鄭重聲明一點:多線程是為了同步完成多項任務,不是為了提高運行效率,而是為了提高資源使用效率來提高系統的效率。線程是在同一時間需要完成多項任務的時候實現的。
2)線程與進程的區別(進程VS線程)
看到這個小標題,每一個學習過計算機的同志都會在心里想出來點什么,但具體是什么又很難一時間全部是清楚,此處就作補充的吧想不明白的說一下:
屬於一個單一的應用程序的所有的線程邏輯上被包含在一種進程之中(線程是進程的子單位),進程是指一個應用程序所運行的操作系統單元。
簡單來講:進程是表示資源分配的基本單位,又是調度運行的基本單位;線程是進程中執行運算的最小單位,亦即執行處理機調度的基本單位。資源分配給進程,同一進程的所有線程共享該進程的所有資源。處理機分給線程,即真正在處理機上運行的是線程。
3)共享函數---線程不安全的根源
共享數據可以是:
- 1>函數把返回結果放到一個公共的位置
- 2>由調用者傳入的線程間共享的指針變量或者引用變量
- 3>函數內部本來就會使用的共享靜態變量
4)線程的可重入與安全
可重入:當被多個線程調用的時候,不會引用任何共享數據。我們可以理解為任何線程不安全的因素都是來源於“資源共享”,如果不適用任何共享是函數,線程是絕對安全的,但是多線程的優勢在於共享資源。因此解決問題並非是拒絕共享,而是正確調度,並發操作!這就是從不安全的線程操作到安全的線程操作。
線程不安全怎么能夠改寫成線程安全:
- 1>.仍舊使用共享數據,但是在使用共享數據的時候做同步操作。對於函數過程中使用的共享數據,可以進行簡單的PV操作,對於返回結果可以在PV操作中把共享數據拷貝到非共享的位置,以便及時釋放共享變量。
- 2>.杜絕使用共享變量。也就是說把函數改成了可重入的函數。但是,徹底杜絕共享變量有的時候不容易做到。對於一個不接受引用和指針的函數,我們可以把它做到絕對的可重入,但是,對於一個接受指針或者引用的函數來說,對不起,我們不能確保他肯定是可重入的。
這兩種方式那個比較好呢?通常來說,多線程是為了在同一時間內能夠處理更多的同樣類型的事情,但是線程不安全卻阻礙了我們達到我們的目的。所以,我們有的時候不得不想方設法的把線程不安全的函數改寫成線程安全的。除了第二種所說的劣勢之外,可重入,杜絕使用共享函數也是不太理想的,但是使用可重入方式的天然優勢是充分利用了處理機的優勢,不存在資源的等待釋放,PV操作以及軟件上的瓶頸,等,各有優勢視情況取舍。
話又說回來了,每當我們拿到一個線程不安全函數是一件郁悶的事情。但是有的時候你必須要使用它,那怎么辦呢?這個時候就要從上面提到的三個可能的共享數據來入手了:
1>如果調用函數的返回結果是共享的,進行簡單的PV操作,對於返回結果可以在PV操作中把共享數據拷貝到非共享的位置,以便及時釋放共享變量。
2>嘗試在調用函數的時候不使用引用或者指針,就不會有共享函數
3>找到這個函數的共享變量,對其做PV操作,或者修改代碼,不適用這個靜態變量
回到原始的問題,該怎么解決呢?第一就是設置不異常檢測:CheckForIllegalCrossThreadCalls = false,第二種辦法就是對window窗體多程程的安全調用
對 Windows 窗體控件進行線程安全調用步驟:
-
查詢控件的 InvokeRequired 屬性。
-
如果 InvokeRequired 返回 true,則使用實際調用控件的委托來調用 Invoke。
-
如果 InvokeRequired 返回 false,則直接調用控件。 例如:在UI線程之外修改UI內部的屬性
if (this.textBox1.InvokeRequired) //檢測是否有線程占用
{
SetTextCallback d = new SetTextCallback(SetText);
this.Invoke(d, new object[] { text }); //使用委托的方式
}
else
{
this.textBox1.Text = text; //直接使用
}
照此樣子就會解決這個問題!