.NET進階篇06-async異步、thread多線程2

知識須要不斷積累、總結和沉澱,思考和寫做是成長的催化劑web

內容目錄

1、線程Thread一、生命週期二、後臺線程三、靜態方法1.線程本地存儲2.內存柵欄四、返回值2、線程池ThreadPool一、工做隊列二、工做線程和IO線程三、和Thread區別四、定時器安全

1、線程Thread

.NET中線程操做封裝爲了Thread類,可讓開發者對線程進行直觀操做。Thread提供了實例方法用於管理線程的生命週期和靜態方法用於控制線程的一些訪問存儲等一些外在的屬性,至關於工做空間環境變量了網絡

一、生命週期

線程的生命週期有建立、啓動、可能掛起、等待、恢復、異常、而後結束。用Thread類能夠容易控制一個線程的全生命週期多線程

Thread類的構造函數重載能夠接受ThreadStart無參數和ParameterizedThreadStart有參數的委託,而後調用實例的Start()方法啓動線程。Thread的構造函數的帶有參數的委託,參數是一個object類型,由於咱們能夠傳入任何信息app

Thread t1 = new Thread(() => {
    Console.WriteLine($"新線程  {Thread.CurrentThread.ManagedThreadId.ToString("00")}");
});
t1.Start();
Thread t2 = new Thread((obj) => {
    Console.WriteLine($"新線程  {Thread.CurrentThread.ManagedThreadId.ToString("00")},參數 {obj.ToString()}");
});
t2.Start("hello kitty");

線程啓動後,能夠調用線程的Suspend()掛起線程,線程就會處於休眠狀態(不繼續執行線程內代碼),調用Resume()喚醒線程,還有一個不太建議使用的Abort()經過拋出異常的方式來銷燬線程,隨後線程的狀態就會變爲AbortRequested框架

經常使用的還有線程的等待,在主線程上啓用工做線程後,有時須要等待工做線程的完成後,主線程才繼續工做。能夠調用實例方法Join(),固然咱們能夠傳入時間參數來代表我主線程最多等你多久異步

二、後臺線程

上一章咱們知道Thread默認建立的是前臺線程,前臺線程會阻止系統進程的退出,就是啓動以後必定要完成任務的後臺線程會伴隨着進程的退出而退出。經過設置屬性IsBackground=true改成後臺線程。另外還能夠經過設置Priority指定線程的優先級。但這個並不總會如你所想設置了高優先級就必定最早執行。操做系統會優化調度,這也是線程不太好控制的緣由之一函數

三、靜態方法

上面介紹的都是Tread的實例方法,Thread還有一些經常使用靜態方法。有時線程設置不當,會有些意想不到的的bug性能

1.線程本地存儲

AllocateDataSlot和AllocateNamedDataSlot用於給全部線程分配一個數據槽。像下面例子所示,若是不在子線程中給數據槽中放入數據,是獲取不到其餘線程往裏面放的數據。測試

var slot= Thread.AllocateNamedDataSlot("testSlot");
//Thread.FreeNamedDataSlot("testSlot");
Thread.SetData(slot, "hello kitty");
Thread t1 = new Thread(() => {
    //Thread.SetData(slot, "hello kitty");
    var obj = Thread.GetData(slot);
    Console.WriteLine($"子線程:{obj}");//obj沒有值
});
t1.Start();

var obj2 = Thread.GetData(slot);
Console.WriteLine($"主線程:{obj2}");

在聲明數據槽的時候.NET提醒咱們若是要更好的性能,請使用ThreadStaticAttribute標記字段。什麼意思?咱們來看下面這個例子

//
// 摘要:
//     在全部線程上分配未命名的數據槽。 爲了得到更好的性能,請改用以 System.ThreadStaticAttribute 特性標記的字段。
//
// 返回結果:
//     全部線程上已分配的命名數據槽。
public static LocalDataStoreSlot AllocateDataSlot();

