C# Interlocked 類

来源:https://www.cnblogs.com/7tiny/archive/2022/11/03/16833391.html
-Advertisement-
Play Games

【前言】 在日常開發工作中,我們經常要對變數進行操作,例如對一個int變數遞增++。在單線程環境下是沒有問題的,但是如果一個變數被多個線程操作,那就有可能出現結果和預期不一致的問題。 例如: static void Main(string[] args) { var j = 0; for (int ...


【前言】

在日常開發工作中,我們經常要對變數進行操作,例如對一個int變數遞增++。在單線程環境下是沒有問題的,但是如果一個變數被多個線程操作,那就有可能出現結果和預期不一致的問題。

例如:

static void Main(string[] args)
{
    var j = 0;
    for (int i = 0; i < 100; i++)
    {
        j++;
    }
    Console.WriteLine(j);
    //100
}

在單線程情況下執行,結果一定為100,那麼在多線程情況下呢?

static void Main(string[] args)
{
    var j = 0;
    var t1 = Task.Run(() =>
    {
        for (int i = 0; i < 50000; i++)
        {
            j++;
        }
    });
    var t2 = Task.Run(() =>
    {
        for (int i = 0; i < 50000; i++)
        {
            j++;
        }
    });
    Task.WaitAll(t1, t2);
    Console.WriteLine(j);
    //82869 這個結果是隨機的,和每個線程執行情況有關
}

我們可以看到,多線程情況下並不能保證執行正確,我們也將這種情況稱為 “非線程安全”

這種情況下我們可以通過加鎖來達到線程安全的目的

static void Main(string[] args)
{
    var locker = new object();
    var j = 0;
    var t1 = Task.Run(() =>
    {
        for (int i = 0; i < 50000; i++)
        {
            lock (locker)
            {
                j++;
            }
        }
    });
    var t2 = Task.Run(() =>
    {
        for (int i = 0; i < 50000; i++)
        {
            lock (locker)
            {
                j++;
            }
        }
    });
    Task.WaitAll(t1, t2);
    Console.WriteLine(j);
    //100000 這裡是一定的
}

加鎖的確能解決上述問題,那麼有沒有一種更加輕量級,更加簡潔的寫法呢?

那麼,今天我們就來認識一下 Interlocked 類

【Interlocked 類下的方法】

Increment(ref int location)

Increment 方法可以輕鬆實現線程安全的變數自增

/// <summary>
/// thread safe increament
/// </summary>
public static void Increament()
{
    var j = 0;

    Task.WaitAll(
        Enumerable.Range(0, 50)
        .Select(t =>
            Task.Run(() =>
            {
                for (int i = 0; i < 2000; i++)
                {
                    Interlocked.Increment(ref j);
                }
            }
        ))
        .ToArray()
        );

    Console.WriteLine($"multi thread increament result={j}");
    //result=100000
}

看到這裡,我們一定好奇這個方法底層是怎麼實現的?

我們通過ILSpy反編譯查看源碼:

首先看到 Increment 方法其實是通過調用 Add 方法來實現自增的

再往下看,Add 方法是通過 ExchangeAdd 方法來實現原子性的自增,因為該方法返回值是增加前的原值,因此返回時增加了本次新增的,結果便是相加的結果,當然 location1 變數已經遞增成功了,這裡只是為了友好地返回增加後的結果。

我們再往下看

這個方法用 [MethodImpl(MethodImplOptions.InternalCall)] 修飾,表明這裡調用的是 CLR 內部代碼,我們只能通過查看源碼來繼續學習。

我們打開 dotnetcore 源碼:https://github.com/dotnet/corefx

找到 Interlocked 中的 ExchangeAdd 方法

image

可以看到,該方法用迴圈不斷自旋賦值並檢查是否賦值成功(CompareExchange返回的是修改前的值,如果返回結果和修改前結果是一致,則說明修改成功)

我們繼續看內部實現

image

image

內部調用 InterlockedCompareExchange 函數,再往下就是直接調用的C++源碼了

image

image

在這裡將變數添加 volatile 修飾符,阻止寄存器緩存變數值(關於volatile不在此贅述),然後直接調用了C++底層內部函數 __sync_val_compare_and_swap 實現原子性的比較交換操作,這裡直接用的是 CPU 指令進行原子性操作,性能非常高。

相同機制函數

Increment 函數機制類似,Interlocked 類下的大部分方法都是通過 CompareExchange 底層函數來操作的,因此這裡不再贅述

  • Add 添加值
  • CompareExchange 比較交換
  • Decrement 自減
  • Exchange 交換
  • And 按位與
  • Or 按位或
  • Read 讀64位數值

public static long Read(ref long location)

Read 這個函數著重提一下

image

可以看到這個函數沒有 32 位(int)類型的重載,為什麼要單獨為 64 位的 long/ulong 類型單獨提供原子性讀取操作符呢?

這是因為CPU有 32 位處理器和 64 位處理器,在 64 位處理器上,寄存器一次處理的數據寬度是 64 位,因此在 64 位處理器和 64 位操作系統上運行的程式,可以一次性讀取 64 位數值。

但是在 32 位處理器和 32 位操作系統情況下,long/ulong 這種數值,則要分成兩步操作來進行,分別讀取 32 位數據後,再合併在一起,那顯然就會出現多線程情況下的併發問題。

因此這裡提供了原子性的方法來應對這種情況。

image

這裡底層同樣用了 CompareExchange 操作來保證原子性,參數這裡就給了兩個0,可以相容如果原值是 0 則寫入 0 ,如果原值非 0 則不寫入,返回原值。

__sync_val_compare_and_swap 函數
在寫入新值之前, 讀出舊值, 當且僅當舊值與存儲中的當前值一致時,才把新值寫入存儲

【關於性能】

多線程下實現原子性操作方式有很多種,我們一定會關心在不同場景下,不同方法間的性能問題,那麼我們簡單來對比下 Interlocked 類提供的方法和 lock 關鍵字的性能對比

我們同樣用線程池調度50個Task(內部可能線程重用),分別執行 200000 次自增運算

public static void IncreamentPerformance()
{
    //lock method

    var locker = new object();

    var stopwatch = new Stopwatch();

    stopwatch.Start();

    var j1 = 0;

    Task.WaitAll(
        Enumerable.Range(0, 50)
        .Select(t =>
            Task.Run(() =>
            {
                for (int i = 0; i < 200000; i++)
                {
                    lock (locker)
                    {
                        j1++;
                    }
                }
            }
        ))
        .ToArray()
        );

    Console.WriteLine($"Monitor lock,result={j1},elapsed={stopwatch.ElapsedMilliseconds}");

    stopwatch.Restart();

    //Increment method

    var j2 = 0;

    Task.WaitAll(
        Enumerable.Range(0, 50)
        .Select(t =>
            Task.Run(() =>
            {
                for (int i = 0; i < 200000; i++)
                {
                    Interlocked.Increment(ref j2);
                }
            }
        ))
        .ToArray()
        );

    stopwatch.Stop();

    Console.WriteLine($"Interlocked.Increment,result={j2},elapsed={stopwatch.ElapsedMilliseconds}");
}

運算結果

可以看到,採用 Interlocked 類中的自增函數,性能比 lock 方式要好一些

雖然這裡看起來性能要好,但是不同的業務場景要針對性思考,採用恰當的編碼方式,不要一味追求性能

我們簡單分析下造成執行時間差異的原因

我們都知道,使用lock(底層是Monitor類),在上述代碼中會阻塞線程執行,保證同一時刻只能有一個線程執行 j1++ 操作,因此能保證操作的原子性,那麼在多核CPU下,也只能有一個CPU核心在執行這段邏輯,其他核心都會等待或執行其他事件,線程阻塞後,並不會一直在這裡傻等,而是由操作系統調度執行其他任務。由此帶來的代價可能是頻繁的線程上下文切換,並且CPU使用率不會太高,我們可以用分析工具來印證下。

Visual Studio 自帶的分析工具,查看線程使用率

使用 Process Explorer 工具查看代碼執行過程中上下文切換數

可以大概估計出,採用 lock(Monitor)同步自增方式,上下文切換 243

那麼我們用同樣的方式看下底層用 CAS 函數執行自增的開銷

Visual Studio 自帶的分析工具,查看線程使用率

使用 Process Explorer 工具查看代碼執行過程中上下文切換數

可以大概估計出,採用 CAS 自增方式,上下文切換 220

可見,不論使用什麼技術手段,線程創建太多都會帶來大量的線程上下文切換

這個應該是和測試的代碼相關

兩者比較大的區別在CPU的使用率上,因為 lock 方式會造成線程阻塞,因此不會所有的CPU核心同時參與運算,CPU在當前進程上使用率不會太高,但 cas 方式CPU在自己的時間分片內並沒有被阻塞或重新調度,而是不停地執行比較替換的動作(其實這種場景算是無用功,不必要的負開銷),造成CPU使用率非常高。

【總結】

簡單來說,Interlocked 類提供的方法給我們帶來了方便快捷操作欄位的方式,比起使用鎖同步的編程方式來說,要輕量不少,執行效率也大大提高。但是該技術並非銀彈,一定要考慮清楚使用的場景後再決定使用,比如伺服器web應用下,多線程執行大量耗費CPU的運算,可能會嚴重影響應用吞吐量。雖然錶面看起來執行這個單一的任務效率高一些(代價是CPU全部撲在這個任務上,無法響應其他任務),其實在我們的測試中,總共執行了 10000000 次運算,這種場景應該是比較極端的,而且在web應用場景下,用 lock 的方式響應時間也沒有達到不能容忍的程度,但是用 lock 的好處是cpu可以處理其他用戶請求的任務,極大提高了吞吐量。

我們建議在競爭較少的場景,或者不需要很高吞吐量的場景下(簡單說是CPU時間不那麼寶貴的場景下)我們可以用 Interlocked 類來保證操作的原子性,可以適當提升性能。而在競爭非常激烈的場景下,一定不要用 Interlocked 來處理原子性操作,改用 lock 方式會好很多。

【源碼地址】

https://github.com/sevenTiny/CodeArts/blob/master/CSharp/ConsoleAppNet60/InterlockedTest.cs

【博主聲明】

本文為站主原創作品,轉載請註明出處:http://www.cnblogs.com/7tiny 且在文章頁面明顯位置給出原文鏈接。
作者:

7tiny
Software Development
北京市海澱區 Haidian Area Beijing 100089,P.R.China
郵箱Email : [email protected]  
網址Http: http://www.7tiny.com
WeChat: seven-tiny
更多聯繫方式點我哦~


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

-Advertisement-
Play Games
更多相關文章
  • Altair是Python統計可視化庫,提供了強大而簡潔的可視化語法,可以產出漂亮的數據分析可視化結果,並支持互動式操作和勾選局部數據深入分析。本文以實例講解Altair的數據分析過程,以及交互文檔報告的生成。... ...
  • json.loads(),json.dumps(): 用來處理數據格式(json <==> python) json.load(),json.dump(): 用於文件操作(讀、寫) ...
  • 什麼是變數 我們只要與生活中的數學做類型就可以清楚的瞭解什麼是變數 在Python中,變數的概念基本上和初中代數的方程變數是一致的。例如,對於方程式 y=x*x ,x就是變數。當x=2時,計算結果是4,當x=5時,計算結果是25 合法的變數名 我們在學習電腦程式過程中,變數不僅可以是數字,還可以是 ...
  • 前言 嗨嘍,大家好呀~這裡是愛看美女的茜茜吶 又到了學Python時刻~ 開發環境 & 第三方模塊: 解釋器版本: python 3.8 代碼編輯器: pycharm 2021.2 requests: pip install requests pyecharts: pip install pyech ...
  • 前言 我們在啟動 Spring Boot 項目時,控制台會列印出 Spring Boot 專屬的標語,也稱 banner(橫幅標語/廣告),效果如下: 實際上,上面這個 banner,我們可以自定義,而很多公司也有使用自己的 banner 的。 下麵介紹在 Spring Boot 項目中使用自定義 ...
  • 昨天,有讀者私信發我一篇文章,說裡面提到的 Intellij IDEA 插件真心不錯,基本上可以一站式開發了,希望能分享給更多的小伙伴,我在本地裝了體驗了一下,覺得確實值得推薦,希望小伙伴們有時間也可以嘗試一下。 Vuesion Theme 顏值是生產力的第一要素,IDE 整好看了,每天對著它也是神 ...
  • 在上一篇文章`《驅動開發:內核封裝WSK網路通信介面》`中,`LyShark`已經帶大家看過瞭如何通過WSK介面實現套接字通信,但WSK實現的通信是內核與內核模塊之間的,而如果需要內核與應用層之間通信則使用TDK會更好一些因為它更接近應用層,本章將使用TDK實現,TDI全稱傳輸驅動介面,其主要負責連... ...
  • 本章`LyShark`將帶大家學習如何在內核中使用標準的`Socket`套接字通信介面,我們都知道`Windows`應用層下可直接調用`WinSocket`來實現網路通信,但在內核模式下應用層API介面無法使用,內核模式下有一套專有的`WSK`通信介面,我們對WSK進行封裝,讓其與應用層調用規範保持... ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...