iT邦幫忙

0

[讀書筆記] Threading in C# - PART 1: GETTING STARTED

  • 分享至 

  • twitterImage
  •  

本篇同步發文在個人Blog: [讀書筆記] Threading in C# - PART 1: GETTING STARTED

前言

  這陣子換了新工作環境,公司使用不少C# Thread相關的技術,而知名書籍C# in a Nutshell的作者Joseph Albahari,將C# Thread的技術教學都免費公開,因此會閱讀他的教學文來撰寫讀書筆記,希望在工作專案或Side Project都有幫助到。

  作者有一些程式碼並非完整,我會盡量寫出實際可執行的範例,且有些功能Net Core以後不支援,也會加上註明。以下正式開始。


Introduction and Concepts

  • Thread是獨立的執行路徑, 也能同時和其他Thread工作

  • C# Client程式(Console, wpf, winform等), CLR都會起單一的Main thread執行

  • 被賦予工作的Thread, 只要那工作(function)完成, 該Thread也就結束,也無法重新工作

  • 每個Thread會分配到記憶體獨立的Stack區塊, 所以function的變數能有地方儲存

  • Thread如果參考到同一物件, 該物件的資料會共享. 如果用static的資料也一樣是共享

  • 但資料共享容易造成_Thread-Safe_的問題, 要特別處理, 像下面範例, 因為兩個取到done的值都是false, 所以都會執行

    using System;
    using System.Threading;
    
    class ThreadTestWithSharedData
    {
        private bool done = false;
        static void Main()
        {
            ThreadTestWithSharedData test = new ThreadTestWithSharedData();
            Thread t = new Thread(test.GoMaybeNotSafe);
            t.Start();
            test.GoMaybeNotSafe();
            Console.Read();
        }
    
        public void Go()
        {
            if(!done){
                done = true;
                Console.WriteLine("done");
            }
        }
    
        public void GoMaybeNotSafe()
        {
            if(!done){
                Console.WriteLine("done");
                done = true;
            }
        }
    }
  • 使用Exclusive lock, 只允許一個thread運算

  • 當Thread被Blocked, 不會消耗CPU資源

Join and Sleep

  • 使用Join可等待Thread完成

  • 使用Sleep讓當前Thread暫停指定的時間

  • 不管是Join或Sleep, 都是_Blocked_

  • Thread.Sleep(0) 將目前Thread的運算時間放其, 將CPU時間交給別的Thread, 等同功能是 Thread.Yield()

  • 用Sleep(0)或Yield, 可以用來找thread safety的問題, 假如把Yield填入程式任何地方且出現問題, 代表這程式碼有Bug

How Threading Works

  • 在CLR裡有個Thread Scheduler, 代表作業系統, 由它Thread的執行時間

  • 單一處理器的系統, 切的time slice時間比switch context的時間還長

  • 多處理器的系統, 切的time slice有concurrency, 可以同時執行多個thread

  • Thread如果被preempted(搶占), 代表它是被interrupted, 比如time-slicing

Threads vs Processes

  • 多個Thread可以執行在1個Process

  • Process之間是互相隔離

  • Thread之間互相分享Heap記憶體的資料

Threading's Uses and Misuses(誤用)

  • Maintaining a responsive user interface: 其他的Worker Thread可背後執行消耗的任務, 而Main(UI) Thread與User操作互動

  • Making efficient use of an otherwise blocked CPU:

  • Parallel programming: 在多核心/多處理器的環境, 多個執行緒能平行分擔工作

  • Speculative(投機性) execution: 有些任務可以用多個演算法同時運算, 最終結果取最快運算完的.

  • Allowing requests to be processed simultaneously: .NET的Server功能(WCF、ASP.NET等) 收到Request, 會自動建立多執行緒來處理. Client也是可同樣的作法.

  • 強調多執行緒之間共用資料時, 都會有Bug的產生. 建議把多執行緒的邏輯能封裝在獨立的library, 也比較好測試

  • 有些功能用太多執行緒不見得更快, 比如Disk IO, 只要幾個thread讀取 比 10幾個thread還快

Creating and Starting Threads

  • Thread建立時會帶入委託TheadStart, 但可以省略直接帶functionc或匿名function

Passing Data to a Thread

  • 可以在Thread.Start(someArgs)代入該function的參數

  • 也可以用ParameterizedThreadStart, 但是function的參數必須用object, 再另外轉型

Lambda expressions and captured variables: 傳參數要注意共用性的問題, 下面的輸出可能是0223557799, 而不是0~9各出現一次, 原因是有時多個Thread對i會存取到一樣的

    for (int i = 0; i < 10; i++)
      new Thread (() => Console.Write (i)).Start();