例子中的若是不在靜態字段上標記ThreadStatic輸出結果就會一致。ThreadStatic標記指示各線程的靜態字段值是否惟一

[ThreadStatic]
static string name = string.Empty;
public void Function()
{
    name = "kitty";
    Thread t1 = new Thread(() => {
        Console.WriteLine($"子線程:{name}");//輸出空
    });
    t1.Start();
    Console.WriteLine($"主線程:{name}");//輸出kitty
}

還有一個ThreadLocal提供線程數據的本地存儲,用法和上面同樣,在每一個線程中聲明數據僅供本身使用

ThreadLocal<string> local = new ThreadLocal<string>() { };
local.Value = "hello kitty";
Thread t = new Thread(() => {
    Console.WriteLine($"子線程:{local.Value}");
});
t.Start();
Console.WriteLine($"主線程:{local.Value}");

上面的靜態方法用於線程的本地存儲TLS(Thread Local Storage),Thread.Sleep方法在開發調試時也是常常用的,讓線程掛起指定的時間來模擬耗時操做

2.內存柵欄

先說一個常識問題,爲何咱們發佈版本時候要用Release發佈?Release更小更快,作了不少優化,但優化對咱們是透明的(計算機裏透明認爲是個黑盒子,內部邏輯細節對咱們不開放,和生活中透明意味着徹底掌握瞭解不欺瞞恰好相反),通常優化不會影響程序的運行,咱們先借用網上的一個例子

bool isStop = false;
Thread t = new Thread(() => {
    bool isSuccess = false;
    while (!isStop)
    {
        isSuccess = !isStop;
    }
});
t.Start();
Thread.Sleep(1000);
isStop = true;
t.Join();
Console.WriteLine("主線程執行結束");

上面例子若是在debug下能正確執行完直到輸出「主程序執行結束」,然而在release下卻一直會等待子線程的完成。這裏子線程中isStop一直爲false。首先這是一個由多線程共享變量引發的問題,因此咱們建議最好的解決辦法就是儘可能不共享變量,其次可使用Thread.MemoryBarrier和VolatileRead/Write以及其餘鎖機制犧牲一點性能來換取數據的安全。(上面例子測試若是在子線程while中進行Console.writeLine操做,奇怪的發現release下也能正常輸出了,猜想應該是進行了內存數據的更新)

release優化會將t線程中的isStop變量的值加載到CPU Cache中,而主線程修改了isStop值在內存中,因此子線程拿不到更新後的值,形成數據不一致。那麼解決辦法就是取值時從內存中獲取。Thread.MemoryBarrier()就可讓在此方法以前的內存寫入都及時的從CPU Cache中更新到內存中,在此以後的內存讀取都要從內存中獲取,而不是CPU Cache。在例子中的while內增長Thread.MemoryBarrier()就能避免數據不一致問題。VolatileRead/Write是對MemoryBarrier的分開解釋,從處理器讀取,從處理器寫入。

四、返回值

前面聲明線程時,能夠傳遞參數,那麼想要有返回值該如何去作呢?Thread並無提供返回值的操做,後面.NET給出的對Thead的高級封裝給出瞭解決方案,直接使用便可。那目前咱們使用thread類就要本身實現下帶有返回值的線程操做,都是經過委託實現的,這裏簡單介紹一種,(共享外部變量也是能夠,不建議)

private Func<T> ThreadWithReturn<T>(Func<T> func)
{
    T t = default(T);
    Thread thread = new Thread(() =>
    {
        t = func.Invoke();
    });
    thread.Start();
    return () =>

    {
        thread.Join();
        return t;
    };
}
//調用
Func<intfunc = this.ThreadWithReturn<int>(() =>
{
    Thread.Sleep(2000);
    return DateTime.Now.Millisecond;
});
int iResult = func.Invoke();

2、線程池ThreadPool

