c# 多線程——入門學習

1. 概念介紹web

1.1 線程編程

  線程是操做系統可以進行運算調度的最小單位,包含在進程之中,是進程中的實際運做單位。一條線程指的時進程中一個單一順序的控制流,一個進程中能夠併發多個線程,每條線程並行執行不一樣的任務。.NET 中System.Thread下能夠建立線程。c#

1.2 主線程windows

  每一個windows進程都包含一個用作程序入口點的主線程。進程入口點(main方法)中建立的第一個線程稱爲主線程,調用main方法時,主線程被建立。 api

1.3 前臺線程安全

  默認狀況下,Thread.Start()方法建立的線程都是前臺線程,屬性isBackground=true/false可以設置線程的線程是否爲後臺線程。前臺線程能阻止應用程序的終結,只有全部的前臺線程執行完畢,CLR(Common Language Runtime,公共語言運行庫)才能關閉應用程序。前臺線程屬於工做者線程。多線程

1.4 後臺線程併發

  後臺線程經過isBackground設置,它不會影響應用程序的終結,當全部前臺線程執行完畢後,後臺線程不管是否執行完畢,都會被終結。通常後臺線程用來作可有可無的任務(如郵箱天氣更新等),後臺線程也屬於工做者線程。異步

2.多線程實現async

2.1 建立線程

  在VS2019中,創建一個控制檯應用程序,測試多線程服務。首先開啓2個線程workThread、printThread,分別實現數字計數、打印字母。代碼實現以下:

   class Program
    {
        static void Main(string[] args)
        {
            //新建兩個線程,單獨運行
            Thread workThread=new Thread(NumberCount);
            Thread printThread=new  Thread(printNumber);
            workThread.Start();
            printThread.Start();
            Console.WriteLine("Hello World!");
        }

        public static void NumberCount()
        {
            for (int i = 0; i < 10; i++)
            {
                Console.WriteLine("the number is {0}",i);
            }
        }

        public static void printNumber()
        {
            for (char i = 'A'; i < 'J'; i++)
            {

                Console.WriteLine("print character {0}", i);
            }
        }
    }

運行結果以下:

   根據上述運行結果能夠看出,主線程workThread和其餘線程printThread運行時相互獨立,互不干擾。

2.2  線程基本屬性瞭解

    static void Main(string[] args)
        {
            Thread th = Thread.CurrentThread;//訪問當前正在運行的線程
            bool aliveRes=th.IsAlive;//當前線程的執行狀態
            Console.WriteLine("IsAlive= {0}", aliveRes);
            th.IsBackground =false;//線程是否爲後臺線程
            Console.WriteLine("IsBackground= {0}", th.IsBackground);
            bool isPool= th.IsThreadPoolThread;//當前線程是否屬於託管線程池
            Console.WriteLine("isPool= {0}", isPool);
            int sysbol = th.ManagedThreadId;//獲取當前託管線程的惟一標識
            Console.WriteLine("ManagedThreadId= {0}", sysbol);
            ThreadPriority pry=th.Priority;//設置線程調度優先級
            Console.WriteLine("pry= {0}", pry);
            ThreadState state=th.ThreadState;//獲取當前線程狀態值
            Console.WriteLine("state= {0}", state);
            th.Name = "main thread";
            Console.WriteLine("this is {0}",th.Name);
            Console.ReadKey();
            Console.WriteLine("Hello World!");
        }

2.3 暫停線程

  暫停線程經過調用sleep()方法實現,使得線程暫停但不佔用計算機資源,實現代碼以下:

    static void NumberCountCouldDelay()
        {
            for (int i = 0; i < 10; i++)
            {
                Console.WriteLine("the number is {0}", i);
                Thread.Sleep(TimeSpan.FromSeconds(1));
            }
        }
        public static void printNumber()
        {
            for (char i = 'A'; i < 'J'; i++)
            {
                Console.WriteLine("print character {0}", i);
                Thread.Sleep(TimeSpan.FromSeconds(1));
            }
        }

運行結果以下:

2.4 線程池

  線程池是一種多線程處理形式,將任務添加到隊列,而後再建立線程後自動啓動這些任務。經過線程池建立的任務屬於後臺任務,每一個線程使用默認的堆棧大小,以默認的優先級運行,並處於多線程單元中。若是某個線程在託管代碼中空閒(如正在等待某個事件),則線程池將插入另外一個輔助線程來使全部的處理器保持繁忙。

實現代碼及運行結果以下:

 
    static void Main(string[] args)
        {
            Console.WriteLine("this is main thread: ThreadId={0}", Thread.CurrentThread.ManagedThreadId);
            ThreadPool.QueueUserWorkItem(printNumber);
            ThreadPool.QueueUserWorkItem(Go);
            Console.Read();
        }
       
        public static void printNumber(object data)
        {
            for (char i = 'A'; i < 'D'; i++)
            {
                Console.WriteLine("print character {0}", i);
                Console.WriteLine("the print process threadId is {0}", Thread.CurrentThread.ManagedThreadId);
            }
        }
        public static void Go(object data)
        {
            Console.Write("this is another thread:ThreadId={0}",Thread.CurrentThread.ManagedThreadId);
        }