解決Captured variable的方法是指定變數:

    for (int i = 0; i < 10; i++)
    {
      int temp = i;
      new Thread (() => Console.Write (temp)).Start();
    }

Naming Threads

  • 可以指定Thread的名字, 比較容易做Debug

  • 用Thread.CurrentThread.Name = XXXX 指定名字

Foreground and Background Threads

  • Thread預設建立是Foreground, 代表它執行完才會讓App結束

  • 指定Thread.IsBackground = true, App終止時並不會理會Background的thread而強制終止

  • 如果在程式要結束且有finally的background thread, 這thread也會被忽略掉, 解決方法有2

  1. 用Join

  2. 如果是Pooled thread, 可用event wait handler

  • 如果有程式被任務管理員中止, 所有程式內的thread都會像background的直接中止

Thread Priority

  • Priority決定thread的執行時間長度

  • 小心使用Priority, 否則可能造成對其他thread取資源的starvation

  • 如果Process的Priority很低, 即使調高Thread的Priority也是會被限制資源

  • Process有個RealTime的Priority, 這會幾乎搶佔所有作業系統的資源, 小心使用, 一般用High就好

  • 如果要做RealTime的應用程式且包含使用者介面, 通常會拆開來, 使用者介面一個程式、後端運算是另一個程式, 彼此溝通用Remoting(WCF, Web Api之類)或memory-mapped files (C# in a Nutshell 有提到!! 沒用過~~)

Exception Handling

  • 建立Thread的try/catch/finally的scope, 無法捕捉到thread拋出的exception, 以下範例會直接拋出Exception, 而程式直接中止, 不會進到catch
    using System;
    using System.Threading;
    
    class ThreadThrowException
    {
        static void Main(string[] args){
            try{
                Thread t = new Thread(Go);
                t.Start();
            }
            catch(Exception ex){
                Console.WriteLine("Hi i am here" + ex.Message);
            }
            
            Console.Read();
        }
    
        static void Go()
        {
            throw new Exception("Null");
        }
    }
  • 將try/catch寫在被Thread執行的function
    using System;
    using System.Threading;
    
    class ThreadThrowException2
    {
        static void Main(string[] args){
            Thread t = new Thread(Go);
            t.Start();
            
            Console.Read();
        }
    
        static void Go()
        {
            try{
                throw new Exception("Null");
            }
            catch(Exception ex){
                Console.WriteLine("Hi i am here" + ex.Message);
            }
            
        }
    }
  • Global的異常事件處理(WPF和Winform的Application.DispatcherUnhandledException和 Application.ThreadException), 只有Main UI thread拋出的異常才會處理, 其他Worker thread的異常要自己處理

  • AppDomain.CurrentDomain.UnhandledException會被任何異常觸發, 但無法阻止後續程式的中止, 以下範例兩個exception都會被UnhandledException捕捉, 但程式仍直接中止

    using System;
    using System.Threading;
    
    class ThreadThrowExceptionWithAppDomainHandler
    {
        static void Main(string[] args){
            AppDomain currentDomain = AppDomain.CurrentDomain;
            currentDomain.UnhandledException += new UnhandledExceptionEventHandler(MyHandler);
            try{
                Thread t = new Thread(Go);
                t.Start();
            }
            catch(Exception ex){
                Console.WriteLine("Hi i am here" + ex.Message);
            }
            
            throw new Exception("TEST");
    
            Console.Read();
        }
    
        static void Go()
        {
            throw new Exception("Null");
        }
    
        static void MyHandler(object s, UnhandledExceptionEventArgs args)
        {
            Exception e = (Exception) args.ExceptionObject;
            Console.WriteLine("runtime terminating: {0} ", args.IsTerminating);
        }
    }

Threading Pool

  • 使用Threading Pool的4種方式
  1. Task Parallel Library

  2. ThreadPool.QueueUserWorkItem

  3. asynchronous delegates (BeginXXXXX...)

  4. BackgroundWorker

  • 以下是間接會用到Threading pool:
  1. WCF, Remoting, ASP.NET, ASMX Web service等的應用程式Server

  2. System.Timers.Timer和System.Threading.Timer

  3. Net有用Async結尾的函式, 比如WebClient(使用event-based asynchronous pattern)和BeginXXXX開頭的函式(asynchronous programming model pattern)

  4. PLINQ

  • 使用Threading Pool的注意事項
  1. 不能對Thread pool設定Name

  2. thread pool都是background thread

  3. block thread pool可能會造成一些潛在問題, 有一些優化的手法(比如ThreadPool.SetMinThreads)

  • Thread pool設過priority後, 任務執行完回收到pool會賦歸成normal priority

  • 可以用Thread.CurrentThread.IsThreadPoolThread 查看目前Thread是不是從pool來的

Entering the Thread Pool via TPL

  • 新的Task類別使用Thread pool更簡單

  • 非泛型的Task類別取代ThreadPool.QueueUserWorkItem

  • 泛型的Task類別取代asynchronous delegate (BeginXXXXX...)

  • 非泛型的Task類別用Task.Factory.StartNew

  • 會回傳一個Task物件, 可以用Wait()等待, 而Task指定的函式發生Exception時, 會捕捉到

  • 如果不對Task物件做Wait, 而中間發生的Exception會造成程式中止 ( 這個用Console程式無法成功, 主程式沒被中止)

  • Task的結果可用.Result取得該Task回傳的結果

  • 在Task取Result有Exception時, 會包裝在AggregateException, 沒處理的話會讓程式中止

Entering the Thread Pool Without TPL

  • ThreadPool.QueueUserWorkItem 和 asynchronous delegates都是不使用TPL而用Thread pool的方法, 差異在於asynchronous delegates可從thread回傳資料、回傳exception給caller

QueueUserWorkItem

  • 像是new Thread一樣, 代入void的function, 也能代入參數, 都包裝在object

  • 如果function有未處理的exception, 將造成程式中止

    using System;
    using System.Threading;
    
    class QueueUserWorkItem
    {
        static void Main(string[] args){
    
            ThreadPool.QueueUserWorkItem(Go);
            ThreadPool.QueueUserWorkItem(Go, 12345);
            Console.Read();
        }
    
        static void Go(object data)
        {
            Console.WriteLine("Hello " + data);
        }
    }

Asynchronous delegates

  • 能夠回傳值, 基於IAsyncResult

  • Asynchronous delegate和asynchronous methods不一樣, 有些函式庫也是用BeginXXX/EndXXX開頭

  • 使用Asynchronous delegates的流程:

  1. 建立要被委託的函式, 必需指定成Func類別

  2. 用Func的BeginInvoke呼叫該函式, 會回傳IAsyncResult

  3. 用Func的EndInvoke代入IAsyncResult變數, 將取得結果

    using System;
    using System.Threading;
    
    class AsynchronousDelegate
    {
        static void Main(string[] args){
            Func<string, int, string> task = Go;
            IAsyncResult cookie = task.BeginInvoke("test", 123, null, null);
            string result = task.EndInvoke(cookie);
            Console.WriteLine("Result is " + result);
            Console.Read();
        }
    
        static string Go(string name, int n)
        {
            return name + " and " + n.ToString();
        }
    }
  • EndInvoke會做3件事:
  1. 如果事情還未完成, 會等它完成

  2. 接收回傳值

  3. 將Exception拋回至Caller

  • 技術上來講, 如果函式沒有要回傳值, 可以不呼叫EndInvoke, 但內部造成的Exception要小心. 所以建議都呼叫EndInvoke

  • 另一種用法是把處理運算結果寫在另一個委託函式, 該函式接收IAsyncResult的參數. 而不是在Caller呼叫 EndInvoke

    using System;
    using System.Threading;
    
    class AsynchronousDelegate2
    {
        static void Main(string[] args){
            Func<string, int, string> task = Go;
            task.BeginInvoke("test", 123, Done, task);
            Console.Read();
        }
    
        static void Done(IAsyncResult cookie)
        {
            var target = (Func<string, int, string>) cookie.AsyncState;
            string result = target.EndInvoke(cookie);
            Console.WriteLine("Result is " + result);
        }
    
        static string Go(string name, int n)
        {
            return name + " and " + n.ToString();
        }
    }

Optimizing the Thread Pool

  • ThreadPool.SetMaxThreads可以設置Thread pool最多的Thread數量

  • 每個環境有預設的上限

  1. Framework 4.0 & 32-bit 可設1023個

  2. Framework 4.0 & 64-bit 可設32768個

  3. Framework 3.5 可設每個核心250個

  4. Framework 2.0 可設每個核心25個

  • ThreadPool.SetMinThreads能設置最小的Thread數量, 預設是每個core會有1個

  • SetMinThreads能優化的狀況是, 因為建立Thread會有延遲, 但如果SetMinThreads指定X個, 這X個Thread不要有延遲.

參考資料

  1. Threading in C#, PART 1: GETTING STARTED, Joseph Albahari.
  2. C# 8.0 in a Nutshell: The Definitive Reference, Joseph Albahari (Amazon)

圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言