.NET起初提供Thread線程類,功能很豐富,API也不少,因此使用起來比較困難,何況線程還不都是很像理想中運行,因此從2.0開始提供了ThreadPool線程池靜態類,全是靜態方法,隱藏了諸多Thread的接口,讓線程使用起來更輕鬆。線程池可用於執行任務、發送工做項、處理異步 I/O、表明其餘線程等待以及處理計時器

一、工做隊列

經常使用ThreadPool線程池靜態方法QueueUserWorkItem用於將方法排入線程池隊列中執行,若是線程池中有閒置線程就會執行,QueueUserWorkItem方法的參數能夠指定一個回調函數委託而且傳入參數,像下面這樣

ThreadPool.QueueUserWorkItem((obj) => {
                Console.WriteLine($"線程池中線程  {Thread.CurrentThread.ManagedThreadId.ToString("00")} ,傳入 {obj.ToString()}");
            },"hello kitty");

二、工做線程和IO線程

通常異步任務的執行,不涉及到網絡文件等IO操做的,計算密集型,開發者來調用。而IO線程通常用在文件網絡上,是CLR調用的,開發者無需管。工做線程發起文件訪問調用,由驅動器完成後通知IO線程,IO線程則執行異步任務的回調函數

獲取和設置最小最大的工做線程和IO線程

ThreadPool.GetMaxThreads(out int workerThreads, out int completionPortThreads);
ThreadPool.GetMinThreads(out int workerThreads, out int completionPortThreads);
ThreadPool.SetMaxThreads(1616);
ThreadPool.SetMinThreads(88);

三、和Thread區別

若是計算機只有8個核,同時能夠有8個任務運行。如今咱們有10個任務須要運行,用Thread就須要建立10個線程,用ThreadPool可能只須要利用8個線程就行,節約了空間和時間。線程池中的線程默認先啓動最小線程數量的線程,而後根據須要增減數量。線程池使用起來簡單,但也有一些限制,線程池中的線程都是後臺線程,不能設置優先級,經常使用於耗時較短的任務。線程池中線程也能夠阻塞等待,利用ManualResetEvent去通知,但通常不會使用。

四、定時器

.NET中有不少能夠實現定時器的功能,在ThreadPool中,咱們能夠利用RegisterWaitForSingleObject來註冊一個指定時間的委託等待。像下面這樣,將每隔一秒就輸出消息

ThreadPool.RegisterWaitForSingleObject(new AutoResetEvent(true), new WaitOrTimerCallback((obj, b) =>
{
    Console.WriteLine($"obj={obj},tid={Thread.CurrentThread.ManagedThreadId},datetime={DateTime.Now}");
}),"hello kitty",1000,false);

咱們日常見過比較多的仍是timer類,timer類在.net內是好幾個地方都有的,在System.Threading、
System.Timer、System.Windows.Form、System.Web.UI等裏面都有Timer,後面都是在第一個System.Threading裏的Timer擴展

System.Threading.Timer timer = new System.Threading.Timer((obj) =>
{
    Console.WriteLine($"obj={obj},tid={Thread.CurrentThread.ManagedThreadId},datetime={DateTime.Now}");
},"hello kitty",1000,1000);

timer的底層有一個TimerQueue,利用ThreadPool.UnsafeQueueUserWorkItem來完成定時功能,和上面咱們使用的ThreadPool定時器有一點區別

實際開發中,簡單定時timer就夠用,但通常業務場景比較複雜,須要定製個性化的定時器,好比每個月幾號執行,每個月第幾個星期幾,幾點執行,工做日執行等。所以咱們使用Quarz.NET定時框架,後面框架整合時會用到,用起來也是很簡單的

先就囉嗦這兩點吧,下一篇應該是Task、Parallel以及Async/Await,而後總結介紹下C#的線程模式、線程同步鎖機制、異常處理,線程取消,線程安全集合和常見的線程問題

天長水闊,見字如面,隨緣更新,拜了個拜~

可關注主頁公號獲取更多哈

相關文章
相關標籤/搜索