C# 多線程編程第一步——理解多線程

来源:http://www.cnblogs.com/Brambling/archive/2017/07/10/7144015.html
-Advertisement-
Play Games

一、進程、線程及多線程的概念 什麼是多線程呢?不理解。 那什麼是線程呢?說到線程就不得不說說進程。我在網上搜索也搜索了一些資料,大部分所說的進程其實是很抽象的東西。通俗的來講,進程就是一個應用程式開始運行,那麼這個應用程式就會存在一個屬於這個應用程式的進程。 那麼線程就是進程中的基本執行單元,每個進 ...


一、進程、線程及多線程的概念

什麼是多線程呢?不理解。

那什麼是線程呢?說到線程就不得不說說進程。我在網上搜索也搜索了一些資料,大部分所說的進程其實是很抽象的東西。通俗的來講,進程就是一個應用程式開始運行,那麼這個應用程式就會存在一個屬於這個應用程式的進程。

那麼線程就是進程中的基本執行單元,每個進程中都至少存在著一個線程,這個線程是根據進程創建而創建的,所以這個線程我們稱之為主線程。那麼多線程就是包含有除了主線程之外的其他線程。如果一個線程可以執行一個任務,那麼多線程就是可以同時執行多個任務。

以上的概念純屬個人理解,如有什麼不對的地方,還請多多指正。

 

二、線程的基本知識

Thread 類

Thread 類是用於控制線程的基礎類,它存在於 System.Threading 命名空間。通過 Thread 可以控制當前應用程式域中線程的創建、掛起、停止、銷毀。

Thread 一些常用屬性:

Thread 一些常用方法:

Thread 的優先順序:

 

三、多線程的簡單示例

下麵就從簡單的多線程開始理解吧,這裡我創建了一個控制台應用程式。

   class Program
    {
        static void Main(string[] args)
        {
            ThreadDemoClass demoClass = new ThreadDemoClass();

            //創建一個新的線程
            Thread thread = new Thread(demoClass.Run);

            //設置為後臺線程
            thread.IsBackground = true;

            //開始線程
            thread.Start();

            Console.WriteLine("Main thread working...");
            Console.WriteLine("Main thread ID is:" + Thread.CurrentThread.ManagedThreadId.ToString());

            Console.ReadKey();
        }
    }

    public class ThreadDemoClass
    {
        public void Run()
        {
            Console.WriteLine("Child thread working...");
            Console.WriteLine("Child thread ID is:" + Thread.CurrentThread.ManagedThreadId.ToString());
        }
    }

創建一個新的線程還可以使用 ThreadStart 委托的方式。如下:

//創建一個委托,並把要執行的方法作為參數傳遞給這個委托
ThreadStart threadStart = new ThreadStart(demoClass.Run);
Thread thread = new Thread(threadStart);

執行結果:

根據以上的結果我們可以分析得到,主線程創建了一個子線程並啟動了它,但是主線程沒有等到子線程執行完成,而是繼續再往下執行的。

這就涉及到了線程非同步或同步的問題了,這個我們後面再說。

繼續上面的問題,那麼如果我想要等到子線程執行完成之後再繼續主線程的工作呢(當然,我覺得一般不會有這種需求)。

我們可以使用 Join() 這個方法,修改之後的代碼:

   class Program
    {
        static void Main(string[] args)
        {
            ThreadDemoClass demoClass = new ThreadDemoClass();

            //創建一個新的線程
            Thread thread = new Thread(demoClass.Run);

            //設置為後臺線程
            thread.IsBackground = true;

            //開始線程
            thread.Start();

            //等待直到線程完成
            thread.Join();

            Console.WriteLine("Main thread working...");
            Console.WriteLine("Main thread ID is:" + Thread.CurrentThread.ManagedThreadId.ToString());

            Console.ReadKey();
        }
    }

    public class ThreadDemoClass
    {
        public void Run()
        {
            Console.WriteLine("Child thread working...");
            Console.WriteLine("Child thread ID is:" + Thread.CurrentThread.ManagedThreadId.ToString());
        }
    }

執行結果:

上面的代碼相比之前就添加了一句 thread.Join(),它的作用就是用於阻塞後面的線程,直到當前線程完成之後。當然,還有其他的方法可以做到,比如我們現在把 thread.Join() 換成

下麵這句代碼。

//掛起當前線程指定的時間
Thread.Sleep(100);

