大家應該掌握的多線程編程
毫無疑問,多線程在各種編程語言中都占有比較重要的一個席位。不管你是初學者,還是資深的老司機,多線程是在學習,面試和工作中都要經(jīng)常被提及的一個話題,下面我們就來看一看具體的相關(guān)內(nèi)容。
1、多線程編程必備知識
1.1 進程與線程的概念
當我們打開一個應用程序后,操作系統(tǒng)就會為該應用程序分配一個進程ID,例如打開QQ,你將在任務管理器的進程選項卡看到QQ.exe進程,如下圖:
進程可以理解為一塊包含了某些資源的內(nèi)存區(qū)域,操作系統(tǒng)通過進程這一方式把它的工作劃分為不同的單元。一個應用程序可以對應于多個進程。
線程是進程中的獨立執(zhí)行單元,對于操作系統(tǒng)而言,它通過調(diào)度線程來使應用程序工作,一個進程中至少包含一個線程,我們把該線程成為主線程。線程與進程之間的關(guān)系可以理解為:線程是進程的執(zhí)行單元,操作系統(tǒng)通過調(diào)度線程來使應用程序工作;而進程則是線程的容器,它由操作系統(tǒng)創(chuàng)建,又在具體的執(zhí)行過程中創(chuàng)建了線程。
1.2線程的調(diào)度
在操作系統(tǒng)的書中貌似有提過,“Windows是搶占式多線程操作系統(tǒng)”。之所以這么說它是搶占式的,是因為線程可以在任意時間里被搶占,來調(diào)度另一個線程。操作系統(tǒng)為每個線程分配了0-31中的某一級優(yōu)先級,而且會把優(yōu)先級高的線程優(yōu)先分配給CPU執(zhí)行。
Windows支持7個相對線程優(yōu)先級:Idle、Lowest、BelowNormal、Normal、AboveNormal、Highest和Time-Critical。其中,Normal是默認的線程優(yōu)先級。程序可以通過設置Thread的Priority屬性來改變線程的優(yōu)先級,該屬性的類型為ThreadPriority枚舉類型,其成員包括Lowest、BelowNormal、Normal、AboveNormal和Highest。CLR為自己保留了Idle和Time-Critical兩個優(yōu)先級。
1.3線程也分前后臺
線程有前臺線程和后臺線程之分。在一個進程中,當所有前臺線程停止運行后,CLR會強制結(jié)束所有仍在運行的后臺線程,這些后臺線程被直接終止,卻不會拋出任何異常。主線程將一直是前臺線程。我們可以使用Tread類來創(chuàng)建前臺線程。
using System;
using System.Threading;
namespace 多線程1
{
internal class Program
{
private static void Main(string[] args)
{
var backThread = new Thread(Worker);
backThread.IsBackground = true;
backThread.Start();
Console.WriteLine("從主線程退出");
Console.ReadKey();
}
private static void Worker()
{
Thread.Sleep(1000);
Console.WriteLine("從后臺線程退出");
}
}
}
以上代碼先通過Thread類創(chuàng)建了一個線程對象,然后通過設置IsBackground屬性來指明該線程為后臺線程。如果不設置這個屬性,則默認為前臺線程。接著調(diào)用了Start的方法,此時后臺線程會執(zhí)行Worker函數(shù)的代碼。所以在這個程序中有兩個線程,一個是運行Main函數(shù)的主線程,一個是運行Worker線程的后臺線程。由于前臺線程執(zhí)行完畢后CLR會無條件地終止后臺線程的運行,所以在前面的代碼中,若啟動了后臺線程,則主線程將會繼續(xù)運行。主線程執(zhí)行完后,CLR發(fā)現(xiàn)主線程結(jié)束,會終止后臺線程,然后使整個應用程序結(jié)束運行,所以Worker函數(shù)中的Console語句將不會執(zhí)行。所以上面代碼的結(jié)果是不會運行Worker函數(shù)中的Console語句的。
可以使用Join函數(shù)的方法,確保主線程會在后臺線程執(zhí)行結(jié)束后才開始運行。
using System;
using System.Threading;
namespace 多線程1
{
internal class Program
{
private static void Main(string[] args)
{
var backThread = new Thread(Worker);
backThread.IsBackground = true;
backThread.Start();
backThread.Join();
Console.WriteLine("從主線程退出");
Console.ReadKey();
}
private static void Worker()
{
Thread.Sleep(1000);
Console.WriteLine("從后臺線程退出");
}
}
}
以上代碼調(diào)用Join函數(shù)來確保主線程會在后臺線程結(jié)束后再運行。
如果你線程執(zhí)行的方法需要參數(shù),則就需要使用new Thread的重載構(gòu)造函數(shù)Thread(ParameterizedThreadStart).
using System;
using System.Threading;
namespace 多線程1
{
internal class Program
{
private static void Main(string[] args)
{
var backThread = new Thread(new ParameterizedThreadStart(Worker));
backThread.IsBackground = true;
backThread.Start("Helius");
backThread.Join();
Console.WriteLine("從主線程退出");
Console.ReadKey();
}
private static void Worker(object data)
{
Thread.Sleep(1000);
Console.WriteLine($"傳入的參數(shù)為{data.ToString()}");
}
}
}
執(zhí)行結(jié)果為:
2、線程的容器——線程池
前面我們都是通過Thead類來手動創(chuàng)建線程的,然而線程的創(chuàng)建和銷毀會耗費大量時間,這樣的手動操作將造成性能損失。因此,為了避免因通過Thread手動創(chuàng)建線程而造成的損失,.NET引入了線程池機制。
2.1 線程池
線程池是指用來存放應用程序中要使用的線程集合,可以將它理解為一個存放線程的地方,這種集中存放的方式有利于對線程進行管理。
CLR初始化時,線程池中是沒有線程的。在內(nèi)部,線程池維護了一個操作請求隊列,當應用程序想要執(zhí)行一個異步操作時,需要調(diào)用QueueUserWorkItem方法來將對應的任務添加到線程池的請求隊列中。線程池實現(xiàn)的代碼會從隊列中提取,并將其委派給線程池中的線程去執(zhí)行。如果線程池沒有空閑的線程,則線程池也會創(chuàng)建一個新線程去執(zhí)行提取的任務。而當線程池線程完成某個任務時,線程不會被銷毀,而是返回到線程池中,等待響應另一個請求。由于線程不會被銷毀,所以也就避免了性能損失。記住,線程池里的線程都是后臺線程,默認級別是Normal。
2.2 通過線程池來實現(xiàn)多線程
要使用線程池的線程,需要調(diào)用靜態(tài)方法ThreadPool.QueueUserWorkItem,以指定線程要調(diào)用的方法,該靜態(tài)方法有兩個重載版本:
public static bool QueueUserWorkItem(WaitCallback callBack);
public static bool QueueUserWorkItem(WaitCallback callback,Object state)
這兩個方法用于向線程池隊列添加一個工作先以及一個可選的狀態(tài)數(shù)據(jù)。然后,這兩個方法就會立即返回。下面通過實例來演示如何使用線程池來實現(xiàn)多線程編程。
using System;
using System.Threading;
namespace 多線程2
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine($"主線程ID={Thread.CurrentThread.ManagedThreadId}");
ThreadPool.QueueUserWorkItem(CallBackWorkItem);
ThreadPool.QueueUserWorkItem(CallBackWorkItem,"work");
Thread.Sleep(3000);
Console.WriteLine("主線程退出");
Console.ReadKey();
}
private static void CallBackWorkItem(object state)
{
Console.WriteLine("線程池線程開始執(zhí)行");
if (state != null)
{
Console.WriteLine($"線程池線程ID={Thread.CurrentThread.ManagedThreadId},傳入的參數(shù)為{state.ToString()}");
}
else
{
Console.WriteLine($"線程池線程ID={Thread.CurrentThread.ManagedThreadId}");
}
}
}
}
結(jié)果為:
2.3 協(xié)作式取消線程池線程
.NET Framework提供了取消操作的模式,這個模式是協(xié)作式的。為了取消一個操作,必須創(chuàng)建一個System.Threading.CancellationTokenSource對象。下面還是使用代碼來演示一下:
using System;
using System.Threading;
namespace 多線程3
{
internal class Program
{
private static void Main(string[] args)
{
Console.WriteLine("主線程運行");
var cts = new CancellationTokenSource();
ThreadPool.QueueUserWorkItem(Callback, cts.Token);
Console.WriteLine("按下回車鍵來取消操作");
Console.Read();
cts.Cancel();
Console.ReadKey();
}
private static void Callback(object state)
{
var token = (CancellationToken) state;
Console.WriteLine("開始計數(shù)");
Count(token, 1000);
}
private static void Count(CancellationToken token, int count)
{
for (var i = 0; i < count; i++)
{
if (token.IsCancellationRequested)
{
Console.WriteLine("計數(shù)取消");
return;
}
Console.WriteLine($"計數(shù)為:{i}");
Thread.Sleep(300);
}
Console.WriteLine("計數(shù)完成");
}
}
}
結(jié)果為:
3、線程同步
線程同步計數(shù)是指多線程程序中,為了保證后者線程,只有等待前者線程完成之后才能繼續(xù)執(zhí)行。這就好比生活中排隊買票,在前面的人沒買到票之前,后面的人必須等待。
3.1 多線程程序中存在的隱患
多線程可能同時去訪問一個共享資源,這將損壞資源中所保存的數(shù)據(jù)。這種情況下,只能采用線程同步技術(shù)。
3.2 使用監(jiān)視器對象實現(xiàn)線程同步
監(jiān)視器對象(Monitor)能夠確保線程擁有對共享資源的互斥訪問權(quán),C#通過lock關(guān)鍵字來提供簡化的語法。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace 線程同步
{
class Program
{
private static int tickets = 100;
static object globalObj=new object();
static void Main(string[] args)
{
Thread thread1=new Thread(SaleTicketThread1);
Thread thread2=new Thread(SaleTicketThread2);
thread1.Start();
thread2.Start();
Console.ReadKey();
}
private static void SaleTicketThread2()
{
while (true)
{
try
{
Monitor.Enter(globalObj);
Thread.Sleep(1);
if (tickets > 0)
{
Console.WriteLine($"線程2出票:{tickets--}");
}
else
{
break;
}
}
catch (Exception)
{
throw;
}
finally
{
Monitor.Exit(globalObj);
}
}
}
private static void SaleTicketThread1()
{
while (true)
{
try
{
Monitor.Enter(globalObj);
Thread.Sleep(1);
if (tickets > 0)
{
Console.WriteLine($"線程1出票:{tickets--}");
}
else
{
break;
}
}
catch (Exception)
{
throw;
}
finally
{
Monitor.Exit(globalObj);
}
}
}
}
}
在以上代碼中,首先額外定義了一個靜態(tài)全局變量globalObj,并將其作為參數(shù)傳遞給Enter方法。使用了Monitor鎖定的對象需要為引用類型,而不能為值類型。因為在將值類型傳遞給Enter時,它將被先裝箱為一個單獨的毒香,之后再傳遞給Enter方法;而在將變量傳遞給Exit方法時,也會創(chuàng)建一個單獨的引用對象。此時,傳遞給Enter方法的對象和傳遞給Exit方法的對象不同,Monitor將會引發(fā)SynchronizationLockException異常。
3.3線程同步技術(shù)存在的問題
(1)使用比較繁瑣。要用額外的代碼把多個線程同時訪問的數(shù)據(jù)包圍起來,還并不能遺漏。
(2)使用線程同步會影響程序性能。因為獲取和釋放同步鎖是需要時間的;并且決定那個線程先獲得鎖的時候,CPU也要進行協(xié)調(diào)。這些額外的工作都會對性能造成影響。
(3)線程同步每次只允許一個線程訪問資源,這會導致線程堵塞。繼而系統(tǒng)會創(chuàng)建更多的線程,CPU也就要負擔更繁重的調(diào)度工作。這個過程會對性能造成影響。
下面就由代碼來解釋一下性能的差距:
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace 線程同步2
{
class Program
{
static void Main(string[] args)
{
int x = 0;
const int iterationNumber = 5000000;
Stopwatch stopwatch=Stopwatch.StartNew();
for (int i = 0; i < iterationNumber; i++)
{
x++;
}
Console.WriteLine($"不使用鎖的情況下花費的時間:{stopwatch.ElapsedMilliseconds}ms");
stopwatch.Restart();
for (int i = 0; i < iterationNumber; i++)
{
Interlocked.Increment(ref x);
}
Console.WriteLine($"使用鎖的情況下花費的時間:{stopwatch.ElapsedMilliseconds}ms");
Console.ReadKey();
}
}
}
執(zhí)行結(jié)果:
總結(jié)
以上就是本文關(guān)于大家應該掌握的多線程編程的全部內(nèi)容,希望對大家有所幫助。感興趣的朋友可以繼續(xù)參閱本站其他相關(guān)專題,如有不足之處,歡迎留言指出。感謝朋友們對本站的支持!
您可能感興趣的文章
- 01-10C#連接數(shù)據(jù)庫的方法
- 01-10C#簡單郵件群發(fā)通用類
- 01-10簡單掌握Windows中C#啟動外部程序進程的方法
- 01-10基于C#實現(xiàn)網(wǎng)頁爬蟲
- 01-10C#中如何獲取文件圖標
- 01-10C#如何遍歷Dictionary
- 01-10C#重寫DataGridView
- 01-10C#實現(xiàn)Winform版計算器
- 01-10分享C#中幾個可用的類
- 01-10C#實現(xiàn)字符串倒序的寫法