2.5 停止線程

  線程停止採用abort方法,實現以下:

static void Main(string[] args)
        {
            ThreadStart childref = new ThreadStart(CallToChildThread);
            Console.WriteLine("In Main: Creating the child thread");
            Thread childThread = new Thread(childref);//建立線程,擴展的Thread類
            childThread.Start();//調用start()方法開始子線程的執行
            //中止主線程一段時間
            Thread.Sleep(2000);
            //如今停止子線程
            Console.WriteLine("In Main: Abort the child thread");
            childThread.Abort();
            Console.WriteLine("Hello World!");
        }
        public static void CallToChildThread()
        {
            try
            {
                //調用abort()方法銷燬線程
                Console.WriteLine("Child thread start");
                for (int counter=0; counter<=10;counter++)
                {
                    Thread.Sleep(500);
                    Console.WriteLine(counter);
                }
                Console.WriteLine("child thread abort");
            }
            catch (ThreadAbortException e)
            {
                Console.WriteLine(e);
                throw;
            }
            finally
            {
                Console.WriteLine("Couldn't catch the Thread Exception");
            }
        }

  運行程序,出現以下錯誤:

  經查找,發現.NET CORE平臺不支持線程停止,在調用abort方法時會拋出ThreadAbortException異常。

2.5 跨線程訪問

  新建一個winform窗體應用程序,實現點擊按鈕爲textbox賦值,代碼以下:

 private void Button1_Click(object sender, EventArgs e)
        {
            Thread thread=new Thread(test);
            thread.IsBackground = true;
            thread.Start();
            Console.ReadLine();
        }

        private void test()
        {
            for (int i = 0; i < 10; i++)
            {
                this.textBox1.Text = i.ToString();
            }
        }

  然而,運行時出現如下錯誤,內容顯示「線程間操做無效:從不是建立控件textBox1的線程訪問它」。是由於控件textBox1是由主線程建立的,thread做爲另一個線程,在.NET上執行的是託管代碼,c#強制要求代碼線程安全,不容許跨線程訪問。

  上述問題解決辦法以下:(參考https://docs.microsoft.com/en-us/dotnet/framework/winforms/controls/how-to-make-thread-safe-calls-to-windows-forms-controls)

   利用委託實現回調機制,回調過程以下:

  (1)定義並聲明委託;

  (2)初始化回調方法;

  (3)定義回調使用的方法

 public partial class UserControl1: UserControl
    {
        private delegate void SetTextboxCallBack(int value);//定義委託

        private SetTextboxCallBack setCallBack;

        /// <summary>
        ///定義回調使用的方法
        /// </summary>
        /// <param name="value"></param>
        private void SetText(int value)
        {
            textBox1.Text = value.ToString();
        }
       public UserControl1()
        {
            InitializeComponent();
        }
        private void Button1_Click(object sender, EventArgs e)
        {
            //初始化回調函數
            setCallBack=new SetTextboxCallBack(SetText);
            //建立一個線程去執行這個回調函數要操做的方法
            Thread thread = new Thread(test);
            thread.IsBackground = true;
            thread.Start();
            Console.ReadLine();
        }
        public void test()
        {
            for (int i = 0; i < 10; i++)
            {
                //控件上執行回調方法,觸發操做
                textBox1.Invoke(setCallBack,i);
            }
        }
  }        

運行結果以下:

 

2.5 多線程使用委託

  線程的建立經過new Thread來實現,c#中該構造函數的實現有如下4種:

  • public Thread(ThreadStart start){}
  • public Thread(ParameterizedThreadStart start){}
  • public Thread(ThreadStart start, int maxStackSize){}
  • public Thread(ParameterizedThreadStart start, int maxStackSize){}

其中,參數ThreadStart定義爲:

public delegate void ThreadStart();//無參數無返回值的委託

參數ParameterizedThreadStart 定義爲:

public delegate void ParameterizedThreadStart(object obj);//有參數無返回值的委託

所以,對無返回值的委託實現以下。

2.5.1 無參數無返回值的委託

  對於無參數無返回值的委託,是最簡單原始的使用方法。Thread thread= new Thread(new ThreadStart(()=>參數),其中參數爲ThreadStart類型的委託。此類多線程代碼實現以下:

class Program
    {
        public delegate void ThreadStart();//新建一個無參數、無返回值的委託
        static void Main(string[] args)
        {
            Thread thread=new Thread(new System.Threading.ThreadStart(NumberCount));
            thread.IsBackground = true;
            thread.Start();
            for (char i = 'A'; i < 'D'; i++)
            {
                Console.WriteLine("print character {0},the threadId id ={1}", i, Thread.CurrentThread.ManagedThreadId);
            }
            Console.WriteLine("Hello World!");
        }

        public static void NumberCount()
        {
            for (int i = 0; i < 3; i++)
            {
                Console.WriteLine("the number is {0},the threadId id ={1}", i, Thread.CurrentThread.ManagedThreadId);
            }
        }
    }

2.5.2 有參數無返回值的委託

  對於有參數無返回值的委託,實現代碼以下:

class Program
    {
        public delegate void ThreadStart(int i);//新建一個無參數、無返回值的委託
        static void Main(string[] args)
        {
            Thread thread=new Thread(new ParameterizedThreadStart(NumberCount));
            thread.IsBackground = true;
            thread.Start(3);
            for (char i = 'A'; i < 'D'; i++)
            {
                Console.WriteLine("print character {0},the threadId id ={1}", i, Thread.CurrentThread.ManagedThreadId);
            }
            Console.WriteLine("Hello World!");
        }

        public static void NumberCount(object i)
        {
           Console.WriteLine("the number is {0},the threadId id ={1}", i, Thread.CurrentThread.ManagedThreadId);
        }

    }

運行結果爲:

2.5.2 有參數有返回值的委託

   對於有參數有返回值的委託,採用異步調用實現,以下所示:

2.6 異步實現

2.6.1 Task.Result

   ..NET中引入了System.Threading.Tasks,簡化了異步編程的方式,而不用直接和線程、線程池打交道。 System.Threading.Tasks中的類型被稱爲任務並行庫(TPL),TPL使用CLR線程池(TPL建立的線程都是後臺線程)自動將應用程序的工做動態分配到可用的CPU的中。

  Result方法能夠返回Task執行後的結果。可是在.NET CORE的webapi中使用result方法來獲取task的輸出值,會形成當前API線程阻塞等待到task執行完成後再繼續。如下代碼中,get方法中的線程id-57,調用一個新線程執行task後,等待TaskCaller()執行結果(threadid=59),待TaskCaller()方法執行完成後,原來的線程繼續以後以後的語句,輸出threadid=57

  public class ValuesController:Controller
    {
        //async/await是用來進行異步調用的形式,
        [HttpGet("get")]
        public async Task<string> Get()
        {
            var info = string.Format("api執行線程{0}",Thread.CurrentThread.ManagedThreadId);//get方法中的線程
            //調用新線程執行task任務
            var infoTask = TaskCaller().Result;//調用result方法獲取task的值
            var infoTaskFinished = string.Format("api執行線程(taks調用completed){0}", Thread.CurrentThread.ManagedThreadId);
            return string.Format("{0},{1},{2}", info, infoTask, infoTaskFinished);
        }
        public async Task<string> TaskCaller()
        {
            await Task.Delay(5000);
            return string.Format("task 執行線程{0}", Thread.CurrentThread.ManagedThreadId);
        }
    }

運行結果以下:

 2.6.2 Async&Await

  c#中async關鍵字用來指定方法,Lambda表達式或匿名方法自動以異步的方式來調用。async/await是用來進行異步調用的形式,內部採用線程池進行管理。若是使用await,在調用await tasjCall()是不會阻塞get方法的主線程,主線程會被釋放,新的線程執行完task後繼續執行await後的代碼,從而減小了線程切換的開銷,而以前的線程則空閒了。

 public class ValuesAwaitController : Controller
    {
        [HttpGet("get")]
        public async Task<string> Get()
        {
            var info = string.Format("api執行線程{0}",Thread.CurrentThread.ManagedThreadId);//get方法中的線程
            //調用新線程執行task任務
            var infoTask = await TaskCaller();//使用await調用不會阻塞Get()中線程
            var infoTaskFinished = string.Format("api執行線程(taks調用completed){0}", Thread.CurrentThread.ManagedThreadId);
            return string.Format("{0},{1},{2}", info, infoTask, infoTaskFinished);
        }
        public async Task<string> TaskCaller()
        {
            await Task.Delay(5000);
            return string.Format("task 執行線程{0}", Thread.CurrentThread.ManagedThreadId);
        }
    }

運行結果以下:

 

   Task.result與await關鍵字具備相似的功能能夠獲取到任務的返回值,但本質上Task.result會讓外層函數執行線程阻塞知道任務完成,而使用await外層函數線程不會阻塞,而是經過任務執行線程來執行await後的代碼。

  • 默認建立的Thread是前臺線程,建立的Task爲後臺線程;
  • ThreadPool建立的線程都是後臺線程;
  • 任務並行庫(TPL)使用的是線程池計數;
  • 調用async標記的方法,剛開始是同步執行,只有當執行到await標記的方法中的異步任務時,纔會掛起。
相關文章
相關標籤/搜索