就當前的場景來說,這樣的確可以滿足需求,但是這樣做有一個弊端,就是,當子線程所執行的方法邏輯比較複雜耗時較長的時候,這樣的方式就不一定可以,雖然可以修改線程掛起的時間,但是這個執行的時間卻是不定的。所以,Thread.Sleep() 方法一般用來設置多線程之間執行的間隔時間的。

另外,Join() 方法也接受一個參數,該參數用於指定阻塞線程的時間,如果在指定的時間內該線程沒有終止,那麼就返回 false,如果在指定的時間內已終止,那麼就返回 true。

 

上面的這種使用多線程的方式只是簡單的輸出一段內容而已,多數情況下我們需要對線程調用的方法傳入參數和接收返回值的,但是上面這種方法是不接受參數並且沒有返回值的,那麼我們可以使用 ParameterizedThreadStart 委托來創建多線程,這個委托可以接受一個 object 類型的參數,我們可以在這上面做文章。

   class Program
    {static void Main(string[] args)
        {
            ThreadDemoClass demoClass = new ThreadDemoClass();

            //創建一個委托,並把要執行的方法作為參數傳遞給這個委托
            ParameterizedThreadStart threadStart = new ParameterizedThreadStart(demoClass.Run);
            
            //創建一個新的線程
            Thread thread = new Thread(threadStart);

            //開始線程,並傳入參數
            thread.Start("Brambling");

            Console.WriteLine("Main thread working...");
            Console.WriteLine("Main thread ID is:" + Thread.CurrentThread.ManagedThreadId.ToString());
            Console.ReadKey();
        }
    }

    public class ThreadDemoClass
    {
        public void Run(object obj)
        {
            string name = obj as string;

            Console.WriteLine("Child thread working...");
            Console.WriteLine("My name is " + name);
            Console.WriteLine("Child thread ID is:" + Thread.CurrentThread.ManagedThreadId.ToString());
        }
    }

執行結果:

PS:這裡我沒有加這句代碼了(thread.IsBackground = true,即把當前線程設置為後臺線程),因為使用 thread.Start() 啟動的線程預設為前臺線程。那麼前臺線程和後臺線程有什麼區別呢?

前臺線程就是系統會等待所有的前臺線程運行結束後,應用程式域才會自動卸載。而設置為後臺線程之後,應用程式域會在主線程執行完成時被卸載,而不會等待非同步線程的執行完成。

那麼上面的結果可以看到在多線程實現了參數的傳遞,可是它也只有一個參數呢。但是它接受的參數是 object 類型的(萬類之源),也就是說既可以是值類型或引用類型,也可以是自定義類型。(當然,自定義類型其實也是屬於引用類型的)下麵我們使用自定義類型作為參數傳遞。

   class Program
    {
        static void Main(string[] args)
        {
            ThreadDemoClass demoClass = new ThreadDemoClass();

            //創建一個委托,並把要執行的方法作為參數傳遞給這個委托
            ParameterizedThreadStart threadStart = new ParameterizedThreadStart(demoClass.Run);

            //創建一個新的線程
            Thread thread = new Thread(threadStart);

            UserInfo userInfo = new UserInfo();
            userInfo.Name = "Brambling";
            userInfo.Age = 333;

            //開始線程,並傳入參數
            thread.Start(userInfo);

            Console.WriteLine("Main thread working...");
            Console.WriteLine("Main thread ID is:" + Thread.CurrentThread.ManagedThreadId.ToString());
            Console.ReadKey();
        }
    }

    public class ThreadDemoClass
    {
        public void Run(object obj)
        {
            UserInfo userInfo = (UserInfo)obj;

            Console.WriteLine("Child thread working...");
            Console.WriteLine("My name is " + userInfo.Name);
            Console.WriteLine("I'm " + userInfo.Age + " years old this year");
            Console.WriteLine("Child thread ID is:" + Thread.CurrentThread.ManagedThreadId.ToString());
        }
    }

    public class UserInfo
    {
        public string Name { get; set; }

        public int Age { get; set; }
    }

執行結果:

使用自定義類型作為參數傳遞,理論上更多個參數也都是可以實現的。

 

四、線程池

