日本综合一区二区|亚洲中文天堂综合|日韩欧美自拍一区|男女精品天堂一区|欧美自拍第6页亚洲成人精品一区|亚洲黄色天堂一区二区成人|超碰91偷拍第一页|日韩av夜夜嗨中文字幕|久久蜜综合视频官网|精美人妻一区二区三区

RELATEED CONSULTING
相關(guān)咨詢
選擇下列產(chǎn)品馬上在線溝通
服務(wù)時(shí)間:8:30-17:00
你可能遇到了下面的問題
關(guān)閉右側(cè)工具欄

新聞中心

這里有您想知道的互聯(lián)網(wǎng)營銷解決方案
C# Interlocked 類

【前言】

在日常開發(fā)工作中,我們經(jīng)常要對(duì)變量進(jìn)行操作,例如對(duì)一個(gè)int變量遞增++。在單線程環(huán)境下是沒有問題的,但是如果一個(gè)變量被多個(gè)線程操作,那就有可能出現(xiàn)結(jié)果和預(yù)期不一致的問題。

創(chuàng)新互聯(lián)專業(yè)為企業(yè)提供海曙網(wǎng)站建設(shè)、海曙做網(wǎng)站、海曙網(wǎng)站設(shè)計(jì)、海曙網(wǎng)站制作等企業(yè)網(wǎng)站建設(shè)、網(wǎng)頁設(shè)計(jì)與制作、海曙企業(yè)網(wǎng)站模板建站服務(wù),十余年海曙做網(wǎng)站經(jīng)驗(yàn),不只是建網(wǎng)站,更提供有價(jià)值的思路和整體網(wǎng)絡(luò)服務(wù)。

例如:

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

在單線程情況下執(zhí)行,結(jié)果一定為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 這個(gè)結(jié)果是隨機(jī)的,和每個(gè)線程執(zhí)行情況有關(guān)
}

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

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

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 這里是一定的
}

加鎖的確能解決上述問題,那么有沒有一種更加輕量級(jí),更加簡(jiǎn)潔的寫法呢?

那么,今天我們就來認(rèn)識(shí)一下 Interlocked 類

【Interlocked 類下的方法】

Increment(ref int location)

Increment 方法可以輕松實(shí)現(xiàn)線程安全的變量自增

/// 
/// thread safe increament
/// 
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
}

看到這里,我們一定好奇這個(gè)方法底層是怎么實(shí)現(xiàn)的?

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

首先看到 Increment 方法其實(shí)是通過調(diào)用 Add 方法來實(shí)現(xiàn)自增的

再往下看,Add 方法是通過 ExchangeAdd 方法來實(shí)現(xiàn)原子性的自增,因?yàn)樵摲椒ǚ祷刂凳窃黾忧暗脑?,因此返回時(shí)增加了本次新增的,結(jié)果便是相加的結(jié)果,當(dāng)然 location1 變量已經(jīng)遞增成功了,這里只是為了友好地返回增加后的結(jié)果。

我們?cè)偻驴?/p>

這個(gè)方法用 [MethodImpl(MethodImplOptions.InternalCall)] 修飾,表明這里調(diào)用的是 CLR 內(nèi)部代碼,我們只能通過查看源碼來繼續(xù)學(xué)習(xí)。

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

找到 Interlocked 中的 ExchangeAdd 方法

可以看到,該方法用循環(huán)不斷自旋賦值并檢查是否賦值成功(CompareExchange返回的是修改前的值,如果返回結(jié)果和修改前結(jié)果是一致,則說明修改成功)

我們繼續(xù)看內(nèi)部實(shí)現(xiàn)

內(nèi)部調(diào)用 InterlockedCompareExchange 函數(shù),再往下就是直接調(diào)用的C++源碼了

在這里將變量添加 volatile 修飾符,阻止寄存器緩存變量值(關(guān)于volatile不在此贅述),然后直接調(diào)用了C++底層內(nèi)部函數(shù) __sync_val_compare_and_swap 實(shí)現(xiàn)原子性的比較交換操作,這里直接用的是 CPU 指令進(jìn)行原子性操作,性能非常高。

相同機(jī)制函數(shù)

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

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

public static long Read(ref long location)

Read 這個(gè)函數(shù)著重提一下

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

這是因?yàn)镃PU有 32 位處理器和 64 位處理器,在 64 位處理器上,寄存器一次處理的數(shù)據(jù)寬度是 64 位,因此在 64 位處理器和 64 位操作系統(tǒng)上運(yùn)行的程序,可以一次性讀取 64 位數(shù)值。

