不一樣的角度一窺多線程


不一樣的角度一窺多線程

最近在性能調試時,發現了一個有趣的現象,我把代碼簡化后如下.

    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Start...");
            DoSomething();
            Console.WriteLine("Ending...");
            Console.ReadLine();
        }

        static void DoSomething()
        {
            var sum="";
            for (int i = 2; i < int.MaxValue; i++)
            {
                sum += "s";
            }
            Console.WriteLine(sum.Length);
        }
    }

可以看到,非常簡單的一段代碼,當我用Windows的性能監測工具來監測每個處理器的使用率時,發現了一個有趣的現象.

我電腦是四核的I7處理器,執行以上代碼后,卻只有處理器2一直處理一個比較高的占用率,而其他的三個則處於一個"摸魚混日子"的狀態,處理器1則更過分,你是睡着了嗎?

同一台電腦上的處理器,難道大家不是有福同享,有難同當的嗎? 為什么其他幾個處理器就忍心看着處理器2水深火熱呢?

然后,我就和這個問題死磕上了,惡補了一些操作系統與多線程的知識,現在把一寫知識點串起來,分享給大家.

分級保護域

電腦操作系統提供不同的資源訪問級別。在計算機體系結構中,Rings是由兩個或更多的特權態組成。在一些硬件或者微代碼級別上提供不同特權態模式的CPU架構上,保護環通常都是硬件強制的。Rings是從最高特權級(通常被叫作0級)到最低特權級(通常對應最大的數字)排列的。在大多數操作系統中,Ring 0擁有最高特權,並且可以和最多的硬件直接交互(比如CPU,內存)。在Windows中, User Space,也就是我們自己安裝的那些應用程序處理Ring 3,而系統內核就在Ring 0.

對於這個問題,舉個例子,大家就好理解了.
錢不是萬能的,但沒錢是萬萬不能的,所以錢是一個家庭的重中之重,家里老婆呢為了這個家的長治久安,掌握家里的財政大權,把家里的小金庫守得死死的,但這就意味着我沒錢花了嗎?當然不是,和老婆大人用正當理由申請不就完事了?😂
申請通過之后,老婆大人是允許我直接伸手去家里小金庫拿錢嗎? 那當然不是,如果我一抓一大把就危險了,所以還得經過她的手從小金庫拿錢給我.
這個現象,我覺得也是一種分級保護域,所以呢,也一直對老婆大人的這種萬惡行徑表示理解.

操作系統也是這樣,CPU,內存這些硬件是電腦安全的根本,所以不能給第三方軟件操作權限,想操作硬件,就通過由Ring 0中內核(Kernel)暴露的嚴格Api進行.

用戶級線程與內核級線程

線程主要有以下兩種實現方式-

  • 用戶級線程 -用戶托管線程。
  • 內核級線程 -作用在內核(操作系統核心)上的操作系統管理的線程。

在上圖中,User Space就可以理解為我上個章節中的Ring 3,而Kernel Space就是Ring 0, 在Ring 0中,是可以直接操作CPU,內存等硬件的,而Ring 3不行.

以下是用戶級線程與內核級線程的對比.

用戶級線程 內核級線程
用戶線程由用戶實現。 內核線程由OS實現。
操作系統無法識別用戶級線程。 內核線程被操作系統識別。
用戶線程的實現很容易。 內核線程的實現很復雜。
上下文切換時間更少。 上下文切換時間更長。
上下文切換不需要硬件支持。 需要硬件支持。
如果一個用戶級別的線程執行阻止操作,則整個過程將被阻止。 如果一個內核線程執行阻止操作,則另一線程可以繼續執行。
無法直接發揮多核處理器的優勢 可以享受多處理起帶來的好處

其中,非常重要的一點,用戶級線程無法直接發揮多核處理器的優勢,難道我們編寫出來的代碼只能在一個處理器上運行了嗎?這就要講講用戶級線程模型.

用戶級線程模型

通常,內核級線程可以使用三個模型之一來執行用戶級線程。

  • Many-to-one
  • One-to-one
  • Many-to-many

所有模型都將用戶級線程映射到內核級線程,一個內核線程就像一個處理器,它是系統編排任務的基本單位。

Many-to-one

多對一模型將許多用戶級線程映射到一個內核級線程。線程管理是通過線程庫在用戶空間中完成的。當線程進行阻塞的系統調用時,整個過程將被阻塞。一次只能有一個線程訪問內核,因此多個線程無法在多處理器上並行運行。
如果用戶級線程庫是以操作系統不支持的方式實現的,則內核線程將使用多對一關系模型。

內核對用戶級線程不可見,在它眼里只有內核線程,而在內核線程的眼里,一個進程無非就是一個偶爾被被它翻牌的黑盒子,進程負責用戶線程的調度與執行.

One-to-one

在這種模型下用戶級線程與內核級線程之間存在一對一的關系。該模型比多對一模型並發性好,當一個線程進行阻塞系統調用時,它還允許另一個線程運行,所以它支持多個線程以在處理器上並行執行。
該模型的缺點是創建用戶線程需要相應的內核線程,而創建內核線程開銷是很大的.

Many-to-many

在多對多模型中,m個內核線程處理n個用戶線程,其中m < n. 該模型並發性最好,並且不用創建過多的內核線程,涉及到的線程切換同步的開銷也更小.

真相浮出水面

.Net的代碼作為托管代碼在“托管線程”上執行,而托管線程是在CLR虛擬機上執行的虛擬線程,也是屬於用戶級線程.

正如JIT編譯器將“虛擬” IL指令映射到在物理計算機上執行的本機指令一樣,CLR的線程基礎結構也將“虛擬”托管線程映射到操作系統提供的內核線程。

說到這里,我們也差不多有了前面我說的那個現象的答案了,並非其他處理器不想與那個水深火熱的處理器有難同享,而是我沒有使用多線程,所以執行的程序只有一個主線程,也就是說用戶線程數為1.只能是one to one 模型,所以只有一個處理器能參與工作.

既然知道了里面的原理,那我們就對前文中的程序進行改造,創建四個線程來執行任務,會不會所有處理器都忙起來呢?

    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Start...");
            for (var i = 0; i < 4; i++)
            {
                var td=new Thread(DoSomething);
                td.Start();
            }
            Console.WriteLine("Ending...");
            Console.ReadLine();
        }

        static void DoSomething()
        {
            var sum="";
            for (int i = 2; i < int.MaxValue; i++)
            {
                sum += "s";
            }
            Console.WriteLine(sum.Length);
        }
    }

可以看到,這次大家的步伐都做到了驚人的一致,四個處理器都被調用起來,加上主線程,這里至少有五個用戶線程,所以這里應該是many to many的模型了.

謝謝觀賞!

參考資料:
https://en.wikipedia.org/wiki/Protection_ring
https://stackoverflow.com/questions/15093510/whats-c-sharp-threading-type
https://github.com/dotnet/coreclr/blob/master/Documentation/botr/threading.md#clr-threading-overview


免責聲明!

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



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