使用 ThreadStart 和 ParameterizedThreadStart 創建線程還是比較簡單的,但是由於線程的創建和銷毀需要耗費一定的開銷,過多的使用線程反而會造成記憶體資源的浪費,從而影響性能,出於對性能的考慮,於是引入了線程池的概念。線程池並不是在 CLR 初始化的時候立刻創建線程的,而是在應用程式要創建線程來執行任務的時候,線程池才會初始化一個線程,初始化的線程和其他線程一樣,但是線上程完成任務之後不會自行銷毀,而是以掛起的狀態回到線程池。當應用程式再次向現成池發出請求的時候,線程池裡掛起的線程會再度激活執行任務。這樣做可以減少線程創建和銷毀所帶來的開銷。線程池建立的線程預設為後臺線程

   class Program
    {
        static void Main(string[] args)
        {
            ThreadDemoClass demoClass = new ThreadDemoClass();

            //設置當沒有請求時線程池維護的空閑線程數
            //第一個參數為輔助線程數
            //第二個參數為非同步 I/O 線程數
            ThreadPool.SetMinThreads(5, 5);

            //設置同時處於活動狀態的線程池的線程數,所有大於次數目的請求將保持排隊狀態,直到線程池變為可用
            //第一個參數為輔助線程數
            //第二個參數為非同步 I/O 線程數
            ThreadPool.SetMaxThreads(100, 100);

            //使用委托綁定線程池要執行的方法(無參數)
            WaitCallback waitCallback1 = new WaitCallback(demoClass.Run1);
            //將方法排入隊列,線上程池變為可用時執行
            ThreadPool.QueueUserWorkItem(waitCallback1);


            //使用委托綁定線程池要執行的方法(有參數)
            WaitCallback waitCallback2 = new WaitCallback(demoClass.Run1);
            //將方法排入隊列,線上程池變為可用時執行
            ThreadPool.QueueUserWorkItem(waitCallback2,"Brambling");


            UserInfo userInfo = new UserInfo();
            userInfo.Name = "Brambling";
            userInfo.Age = 33;

            //使用委托綁定線程池要執行的方法(有參數,自定義類型的參數)
            WaitCallback waitCallback3 = new WaitCallback(demoClass.Run2);
            //將方法排入隊列,線上程池變為可用時執行
            ThreadPool.QueueUserWorkItem(waitCallback3, userInfo);

            Console.WriteLine();
            Console.WriteLine("Main thread working...");
            Console.WriteLine("Main thread ID is:" + Thread.CurrentThread.ManagedThreadId.ToString());
            Console.ReadKey();
        }
    }

    public class ThreadDemoClass
    {
        public void Run1(object obj)
        {
            string name = obj as string;

            Console.WriteLine();
            Console.WriteLine("Child thread working...");
            Console.WriteLine("My name is " + name);
            Console.WriteLine("Child thread ID is:" + Thread.CurrentThread.ManagedThreadId.ToString());
        }

        public void Run2(object obj)
        {
            UserInfo userInfo=(UserInfo)obj;

            Console.WriteLine();
            Console.WriteLine("Child thread working...");
            Console.WriteLine("My name is " + userInfo.Name);
            Console.WriteLine("I'm " + userInfo.Age + " years old this year");
            Console.WriteLine("Child thread ID is:" + Thread.CurrentThread.ManagedThreadId.ToString());
        }
    }

    public class UserInfo
    {
        public string Name { get; set; }

        public int Age { get; set; }
    }

執行結果:

使用線程池建立的線程也可以選擇傳遞參數或不傳遞參數,並且參數也可以是值類型或引用類型(包括自定義類型)。看上面的結果發現了什麼?沒錯,第一次執行的方法的線程ID為6,最後一次執行的方法的線程ID也為6。這就說明第一次請求線程池的時候,線程池建立了一個線程,當它執行完成之後就以掛起狀態回到了線程池,在最後一次請求的時候,再次喚醒了該線程執行任務。這樣就很容易理解了。

在這裡我還發現了一個問題,就是,每次運行的時候,輸出的內容的順序都不一定是一樣的。(不只是線程池,前面的也是)因為,我沒有給任何線程設置優先順序(線程池不能設置線程的優先順序),這裡其實就涉及到線程安全的問題了,很明顯現在這樣是非線程安全的。讓我舉個慄子形容一下的話,就像以前在學校下課了去吃飯一樣,一擁而上,毫無秩序。

線程安全就先不說,留在後面再說(包括前面所提到的線程同步的問題),這篇博客旨在理解多線程。因為我也沒有太深的理解。。。

