最近由於工作的需要,一直在使用C#的多執行緒進行開發,其中也遇到了很多問題,但也都解決了。後來發覺自己對於執行緒的知識和運用不是很熟悉,所以將利用幾篇文章來系統性的學習彙總下C#中的多執行緒開發。
執行緒基礎
“程式是作業系統分配資源的最小單元,執行緒是作業系統排程的最小單元” 這句話應該學習計算機的朋友或多或少都聽說過,這在作業系統這門課中是很重要的一個概念。
在作業系統中可以同時執行很多個應用程式,那麼你知道計算機是如何分配和排程這些應用程式去使用CPU進行工作的嗎?
這裡面就牽扯到了程式、執行緒的概念,也就是我們接下來要學習的內容。
一個應用程式會有很多個執行緒,但是隻能有一個程式。也就是說一個程式中可以有很多個執行緒。那麼這是為什麼呢?以前計算機只有一個計算模組,每次只能單一的執行一個計算單元,不能同時執行多個計算任務。現在隨著科技的發展,有了多核CPU,可以一次性執行多個應用程式,這樣就實現了多工。作業系統為了不讓一個應用程式獨佔CPU,導致其餘程式掛起等待,不得不設計出一種將物理計算單元分割為一些虛擬的程式,並給予每個執行程式一定量的計算能力。此外,作業系統必須始終能夠優先訪問CPU,並能調整不同程式訪問CPU的優先順序(說白了就是典型的以空間換時間)。
執行緒正是這一概念的實現,可以認為執行緒是一個虛擬的程式,用於獨立執行一個特定的程式。
大量使用執行緒會消耗大量的OS資源
那麼為什麼需要使用執行緒呢!其實就是為了在相同的時間內,讓作業系統或CPU幹更多的活,那麼在C#中執行緒應該如何使用或者說在什麼場景下使用呢!
在C#中關於執行緒的使用,大多數時候是在當程式需要處理大量繁瑣、佔用資源多、花費大量時間的任務時進行應用,比如訪問資料庫,視訊顯示,檔案IO操作、網路傳輸等。
執行緒在應用程式中可以進行如何操作:1、建立執行緒;2、暫停執行緒;3、執行緒等待;4、終止執行緒。
1、建立執行緒
通過宣告並例項化Thread就可以建立執行緒,它接收方法作為引數。使用Thread.Start()就可以開啟子執行緒,讓其去執行方法中的內容。
static void Main(string[] args)
{
//新建立的執行緒中輸出
Thread oneThread = new Thread(PrintNumber);
oneThread.Start();
//主執行緒中輸出
PrintNumber();
Console.ReadKey();
}
static void PrintNumber()
{
Console.WriteLine("開始......");
for (int i = 0; i < 10; i++)
{
Console.WriteLine(i);
}
}
可以看到當我們在子執行緒和主執行緒中同時輸出PrintNumber()中的內容時,它是亂的隨機交叉輸出的。
2、暫停執行緒
暫停執行緒故名思意就是讓執行緒暫停,不讓其佔用CPU資源,在一直等待,啥時候取消暫停就恢復執行。在C#中暫停就是讓這個執行緒進入睡眠狀態,讓其休眠,不讓其佔用系統資源就可以了。
Thread.Sleep(TimeSpan.FromSeconds(2)); //睡眠2s
3、執行緒等待
執行緒等待就是多個執行緒在處理某個任務時,某個執行緒必須等待前一個執行緒處理所有資料後才可以進行執行,在這個期間,這個執行緒是阻塞狀態的。只有前一個執行緒完事了,他才可以再繼續執行。
static void Main(string[] args)
{
//新建立的執行緒中輸出
Thread oneThread = new Thread(PrintNumber);
oneThread.Start();
oneThread.Join();
//主執行緒中輸出
PrintNumber();
Console.ReadKey();
}
也就是說上面的程式主執行緒必須得等oneThread執行緒執行完PrintNumber方法後,它才可以執行。
4、執行緒終止
就是執行緒在執行過程中,利用某些操作(Thread.Abort())可以使其執行緒立即退出,不進行工作了。
static void Main(string[] args)
{
//新建立的執行緒中輸出
Thread oneThread = new Thread(PrintNumber);
oneThread.Start();
Thread.Sleep(TimeSpan.FromSeconds(6));
oneThread.Abort();
//主執行緒中輸出
PrintNumber();
Console.ReadKey();
}
上面的程式可以看到,當主程式再等待6s後,立即將oneThread執行緒終止掉。
其實Abort()方法是給執行緒注入了ThreadAbortException方法,導致執行緒被終結,這其實很危險,因為該執行緒可能正在處理某些重要的資料,比如接收傳輸資料等,這樣子就傳遞摧毀了程式,資料也就丟失了。還有就是這個方法不能保證100%終止執行緒。有時候有些異常會被吃掉,我們可以利用某些關鍵變數在子執行緒中進行控制,從而取消執行緒的執行就可以。
在實際編碼使用執行緒的過程中,可以通過oneThread.ThreadState來獲取目前執行緒的狀態。有時候我們也可以手動的設定執行緒的優先順序,設定為最高的則提前執行,但是這個只是針對於單核CPU時,目前市面上基本都是多核的了,這種使用場景也就很少了。
一般我們建立的執行緒都是屬於前臺執行緒,通過手動設定ontThread物件的IsBackground屬性為true時才會為後臺執行緒。通常前臺執行緒會比後臺執行緒提前執行完。當前臺執行緒執行完成後,程式結束並且後臺執行緒被終結。程式會等待所有的前臺執行緒完成後再結束工作,但是如果只剩下後臺執行緒,程式會直接結束工作。
C#中的lock關鍵字
某一個資源當被多個執行緒同時訪問時,可能這個資源的某些值對於各個執行緒來說會出問題。如果在某一時刻,一個執行緒是使其遞增,一個執行緒是遞減,會導致其值不唯一,各個執行緒拿到的值不對。這種情況就是所謂的競爭條件,競爭條件是多執行緒環境中非常常見的導致錯誤的原因。
class PepoleCount
{
int count = 0;
public void AddCount()
{
++count;
}
public void DeleteCount()
{
--count;
}
}
比如是上面的程式,當兩個執行緒同時訪問這個PepoleCount類時,會導致count變數出現競爭條件。就是每個執行緒可能拿到的數值不是最新的。那麼如何辦呢,此時就需要使用到lock機制,也就是加鎖。目的是為了當一個執行緒訪問某個資源時,其餘執行緒如果在訪問時,必須等待當前訪問完事後,它才可以訪問。保證了資料的有效性。
lock關鍵字是如果鎖定了一個物件,需要訪問該物件的所有其他執行緒則會處於阻塞狀態,並等待知道該物件解除鎖定才可以訪問。
class PepoleCount
{
private readonly object _syncRoot = new object();
int count = 0;
public void AddCount()
{
lock(_syncRoot)
{
++count;
}
}
public void DeleteCount()
{
lock(_syncRoot)
{
--count;
}
}
}
關於加鎖這塊還是有很多講究的,不是說每一個方法,每一個變數都需要進行加鎖,如果頻繁的加鎖會導致其餘執行緒處於阻塞狀態,那麼也會導致應用程式出現嚴重的效能問題。
好了,今天關於執行緒的分享就先到這裡。
期待下一篇文章的推送吧,希望我可以寫的簡單點,讓大家對多執行緒開發有一些全新的認識。
小寄語
人生短暫,我不想去追求自己看不見的,我只想抓住我能看的見的。
原創不易,給個關注。
我是阿輝,感謝您的閱讀,如果對你有幫助,麻煩點贊、轉發 謝謝。