閱讀排行
本欄相關(guān)
- 01-10C#通過反射獲取當前工程中所有窗體并
- 01-10關(guān)于ASP網(wǎng)頁無法打開的解決方案
- 01-10WinForm限制窗體不能移到屏幕外的方法
- 01-10WinForm繪制圓角的方法
- 01-10C#實現(xiàn)txt定位指定行完整實例
- 01-10WinForm實現(xiàn)仿視頻播放器左下角滾動新
- 01-10C#停止線程的方法
- 01-10C#實現(xiàn)清空回收站的方法
- 01-10C#通過重寫Panel改變邊框顏色與寬度的
- 01-10C#實現(xiàn)讀取注冊表監(jiān)控當前操作系統(tǒng)已
隨機閱讀
- 08-05織夢dedecms什么時候用欄目交叉功能?
- 01-10delphi制作wav文件的方法
- 01-11Mac OSX 打開原生自帶讀寫NTFS功能(圖文
- 01-10使用C語言求解撲克牌的順子及n個骰子
- 01-10SublimeText編譯C開發(fā)環(huán)境設置
- 01-11ajax實現(xiàn)頁面的局部加載
- 04-02jquery與jsp,用jquery
- 08-05dedecms(織夢)副欄目數(shù)量限制代碼修改
- 01-10C#中split用法實例總結(jié)
- 08-05DEDE織夢data目錄下的sessions文件夾有什