上面我們已經實現了無參數和有參數以及自定義參數多線程的實例,可是還有一個共同的問題那就是都沒有返回值。當我們用多線程做實際開發的時候大部分都是會需要返回值的,那麼我們可以使用成員變數來試試。

   class Program
    {
        List<UserInfo> userInfoList = new List<UserInfo>();

        static void Main(string[] args)
        {
            Program program = new Program();

            ParameterizedThreadStart threadStart = new ParameterizedThreadStart(program.Run);
            Thread thread = null;
            UserInfo userInfo = null;


            for (int i = 0; i < 3; i++)
            {
                userInfo = new UserInfo();
                userInfo.Name = "Brambling" + i.ToString();
                userInfo.Age = 33 + i;

                thread = new Thread(threadStart);
                thread.Start(userInfo);
                thread.Join();
            }

            foreach (UserInfo user in program.userInfoList)
            {
                Console.WriteLine("My name is " + user.Name);
                Console.WriteLine("I'm " + user.Age + " years old this year");
                Console.WriteLine("Thread ID is:" + user.ThreadId);
            }
            
            Console.ReadKey();
        }

        public void Run(object obj)
        {
            UserInfo userInfo = (UserInfo)obj;

            userInfo.ThreadId = Thread.CurrentThread.ManagedThreadId;
            userInfoList.Add(userInfo);
        }
    }

執行結果:

用上面這種方法勉強可以滿足返回值的需求,但是卻有很大的局限性,因為這裡我使用的是成員變數,所以也就限制了線程調用的方法必須是在同一個類裡面。

所以也就有了下麵的方法,使用委托非同步調用的方法。

 

五、委托

委托的非同步調用有兩個比較重要的方法:BeginInvoke() 和 EndInvoke()

 

未完待續。。。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 前 言 前端 AngularJS是為了剋服HTML在構建應用上的不足而設計的。(引用百度百科) AngularJS使用了不同的方法,它嘗試去補足HTML本身在構建應用方面的缺陷。AngularJS通過使用我們稱為指令(directives)的結構,讓瀏覽器能夠識別新的語法。(引用百度百科) 例如: ...
  • 這個vue實現備忘錄的功能demo是K在github上找到的,K覺得這是一個用來對vue.js入門的一個非常簡單的demo,所以拿在這裡共用一下。 (尊重他人勞動成果,從小事做起~ demo原github地址:https://github.com/vuejs/vue) 一、實現效果 二、代碼展示 ...
  • 今天編程時,JavaScript 程式報了這樣的錯誤:Cannot use 'in' operator to search for...,具體錯誤信息如下: 坦白說,這樣的錯誤最難調試。因為它並不指向你所寫的具體代碼,而是泛泛指向了 lib.js 文件(該文件通常是第三方的打包壓縮庫),你幾乎無法依 ...
  • 一、http方法 二、http常用狀態碼 1. 100~199信息狀態碼 2. 200~299成功狀態碼 3. 300 ~ 399重定向狀態碼 4. 400~499錯誤狀態碼 5. 500~599狀態碼 ...
  • 剛開始做NDK 開發的時候,Android Studio 還沒提供了 native C/C++ 設置斷點 調試,我們都是通過輸出 日誌來調試,這樣費時耗力。Android Studio 應該是在 2.2 版本才提供的設置斷點 debug 功能,同時在該版本也提供了 cmake 編譯。 我目前在做 N ...
  • 傳統機器學習依賴良好的特征工程。深度學習解決有效特征難人工提取問題。無監督學習,不需要標註數據,學習數據內容組織形式,提取頻繁出現特征,逐層抽象,從簡單到複雜,從微觀到巨集觀。 稀疏編碼(Sparse Coding),基本結構組合。自編碼器(AutoEncoder),用自身高階特征編碼自己。期望輸入/ ...
  • 換了四種黑蘋果,最終成功了 步驟: 1、升級vs2017, 2、安裝XCODE 8.3 3、安裝vs2017 for mac 企業版 4、啟動vs2017 for mac ,設置xcode 位置 5、打開遠程登錄與屏幕共用 6、打開WINDOWS中的VS2017,在 選項中設置XCODE位置,使用I... ...
  • 詮釋: 1. 破解VIP登陸限制 2.去後門 (自查) 下載地址 :https://pan.baidu.com/s/1eR2rUOM 查毒地址:http://a.virscan.org/a3983f36d31d08a51486501965d04cb5 Xise_V20.0.exe 更新日誌 生成內頁 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...