1. 程式人生 > >.NET進階篇06-async非同步、thread多執行緒2

.NET進階篇06-async非同步、thread多執行緒2

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

內容目錄

一、執行緒Thread1、生命週期2、後臺執行緒3、靜態方法1.執行緒本地儲存2.記憶體柵欄4、返回值二、執行緒池ThreadPool1、工作佇列2、工作執行緒和IO執行緒3、和Thread區別4、定時器

一、執行緒Thread

.NET中執行緒操作封裝為了Thread類,可以讓開發者對執行緒進行直觀操作。Thread提供了例項方法用於管理執行緒的生命週期和靜態方法用於控制執行緒的一些訪問儲存等一些外在的屬性,相當於工作空間環境變量了

1、生命週期

執行緒的生命週期有建立、啟動、可能掛起、等待、恢復、異常、然後結束

。用Thread類可以容易控制一個執行緒的全生命週期

Thread類的建構函式過載可以接受ThreadStart無引數和ParameterizedThreadStart有引數的委託,然後呼叫例項的Start()方法啟動執行緒。Thread的建構函式的帶有引數的委託,引數是一個object型別,因為我們可以傳入任何資訊

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(),當然我們可以傳入時間引數來表明我主執行緒最多等你多久

2、後臺執行緒

上一章我們知道Thread預設建立的是前臺執行緒,前臺執行緒會阻止系統程序的退出,就是啟動之後一定要完成任務的後臺執行緒會伴隨著程序的退出而退出。通過設定屬性IsBackground=true改為後臺執行緒。另外還可以通過設定Priority指定執行緒的優先順序。但這個並不總會如你所想設定了高優先順序就一定最先執行。作業系統會優化排程,這也是執行緒不太好控制的原因之一

3、靜態方法

上面介紹的都是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的分開解釋,從處理器讀取,從處理器寫入。

4、返回值

前面宣告執行緒時,可以傳遞引數,那麼想要有返回值該如何去做呢?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<int> func = this.ThreadWithReturn<int>(() =>
{
    Thread.Sleep(2000);
    return DateTime.Now.Millisecond;
});
int iResult = func.Invoke();

二、執行緒池ThreadPool

.NET起初提供Thread執行緒類,功能很豐富,API也很多,所以使用起來比較困難,況且執行緒還不都是很像理想中執行,所以從2.0開始提供了ThreadPool執行緒池靜態類,全是靜態方法,隱藏了諸多Thread的介面,讓執行緒使用起來更輕鬆。執行緒池可用於執行任務、傳送工作項、處理非同步 I/O、代表其他執行緒等待以及處理計時器

1、工作佇列

常用ThreadPool執行緒池靜態方法QueueUserWorkItem用於將方法排入執行緒池佇列中執行,如果執行緒池中有閒置執行緒就會執行,QueueUserWorkItem方法的引數可以指定一個回撥函式委託並且傳入引數,像下面這樣

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

2、工作執行緒和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(16, 16);
ThreadPool.SetMinThreads(8, 8);

3、和Thread區別

如果計算機只有8個核,同時可以有8個任務執行。現在我們有10個任務需要執行,用Thread就需要建立10個執行緒,用ThreadPool可能只需要利用8個執行緒就行,節約了空間和時間。執行緒池中的執行緒預設先啟動最小執行緒數量的執行緒,然後根據需要增減數量。執行緒池使用起來簡單,但也有一些限制,執行緒池中的執行緒都是後臺執行緒,不能設定優先順序,常用於耗時較短的任務。執行緒池中執行緒也可以阻塞等待,利用ManualResetEvent去通知,但一般不會使用。

4、定時器

.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#的執行緒模式、執行緒同步鎖機制、異常處理,執行緒取消,執行緒安全集合和常見的執行緒問題

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

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