c# 多線程——入門學習


1. 概念介紹

1.1 線程

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

1.2 主線程

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

1.3 前台線程

  默認情況下,Thread.Start()方法創建的線程都是前台線程,屬性isBackground=true/false能夠設置線程的線程是否為后台線程。前台線程能阻止應用程序的終結,只有所有的前台線程執行完畢,CLR(Common Language Runtime,公共語言運行庫)才能關閉應用程序。前台線程屬於工作者線程。

1.4 后台線程

  后台線程通過isBackground設置,它不會影響應用程序的終結,當所有前台線程執行完畢后,后台線程無論是否執行完畢,都會被終結。一般后台線程用來做無關緊要的任務(如郵箱天氣更新等),后台線程也屬於工作者線程。

2.多線程實現

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標記的方法中的異步任務時,才會掛起。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM