1. 程式人生 > >C#多執行緒系列(3):原子操作

C#多執行緒系列(3):原子操作

本章主要講述多執行緒競爭下的原子操作。

目錄

  • 知識點
    • 競爭條件
    • 執行緒同步
    • CPU時間片和上下文切換
    • 阻塞
    • 核心模式和使用者模式
  • Interlocked 類
    • 1,出現問題
    • 2,Interlocked.Increment()
    • 3,Interlocked.Exchange()
    • 4,Interlocked.CompareExchange()
    • 5,Interlocked.Add()
    • 6,Interlocked.Read()

知識點

競爭條件

當兩個或兩個以上的執行緒訪問共享資料,並且嘗試同時改變它時,就發生爭用的情況。它們所依賴的那部分共享資料,叫做競爭條件。

資料爭用是競爭條件中的一種,出現競爭條件可能會導致記憶體(資料)損壞或者出現不確定性的行為。

執行緒同步

如果有 N 個執行緒都會執行某個操作,當一個執行緒正在執行這個操作時,其它執行緒都必須依次等待,這就是執行緒同步。

多執行緒環境下出現競爭條件,通常是沒有執行正確的同步而導致的。

CPU時間片和上下文切換

時間片(timeslice)是作業系統分配給每個正在執行的程序微觀上的一段 CPU 時間。

首先,核心會給每個程序分配相等的初始時間片,然後每個程序輪番地執行相應的時間,當所有程序都處於時間 片耗盡的狀態時,核心會重新為每個程序計算並分配時間片,如此往復。

請參考:https://zh.wikipedia.org/wiki/%E6%97%B6%E9%97%B4%E7%89%87

上下文切換(Context Switch),也稱做程序切換或任務切換,是指 CPU 從一個程序或執行緒切換到另一個程序或執行緒。

在接受到中斷(Interrupt)的時候,CPU 必須要進行上下文交換。進行上下文切換時,會帶來效能損失。

請參考[https://zh.wikipedia.org/wiki/上下文交換

阻塞

阻塞狀態指執行緒處於等待狀態。當執行緒處於阻塞狀態時,會盡可能少佔用 CPU 時間。

當執行緒從執行狀態(Runing)變為阻塞狀態時(WaitSleepJoin),作業系統就會將此執行緒佔用的 CPU 時間片分配給別的執行緒。當執行緒恢復執行狀態時(Runing),作業系統會重新分配 CPU 時間片。

分配 CPU 時間片時,會出現上下文切換。

核心模式和使用者模式

只有作業系統才能切換執行緒、掛起執行緒,因此阻塞執行緒是由作業系統處理的,這種方式被稱為核心模式(kernel-mode)。

Sleep()Join() 等,都是使用核心模式來阻塞執行緒,實現執行緒同步(等待)。

核心模式實現執行緒等待時,出現上下文切換。這適合等待時間比較長的操作,這樣會減少大量的 CPU 時間損耗。

如果執行緒只需要等待非常微小的時間,阻塞執行緒帶來的上下文切換代價會比較大,這時我們可以使用自旋,來實現執行緒同步,這一方法稱為使用者模式(user-mode)。

Interlocked 類

為多個執行緒共享的變數提供原子操作。

使用 Interlocked 類,可以在不阻塞執行緒(lock、Monitor)的情況下,避免競爭條件。

Interlocked 類是靜態類,讓我們先來看看 Interlocked 的常用方法:

方法 作用
CompareExchange() 比較兩個數是否相等,如果相等,則替換第一個值。
Decrement() 以原子操作的形式遞減指定變數的值並存儲結果。
Exchange() 以原子操作的形式,設定為指定的值並返回原始值。
Increment() 以原子操作的形式遞增指定變數的值並存儲結果。
Add() 對兩個數進行求和並用和替換第一個整數,上述操作作為一個原子操作完成。
Read() 返回一個以原子操作形式載入的值。

全部方法請檢視:https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.interlocked?view=netcore-3.1#methods

1,出現問題

問題:

​ C# 中賦值和一些簡單的數學運算不是原子操作,受多執行緒環境影響,可能會出現問題。

我們可以使用 lock 和 Monitor 來解決這些問題,但是還有沒有更加簡單的方法呢?

首先我們編寫以下程式碼:

        private static int sum = 0;
        public static void AddOne()
        {
            for (int i = 0; i < 100_0000; i++)
            {
                sum += 1;
            }
        }

這個方法的工作完成後,sum 會 +100。

我們在 Main 方法中呼叫:

        static void Main(string[] args)
        {
            AddOne();
            AddOne();
            AddOne();
            AddOne();
            AddOne();
            Console.WriteLine("sum = " + sum);
        }

結果肯定是 5000000,無可爭議的。

但是這樣會慢一些,如果作死,要多執行緒同時執行呢?

好的,Main 方法改成如下:

        static void Main(string[] args)
        {
            for (int i = 0; i < 5; i++)
            {
                Thread thread = new Thread(AddOne);
                thread.Start();
            }

            Thread.Sleep(TimeSpan.FromSeconds(2));
            Console.WriteLine("sum = " + sum);
        }

筆者執行一次,出現了 sum = 2633938

我們將每次運算的結果儲存到陣列中,擷取其中一段發現:

8757
8758
8760
8760
8760
8761
8762
8763
8764
8765
8766
8766
8768
8769

多個執行緒使用同一個變數進行操作時,並不知道此變數已經在其它執行緒中發生改變,導致執行完畢後結果不符合期望。

我們可以通過下面這張圖來解釋:

因此,這裡就需要原子操作,在某個時刻,必須只有一個執行緒能夠進行某個操作。而上面的操作,指的是讀取、計算、寫入這一過程。

當然,我們可以使用 lock 或者 Monitor 來解決,但是這樣會帶來比較大的效能損失。

這時 Interlocked 就起作用了,對於一些簡單的操作運算, Interlocked 可以實現原子性的操作。

實現原子性,可以通過多種鎖來解決,目前我們學習到了 lock、Monitor,現在來學習 Interlocked ,後面會學到更加多的鎖的實現。

2,Interlocked.Increment()

用於自增操作。

我們修改一下 AddOne 方法:

        public static void AddOne()
        {
            for (int i = 0; i < 100_0000; i++)
            {
                Interlocked.Increment(ref sum);
            }
        }

然後執行,你會發現結果 sum = 5000000 ,這就對了。

說明 Interlocked 可以對簡單值型別進行原子操作。

Interlocked.Increment() 是遞增,而 Interlocked.Decrement() 是遞減。

3,Interlocked.Exchange()

Interlocked.Exchange() 實現賦值運算。

這個方法有多個過載,我們找其中一個來看看:

public static int Exchange(ref int location1, int value);

意思是將 value 賦給 location1 ,然後返回 location1 改變之前的值。

測試:

        static void Main(string[] args)
        {
            int a = 1;
            int b = 5;

            // a 改變前為1
            int result1 = Interlocked.Exchange(ref a, 2);

            Console.WriteLine($"a新的值 a = {a}   |  a改變前的值 result1 = {result1}");

            Console.WriteLine();

            // a 改變前為 2,b 為 5
            int result2 = Interlocked.Exchange(ref a, b);

            Console.WriteLine($"a新的值 a = {a}   | b不會變化的  b = {b}   |   a 之前的值  result2 = {result2}");
        }

另外 Exchange() 也有對引用型別的過載:

Exchange<T>(T, T)

4,Interlocked.CompareExchange()

其中一個過載:

public static int CompareExchange (ref int location1, int value, int comparand)

比較兩個 32 位有符號整數是否相等,如果相等,則替換第一個值。

如果 comparandlocation1 中的值相等,則將 value 儲存在 location1中。 否則,不會執行任何操作。

看準了,是 location1comparand 比較!

使用示例如下:

        static void Main(string[] args)
        {
            int location1 = 1;
            int value = 2;
            int comparand = 3;

            Console.WriteLine("執行前:");
            Console.WriteLine($" location1 = {location1}    |   value = {value} |   comparand = {comparand}");

            Console.WriteLine("當 location1 != comparand 時");
            int result = Interlocked.CompareExchange(ref location1, value, comparand);
            Console.WriteLine($" location1 = {location1} | value = {value} |  comparand = {comparand} |  location1 改變前的值  {result}");

            Console.WriteLine("當 location1 == comparand 時");
            comparand = 1;
            result = Interlocked.CompareExchange(ref location1, value, comparand);
            Console.WriteLine($" location1 = {location1} | value = {value} |  comparand = {comparand} |  location1 改變前的值  {result}");
        }

5,Interlocked.Add()

對兩個 32 位整數進行求和並用和替換第一個整數,上述操作作為一個原子操作完成。

public static int Add (ref int location1, int value);

只能對 int 或 long 有效。

回到第一小節的多執行緒求和問題,使用 Interlocked.Add() 來替換Interlocked.Increment()

        static void Main(string[] args)
        {
            for (int i = 0; i < 5; i++)
            {
                Thread thread = new Thread(AddOne);
                thread.Start();
            }

            Thread.Sleep(TimeSpan.FromSeconds(2));
            Console.WriteLine("sum = " + sum);
        }
        private static int sum = 0;
        public static void AddOne()
        {
            for (int i = 0; i < 100_0000; i++)
            {
                Interlocked.Add(ref sum,1);
            }
        }

6,Interlocked.Read()

返回一個以原子操作形式載入的 64 位值。

64位系統上不需要 Read 方法,因為64位讀取操作已是原子操作。 在32位系統上,64位讀取操作不是原子操作,除非使用 Read 執行。

public static long Read (ref long location);

就是說 32 位系統上才用得上。

具體場景我沒有找到。

你可以參考一下 https://www.codenong.com/6139699/

貌似沒有多大用處?那我懶得看了。

相關推薦

C#執行系列(3)原子操作

本章主要講述多執行緒競爭下的原子操作。 目錄知識點競爭條件執行緒同步CPU時間片和上下文切換阻塞核心模式和使用者模式Interlocked 類1,出現問題2,Interlocked.Increment()3,Interlocked.Exchange()4,Interlocked.CompareExchange

java執行系列3悲觀鎖和樂觀鎖

1.悲觀鎖和樂觀鎖的基本概念 悲觀鎖: 總是認為當前想要獲取的資源存在競爭(很悲觀的想法),因此獲取資源後會立刻加鎖,於是其他執行緒想要獲取該資源的時候就會一直阻塞直到能夠獲取到鎖; 在傳統的關係型資料庫中,例如行鎖、表鎖、讀鎖、寫鎖等,都用到了悲觀鎖。還有java中的同步關鍵字Synchroniz

C#執行系列(1)Thread

目錄1,獲取當前執行緒資訊2,管理執行緒狀態2.1 啟動與引數傳遞2.1.1 ParameterizedThreadStart2.1.2 使用靜態變數或類成員變數2.1.3 委託與Lambda2.2 暫停與阻塞2.3 執行緒狀態2.4 終止2.5 執行緒的不確定性2.6 執行緒優先順序、前臺執行緒和後臺執行緒

C++執行-第一篇-Atomic-原子操作

此係列基於Boost庫多執行緒,但是大部分都在C++11中已經實現,所以兩者基本一致。沒什麼特殊要求,練手還是C++11吧,方便不用配置。 PS:Boost不愧為C++準標準庫。 本來不打算寫,畢竟都是書上的內容,但是後來發現查書太麻煩,所以動手寫了這個系列,幫助我只看程式

秒殺執行第三篇 原子操作 Interlocked系列函式

上一篇《多執行緒第一次親密接觸 CreateThread與_beginthreadex本質區別》中講到一個多執行緒報數功能。為了描述方便和程式碼簡潔起見,我們可以只輸出最後的報數結果來觀察程式是否執行出錯。這也非常類似於統計一個網站每天有多少使用者登入,每個使用者登入用一個執

C++執行系列C++11)-uniqu_lock(四)

Data 2018/11/12 Add By  WJB 在多執行緒中,有時候會出現一個方法中又一斷或者多段程式碼需要加鎖,但是並非整個方法程式碼加鎖,那麼我們就需要一個靈活的鎖-unique_lock;說明:unique_lock會降低程式碼執行效率,不推薦使用。 我們接

MFC筆記(四)——執行程式設計3用_beginthreadex()來代替使用CreateThread()

        CreateThread()函式是Windows提供的API介面,在C/C++語言另有一個建立執行緒的函式_beginthreadex(),在很多書上(包括《Windows核心程式設計》)提到過儘量使用_begin

C++執行系列(二)執行互斥

首先了解一下執行緒互斥的概念,執行緒互斥說白了就是在程序中多個執行緒的相互制約,如執行緒A未執行完畢,其他執行緒就需要等待! 執行緒之間的制約關係分為間接相互制約和直接相互制約。 所謂間接相互制約:一個系統中的多個執行緒必然要共享某種系統資源如共享CPU,共享印表機。間接制

C++執行系列(一)CreateThread和_beginthreadex區別

現在在學習多執行緒,順便將蒐集到的資料整理下來以供參考和查詢。首先在開始多執行緒學習的時候遇到的首要問題便是多執行緒的建立,在查閱資料後有CreateThread和_beginthreadex兩種方法,可能不止這兩種,以後學習到了再補充。-------------------

C++執行初級一建立執行

以函式為引數建立執行緒: // PolythreadDemo.cpp : 定義控制檯應用程式的入口點。 //這裡有一個觀點,就是當使用某個函式的時候,再 //寫上標頭檔案,不用一開始就來、

C#執行開發10執行同步之Semaphore類

Semaphore類表示訊號量。 訊號量和互斥類似,只是訊號量可以同時由多個執行緒使用,而互斥只能由一個執行緒使用。也就是說,使用訊號量時,可以多個執行緒同時訪問受保護的資源。下面例項演示了“學生到食

C#執行開發5執行的Abort和Interrupt方法

使用執行緒的Abort方法可以終止執行緒;而使用執行緒的Interrupt方法只可以中斷處於 WaitSleepJoin 狀態的執行緒,當執行緒狀態不再為WaitSleepJoin時,執行緒將恢復執行

執行系列併發工具類和併發容器

一、併發容器 1.ConcurrentHashMap 為什麼要使用ConcurrentHashMap 在多執行緒環境下

執行上下文角度重新理解.NET(Core)的執行程式設計[3]安全上下文

在前兩篇文章(《基於呼叫鏈的”引數”傳遞》和《同步上下文》)中,我們先後介紹了CallContext(IllogicalCallContext和LogicalCallContext)、AsyncLocal<T>和SynchronizationContext,它們都是執行緒執行上下文的一部分。本篇介

執行和鎖和原子操作和記憶體柵欄(二)

        這裡記錄下各種鎖的使用和使用場景,在多執行緒場景開發時,我們經常遇到多個執行緒同時讀寫一塊資源爭搶一塊資源的情況,比如同時讀寫同一個欄位屬性,同時對某個集合進行增刪改查,同時對資料庫進行讀寫(這裡

執行和鎖和原子操作和記憶體柵欄(一)

執行緒的定義是執行流的最小單元,而程序是一個邏輯執行緒容器,用來隔離執行緒。 Task類封裝了執行緒池執行緒,啟動的所有線都由執行緒池管理,他提供了很多使用方便的API函式,使多執行緒開發變得容易。 上述程式碼中我啟動了一個執行緒,並在執行緒方法中使用了非同步關鍵字,非同步方法實現了一個狀態

Java執行併發鎖和原子操作,你真的瞭解嗎?

前言                 對於Java多執行緒,接觸最多的莫過於使用synchronized,這個簡單易懂,但是這synchronized並非效能最優的。今天我就簡單介紹一下幾種鎖。可能我下面講的時候其實很多東西不會特別深刻,最好的方式是自己做實驗,把各種場景在

執行程式設計學習八(原子操作類).

簡介 Java 在 JDK 1.5 中提供了 java.util.concurrent.atomic 包,這個包中的原子操作類提供了一種用法簡單、效能高效、執行緒安全地更新一個變數的方式。主要提供了四種類型的原子更新方式,分別是原子更新基本型別、原子更新陣列、原子更新引用和原子更新屬性。 Atomic 類基本

C++ 執行框架(3訊息佇列

之前,多執行緒一些基本的東西,包括執行緒建立,互斥鎖,訊號量,我們都已經封裝,下面來看看訊息佇列 我們儘量少用系統自帶的訊息佇列(比如Linux的sys/msgqueue),那樣移植性不是很強,我們希望的訊息佇列,在訊息打包和提取都是用的標準的C++資料結構,當然,

c++執行重點難點(一)interlocked系列原子操作

_beginthreadex()函式在建立新執行緒時會分配並初始化一個_tiddata塊。這個_tiddata塊自然是用來存放一些需要執行緒獨享的資料。事實上新執行緒執行時會首先將_tiddata塊與自己進一步關聯起來。然後新執行緒呼叫標準C執行庫函式如st