但是在 32 位處理器和 32 位操作系統(tǒng)情況下,long/ulong 這種數(shù)值,則要分成兩步操作來進(jìn)行,分別讀取 32 位數(shù)據(jù)后,再合并在一起,那顯然就會(huì)出現(xiàn)多線程情況下的并發(fā)問題。

因此這里提供了原子性的方法來應(yīng)對(duì)這種情況。

這里底層同樣用了 CompareExchange 操作來保證原子性,參數(shù)這里就給了兩個(gè)0,可以兼容如果原值是 0 則寫入 0 ,如果原值非 0 則不寫入,返回原值。

__sync_val_compare_and_swap 函數(shù)
在寫入新值之前, 讀出舊值, 當(dāng)且僅當(dāng)舊值與存儲(chǔ)中的當(dāng)前值一致時(shí),才把新值寫入存儲(chǔ)

【關(guān)于性能】

多線程下實(shí)現(xiàn)原子性操作方式有很多種,我們一定會(huì)關(guān)心在不同場(chǎng)景下,不同方法間的性能問題,那么我們簡(jiǎn)單來對(duì)比下 Interlocked 類提供的方法和 lock 關(guān)鍵字的性能對(duì)比

我們同樣用線程池調(diào)度50個(gè)Task(內(nèi)部可能線程重用),分別執(zhí)行 200000 次自增運(yùn)算

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}");
}

運(yùn)算結(jié)果

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

雖然這里看起來性能要好,但是不同的業(yè)務(wù)場(chǎng)景要針對(duì)性思考,采用恰當(dāng)?shù)木幋a方式,不要一味追求性能

我們簡(jiǎn)單分析下造成執(zhí)行時(shí)間差異的原因

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

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

使用 Process Explorer 工具查看代碼執(zhí)行過程中上下文切換數(shù)

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

那么我們用同樣的方式看下底層用 CAS 函數(shù)執(zhí)行自增的開銷

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

使用 Process Explorer 工具查看代碼執(zhí)行過程中上下文切換數(shù)

可以大概估計(jì)出,采用 CAS 自增方式,上下文切換 220

可見,不論使用什么技術(shù)手段,線程創(chuàng)建太多都會(huì)帶來大量的線程上下文切換

這個(gè)應(yīng)該是和測(cè)試的代碼相關(guān)

兩者比較大的區(qū)別在CPU的使用率上,因?yàn)?lock 方式會(huì)造成線程阻塞,因此不會(huì)所有的CPU核心同時(shí)參與運(yùn)算,CPU在當(dāng)前進(jìn)程上使用率不會(huì)太高,但 cas 方式CPU在自己的時(shí)間分片內(nèi)并沒有被阻塞或重新調(diào)度,而是不停地執(zhí)行比較替換的動(dòng)作(其實(shí)這種場(chǎng)景算是無用功,不必要的負(fù)開銷),造成CPU使用率非常高。

【總結(jié)】

簡(jiǎn)單來說,Interlocked 類提供的方法給我們帶來了方便快捷操作字段的方式,比起使用鎖同步的編程方式來說,要輕量不少,執(zhí)行效率也大大提高。但是該技術(shù)并非銀彈,一定要考慮清楚使用的場(chǎng)景后再?zèng)Q定使用,比如服務(wù)器web應(yīng)用下,多線程執(zhí)行大量耗費(fèi)CPU的運(yùn)算,可能會(huì)嚴(yán)重影響應(yīng)用吞吐量。雖然表面看起來執(zhí)行這個(gè)單一的任務(wù)效率高一些(代價(jià)是CPU全部撲在這個(gè)任務(wù)上,無法響應(yīng)其他任務(wù)),其實(shí)在我們的測(cè)試中,總共執(zhí)行了 10000000 次運(yùn)算,這種場(chǎng)景應(yīng)該是比較極端的,而且在web應(yīng)用場(chǎng)景下,用 lock 的方式響應(yīng)時(shí)間也沒有達(dá)到不能容忍的程度,但是用 lock 的好處是cpu可以處理其他用戶請(qǐng)求的任務(wù),極大提高了吞吐量。

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

【源碼地址】

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


本文題目:C# Interlocked 類
本文鏈接:http://www.dlmjj.cn/article/dsoiojs.html