深入理解併發編程藝術之記憶體模型

来源:https://www.cnblogs.com/88223100/archive/2023/08/06/Deeply-Understanding-the-Memory-Model-of-Concurrent-Programming-Art.html
-Advertisement-
Play Games

隨著硬體技術的飛速發展,多核處理器已經成為計算設備的標配,這使得開發人員需要掌握併發編程的知識和技巧,以充分發揮多核處理器的潛力。然而併發編程並非易事,它涉及到許多複雜的概念和原理。為了更好地理解併發編程的內在機制,需要深入研究記憶體模型及其在併發編程中的應用。本文將主要以 Java 記憶體模型來探討並 ...


隨著硬體技術的飛速發展,多核處理器已經成為計算設備的標配,這使得開發人員需要掌握併發編程的知識和技巧,以充分發揮多核處理器的潛力。然而併發編程並非易事,它涉及到許多複雜的概念和原理。為了更好地理解併發編程的內在機制,需要深入研究記憶體模型及其在併發編程中的應用。本文將主要以 Java 記憶體模型來探討併發編程中 BUG 的源頭和處理這些問題的底層實現原理,助你更好地把握併發編程的內在機制。

 

併發編程問題-可見性和有序性

    private int a, b;
    private int x, y;

    public void test() {
        Thread t1 = new Thread(() -> {
            a = 1;
            x = b;
        });

        Thread t2 = new Thread(() -> {
            b = 2;
            y = a;
        });
        // ...start啟動線程,join等待線程
        assert x == 2;
        assert y == 1;
    }

首先我們先看一段代碼,這裡定義了兩個共用變數 x 和 y,在兩個線程中分別對 x 和 y 賦值,當同時開啟兩個線程並等待線程執行完成,最終結果是否是共用變數 x 等於 2 並且 y 等於 1 呢?答案是未可知,即共用變數 x 和 y 可能存在多種執行結果。可以看到在併發編程中,常常會遇到一些與預期不符的結果,導致程式邏輯的失敗。這樣的異常問題,會讓開發人員感到困惑。但是如果細細探究這些問題的根源,發現是有跡可循的。

這個問題的原因主要是兩點:一是處理器和記憶體對共用變數的處理的速度差異。二是編譯優化和處理器優化造成代碼指令重排序。前者導致可見性問題,後者導致有序性問題。

處理器緩存導致的可見性問題

圖片

如上圖所示,由於處理器和記憶體的速度差距太大。為了提高處理速度,處理器不直接和記憶體進行通信,而是先將系統記憶體的數據讀到內部緩存(L1,L2 或其他)後再進行操作。基於局部性原理,處理器在讀取記憶體數據時,是一塊塊地讀取,每一小塊數據也叫緩存行(cache line)。當處理器操作完數據,也不直接寫回記憶體,而且先寫入緩存中,並將當前緩存標記為臟(dirty)。等到當前緩存被替換時,才將數據寫回記憶體。這個過程叫寫回策略(write-back)。

同時為了提高效率,處理器使用寫緩存區(store buffer)臨時保存向記憶體寫入的數據。寫緩衝區可以保證指令流水線的持續運行,同時合併寫緩衝區中對同一記憶體地址的多次寫,減少記憶體匯流排的占用。但是由於緩衝區的數據並非及時寫回記憶體,且寫緩衝區僅對自己的處理器可見,其他處理器無法感知當前共用變數已經變更。處理器的讀寫順序與記憶體實際操作的讀寫順序可能存在不一致。

圖片

現在再回來看上面代碼,那麼可以得到四種結果:

1)假設處理器 A 對變數 a 賦值,但沒及時回寫記憶體。處理器 B 對變數 b 賦值,且及時回寫記憶體。處理器 A 從記憶體中讀到變數 b 最新值。那麼這時結果是:x 等於 2,y 等於 0。

2)假設處理器 A 對變數 a 賦值,且及時回寫記憶體。處理器 B 從記憶體中讀到變數 a 最新值。處理器 B 對變數 b 賦值,但沒及時回寫記憶體。那麼這時結果是:x 等於 0,y 等於 1。

3)假設處理器 A 和 B,都沒及時回寫變數 a 和 b 值到記憶體。那麼這時結果是:x 等於 0,y 等於 0。

4)假設處理器 A 和 B,都及時回寫變數 a 和 b 值到記憶體,且從記憶體中讀到變數 a 和 b 的最新值。那麼這時結果是:x 等於 2,y 等於 1。

從上面可發現除了第四種情況,其他三種情況都存在對共用變數的操作不可見。所謂可見性,便是當一個線程對某個共用變數的操作,另外一個線程立即可見這個共用變數的變更。

而從上面推論可以發現,要達到可見性,需要處理器及時回寫共用變數最新值到記憶體,也需要其他處理器及時從記憶體中讀取到共用變數最新值。

因此也可以說只要滿足上述兩個條件。那麼就可以保證對共用變數的操作,在併發情況下是線程安全的。在 Java 語言中,是通過 volatile 關鍵字實現。volatile 關鍵字並不是 Java 語言的特產,古老的 C 語言里也有,它最原始的意義就是禁用 CPU 緩存。

對如下共用變數:

  // instance是volatile變數
  volatile Singlenton instance = new Singlenton();

轉換成彙編代碼,如下:

0x01a3de1d: movb 5 0 x 0, 0 x 1104800(% esi);
0x01a3de24: lock addl $ 0 x 0,(% esp);

可以看到 volatile 修飾的共用變數會多出第二行彙編變數,並且多了一個 LOCK 指令。LOCK 首碼的指令在多核處理器會引發兩件事:

1)將當前處理器緩存行的數據寫回到系統記憶體。

2)這個寫回記憶體的操作會使在其他 CPU 里緩存了該記憶體地址的數據無效。

上述的操作是通過匯流排嗅探和匯流排仲裁來實現。而基於匯流排嗅探和匯流排仲裁,現代處理器逐漸形成了各種緩存一致性協議,例如 MESI 協議。

圖片

總之操作系統便是基於上述實現,從底層來保證共用變數在併發情況下的線程安全。而對開發人員,只需要在恰當時候加上 volatile 關鍵字就可以。

除了 volatile,也可以使用 synchronized 關鍵字來保證可見性。關於 volatile 和 synchronized 的具體實現,會在下篇文章詳細闡述。

編譯優化導致的有序性問題

前面講到通過緩存一致性協議,來保障共用變數的可見性。那麼是否還有其他情況,導致對共用變數操作不符合預期結果。可以看下麵的代碼:

    private int a, b;
    private int x, y;

    public void test() {
        Thread t1 = new Thread(() -> {
            x = b;
            a = 1;
        });

        Thread t2 = new Thread(() -> {
            y = a;
            b = 2;
        });
        // ...start啟動線程,join等待線程
        assert x == 2;
        assert y == 1;
    }

假設將線程 t1 的代碼塊從 a = 1;x = b;改成 x = b;a = 1; 。將線程 t2 的代碼塊從 b = 2;y = a;改成 y = a;b = 2;。

對於線程 t1 和 t2 自己來說,代碼的重排序,不會影響當前線程執行。但是在多線程併發執行下,會出現如下情況:

1)假設處理器 A 先將變數 b=0 賦值給 x,再將變數 a 賦值 1。處理器 B 先將變數 a=0 賦值給 y,再將變數 b 賦值 2。那麼這時結果是:x 等於 0,y 等於 0。

可見代碼的重排序也會影響到程式最終結果。

代碼和指令的重排序的主要原因有三個,分別為編譯器的重排序,處理器的亂序執行,以及記憶體系統的重排序。後面兩點是處理器優化。

圖片

重排序是指編譯器和處理器為了優化程式性能而對指令序列進行重新排序的一種手段。重排序需要遵守兩點:

1)數據依賴性:如果兩個操作之間存在數據依賴,那麼編譯器和處理器不能調整它們的順序。

// 寫後讀
a = 1;
b = a;
// 寫後寫
a = 1;
a = 2;
// 讀後寫
a = b;
b = 1;

上面 3 種情況,編譯器和處理器不能調整它們的順序,否則將會造成程式語義的改變。

2)as-if-serial 語義:即給程式一個順序執行的假象。即經過重排序的執行結果要與順序執行的結果保持一致。

a = 1;
b = 2;
c = a * b;

如上對變數 a 的賦值和對變數 b 的賦值,不存在數據依賴關係。因此對變數 a 和 b 重排序不會影響變數 c 的結果。

但數據依賴性和 as-if-serial 語義只保證單個處理器中執行的指令序列和單個線程中執行的操作,並不考慮多處理器和多線程之間的數據依賴情況。因此在多線程程式中,對存在數據依賴的操作重排序,可能會改變程式的執行結果。因此要避免程式的錯誤的執行,便是需要禁止這種編譯和處理器優化導致的重排序。

這種方式叫做記憶體屏障(memory barriers)。記憶體屏障是一組處理器指令,用戶實現對記憶體操作的順序限制。以我們日常接觸的 X86_64 架構來說,讀讀(loadload)、讀寫(loadstore)以及寫寫(storestore)記憶體屏障是空操作(no-op),只有寫讀(storeload)記憶體屏障會被替換成具體指令。

在 Java 語言中,記憶體屏障通過 volatile 關鍵字實現,禁止被它修飾的變數發生指令重排序操作:

1)不允許 volatile 欄位寫操作之前的記憶體訪問被重排序至其之後。

2)不允許 volatile 欄位讀操作之後的記憶體訪問被重排序至其之前。

    //  變數a,b通過volatile修飾
    private volatile int a, b;
    private int x, y;

    public void test() {
        Thread t1 = new Thread(() -> {
            a = 1;
            // 編譯器插入storeload記憶體屏障指令
            // 1)禁止代碼和指令重排序
            // 2)強制刷新變數a的最新值到記憶體
            x = b;
            // 1)強制從記憶體中讀取變數b的最新值
        });

        Thread t2 = new Thread(() -> {
            b = 2;
            // 編譯器插入storeload記憶體屏障指令
            // 1)禁止代碼和指令重排序
            // 2)強制刷新變數b的最新值到記憶體
            y = a;
            // 1)強制從記憶體中讀取變數a的最新值
        });
        // ...start啟動線程,join等待線程
        assert x == 2;
        assert y == 1;
    }

可以看到通過 volatile 修飾的變數通過 LOCK 指令和記憶體屏障,實現共用變數的可見性和避免代碼和指令的重排序,最終保障了程式在多線程情況下的正常執行。

 

併發編程問題-原子性

    private int count = 0;

    public void test() {
        List<Thread> ts = new ArrayList<>();
        for (int i = 0; i < 100; i++) {
            Thread t = new Thread(() -> {
                for (int j = 0; j < 10000; j++) {
                    count += 1;
                }
            });
            ts.add(t);
        }
        // ...start啟動線程,join等待線程
        assert count == 100 * 10000;
    }

對於 Java 這樣的高級語言,一條語句最終會被轉換成多條 CPU 指令完成,例如上面代碼中的 count+=1,至少需要三條 CPU 指令:

1)指令 1:把變數 count 從記憶體載入到 CPU 的寄存器;

2)指令 2:在寄存器中執行+1 操作;

3)指令 3:將結果寫入記憶體(緩存機制導致可能寫入的是處理器緩存而不是記憶體)。

那麼假設有兩個線程 A 和 B,同時執行 count+=1,可能存在如下情況:

1)線程 A 從記憶體載入 count 並執行 count+=1,同時線程 B 從記憶體載入 count 並執行 count+=1,並同時寫回記憶體。那麼這時結果是:count = 1。

2)線程 A 從記憶體載入 count 並執行 count+=1,並將 count 結果寫回記憶體。線程 B 再從記憶體載入 count 並執行 count+=1。那麼這時結果是:count = 2。

可以看到如果要 count 結果正確,要保證 count 讀取,操作,寫入三個過程不被中斷。這個過程,可以稱之為原子操作。原子 (atomic)本意是“不能被進一步分割的最小粒子”,而原子操作 (atomicoperation) 意為“不可被中斷的一個或一系列操作”。

處理器主要使用基於對緩存加鎖或匯流排加鎖的方式來實現原子操作:

1)匯流排加鎖:通過 LOCK#信號鎖住匯流排 BUS,使當前處理器獨享記憶體空間。但是此時其他處理器都不能訪問記憶體其他地址,效率低。

圖片

2)緩存加鎖:緩存一致性協議(MESI)。強制當前處理器緩存行失效,並從記憶體讀取其他處理器回寫的數據。當有些無法被緩存或跨域多個緩存行的數據,依然需要使用匯流排鎖。

圖片

對於以上兩個機制,處理器底層提供了很多指令來實現,其中最重要的是 CMPXCHG 指令。但 CMPXCHG 只在單核處理器下有效,多核處理器依然要加上 LOCK 首碼(LOCK CMPXCHG)。利用 CMPXCHG 指令可以通過迴圈 CAS 方式來實現原子操作。

// 判斷當前機器是否是多核處理器
int mp = os::is MP();
_asm {
    mov edx, dest
    mov ecx, exchange value
    mov eax, compare_value
    // 這裡需要先進行判斷是否為多核處理器
    LOCK IF MP(mp)
    // 如果是多核處理器就會在這行指令前加Lock標記
    cmpxchg dword ptr [edx],ecx
}

CAS 即 Compare and Swap。CAS 操作需要輸入兩個數值,一個舊值 (期望操作前的值)和一個新值,在操作期間先比較舊值有沒有發生變化,如果沒有發生變化,才交換成新值,發生了變化則不交換。

Java 語言提供了大量的原子操作類,來實現對應的 CAS 操作。比如 AtomicBoolean,AtomicInteger,AtomicLong 等。

    private AtomicInteger count = new AtomicInteger(0);

    public void test() {
        List<Thread> ts = new ArrayList<>();
        for (int i = 0; i < 100; i++) {
            Thread t = new Thread(() -> {
                for (int j = 0; j < 10000; j++) {
                   count.incrementAndGet();
                }
            });
            ts.add(t);
        }
        // ...start啟動線程,join等待線程
        assert count.get() == 100 * 10000;
    }

CAS 雖然很高效解決了原子操作,但是 CAS 也存在一些問題,比如 ABA 問題,迴圈時間長開銷大,只能保障一個共用變數的原子操作。關於如上問題解決和 Atomic 包介紹,會在下篇文章詳細闡述。

 

記憶體模型與 happens-before 關係

前面說到處理器提供了一些特殊指令比如 LOCK,CMPXCHG,記憶體屏障等來保障多線程情況下的程式邏輯正常執行。但這依然存在幾個問題:

1)處理器底層指令實現細節複雜難懂,開發人員需要付出巨大的學習成本。

2)不同的硬體和操作系統,對指令的支持和實現不一樣,需要考慮跨平臺的相容性。

3)程式業務邏輯複雜多變,處理器和線程之間的數據操作依賴關係也相應更複雜。

因此高級語言會提供一種抽象的記憶體模型,用於描述多線程環境下的記憶體訪問行為。無需關心底層硬體和操作系統的具體實現細節,就可以編寫出高效、可移植的併發程式。對於 Java 語言,這種記憶體模型便是 Java 記憶體模型(Java Memory Model,簡稱 JMM)。

Java 記憶體模型主要特性是提供了 volatile、synchronized、final 等同步原語,用於實現原子性、可見性和有序性。另一個重要的概念便是 happens-before 關係,用來描述併發編程中操作之間的偏序關係。除了 Java 語言,包括 golang,c++,rust 等高級語言也實現了自己的 happens-before 關係。

Java 記憶體模型的 happens-before 關係是用來描述兩個線程操作的記憶體可見性。需註意這裡的可見性,並不代表 A 線程的執行順序一定早於 B 線程, 而是 A 線程對某個共用變數的操作對 B 線程可見。即 A 線程對變數 a 進行寫操作,B 線程讀取到是變數 a 的變更值。

Java 記憶體模型定義了主記憶體(main memory),本地記憶體(local memory),共用變數等抽象關係,來決定共用變數在多線程之間通信同步方式,即前面所說兩個線程操作的記憶體可見性。其中本地記憶體,涵蓋了緩存,寫緩衝區,寄存器以及其他硬體和編譯器優化等概念。

圖片

如圖所示,如果線程 A 與線程 B 之間要通信的話,必須要經歷下麵 2 個步驟:

1)線程 A 把本地記憶體 A 中更新過的共用變數刷新到主記憶體中

2)線程 B 到主記憶體中去讀取線程 A 之前已更新過的共用變數

儘管定義這樣的數據通信方式,但實際程式的數據依賴操作是複雜多變的。為了進一步抽象這種線程間的數據同步方式,Java 記憶體模型定義了下述線程間的 happens-before 關係:

1)程式順序規則:單線程內,每個操作 happens-before 於該線程中的任意後續操作。

2)監視器鎖規則:解鎖操作 happens-before 之後對同一把鎖的加鎖操作。

3)volatile 變數規則:volatile 欄位的寫操作 happens-before 之後對同一欄位的讀操作。

4)傳遞性規則:如果 A happens-before B,且 B happens-before C,那麼 A happens-before C。

5)start()規則:如果線程 A 執行操作 ThreadB.start(),那麼 A 線程的 ThreadB.start()操作 happens-before 於線程 B 中的任意操作。

6)join()規則:如果線程 A 執行操作 ThreadB.join()併成功返回,那麼線程 B 中的任意操作 happens-before 線程 A 從 ThreadB.join()操作成功返回。

與開發人員密切相關的是 1、2、3、4 四個規則。其中規則 1 滿足了 as-if-serial 語義,即 Java 記憶體模型允許代碼和指令重排序,只要不影響程式執行結果。規則 2 和 3 是通過 synchronized、volatile 關鍵字實現。結合規則 1、2、3 來看看規則 4 的具體使用,可以看到如下的代碼,程式最終執行且得到正確結果:

   // x, y, z被volatile關鍵字修飾
    private volatile int x, y, z;

    public void test() {
        Thread a = new Thread(() -> {
            // 基於程式順序規則
            // 沒有數據依賴關係,可以重排序下麵代碼
            int i = 0;
            x = 2;
            // 基於volatile變數規則
            // 編譯器插入storeload記憶體屏障指令
            // 1)禁止代碼和指令重排序
            // 2)強制刷新變數x的最新值到記憶體
        });

        Thread b = new Thread(() -> {
            int i = 0;
            // 存在數據依賴關係,無法重排序下麵代碼
            // 強制從主記憶體中讀取變數x的最新值
            y = x;
            // 基於volatile變數規則
            // 編譯器插入storeload記憶體屏障指令
            // 1)禁止代碼和指令重排序
            // 2)強制刷新變數y的最新值到記憶體
            // 3)y = x;可能會被編譯優化去除
            y = 3;
            // 編譯器插入storeload記憶體屏障指令
            // 1)禁止代碼和指令重排序
            // 2)強制刷新變數y的最新值到記憶體
        });

        Thread c = new Thread(() -> {
            // 基於程式順序規則
            // 沒有數據依賴關係,可以重排序下麵代碼
            int i = 0;
            // 基於volatile變數規則
            // 強制從主記憶體中讀取變數x和y的最新值
            z = x * y;
            // 編譯器插入storeload記憶體屏障指令
            // 1)禁止代碼和指令重排序
            // 2)強制刷新變數z的最新值到記憶體
        });
        // ...start啟動線程,join等待線程
        assert z == 6;
        // 可以看到a線程對變數x變更,b線程對變數y變更,最終對線程c可見
        // 即滿足傳遞性規則
    }
  private  int x, y, z;

    public void test() {
        Thread a = new Thread(() -> {
           // synchronized,同步原語,程式邏輯將順序串列執行
            synchronized (this){
                // 基於程式順序規則
                // 沒有數據依賴關係,可以重排序下麵代碼
                int i = 0;
                x = 2;
                // 基於監視器鎖規則
                // 強制刷新變數x的最新值到記憶體
            }
        });

        Thread b = new Thread(() -> {
           // synchronized,同步原語,程式邏輯將順序串列執行
            synchronized (this) {
                int i = 0;
                // 存在數據依賴關係,無法重排序下麵代碼
                // 強制從主記憶體中讀取變數x的最新值
                y = x;
                // 基於監視器鎖規則
                // 1)強制刷新變數y的最新值到記憶體
                // 2)y = x;可能會被編譯優化去除
                y = 3;
                // 強制刷新變數y的最新值到記憶體
            }
        });

        Thread c = new Thread(() -> {
           // synchronized,同步原語,程式邏輯將順序串列執行
            synchronized (this) {
                // 基於程式順序規則
                // 沒有數據依賴關係,可以重排序下麵代碼
                int i = 0;
                // 基於監視器鎖規則
                // 強制從主記憶體中讀取變數x和y的最新值
                z = x * y;
                // 強制刷新變數z的最新值到記憶體
            }
        });
        // ...start啟動線程,join等待線程
        assert z == 6;
        // 可以看到a線程對變數x變更,b線程對變數y變更,最終對線程c可見
        // 即滿足傳遞性規則
    }

記憶體模型綜述

在本文中,我們對 Java 記憶體模型進行了全面的概述。Java 記憶體模型是 Java 虛擬機規範的一部分,為 Java 開發人員提供了一種抽象的記憶體模型,用於描述多線程環境下的記憶體訪問行為。

jJava 記憶體模型關註併發編程中的原子性、可見性和有序性問題,並提供了一系列同步原語(如 volatile、synchronized 等)來實現這些原則。此外,還定義 happens-before 關係,用於描述操作之間的偏序關係,從而確保記憶體訪問的正確性和一致性。

Java 記憶體模型的主要優勢在於它為併發編程提供了基礎,簡化了複雜性。屏蔽不同處理器差異性,在不同的處理器平臺之上呈現了一致的記憶體模型,並允許一定程度的性能優化。這些優勢使得 Java 開發人員可以更容易地編寫出正確、高效、可移植的併發程式。

瞭解 Java 記憶體模型的原理和實踐對於編寫高質量的 Java 併發程式至關重要。希望本文能為您提供有關 Java 記憶體模型的有用信息,幫助您更好地理解併發編程的內在機制,以及在實際項目中選擇合適的同步原語和策略。

 

作者:ehtan

本文來自博客園,作者:古道輕風,轉載請註明原文鏈接:https://www.cnblogs.com/88223100/p/Deeply-Understanding-the-Memory-Model-of-Concurrent-Programming-Art.html


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

-Advertisement-
Play Games
更多相關文章
  • ## windows程式圖標緩存 在vs中替換c++程式的圖標後,需要重新編譯,但是很多情況下都不會刷新,還是看到老的圖標,只能重啟電腦才能看到新的圖標。 通過ChatGPT得到相關的回答如下: 如果在 Windows 上更換了可執行文件 (.exe) 的圖標,但是在圖標文件已經更改的情況下仍然顯示 ...
  • 進程函數 簡單描述一下關於進程的函數,主要有fork調用、exec調用、exit調用、wait調用和sleep調用。 fork調用 所需頭文件 #include <unistd.h> //標準函數庫 #include <sys/types.h> //提供系統調用的標誌 函數原型 pid_t fork ...
  • 海納思全稱是海思機頂盒NAS系統, 網站 https://www.ecoo.top/. 他們做的固件在 N2 NS-1 上應該是集成度做的最好的一個, 包含的功能很豐富, 除了常見的 Aria2, Transmission, Samba, 還集成了命令行, 網盤和筆記等. 適合想直接刷機使用, 不喜... ...
  • 一.前言 從上個世紀到現在,工程師們在優化伺服器性能的過程中,提出了各種不同的io模型,比如非阻塞io,io復用,信號驅動式io,非同步io。具體io模型在不同平臺上的實現也不一樣,比如io復用在bsd上可以由kqueue實現,在solaris系統上可以由/dev/poll實現。為了實現系統的可移植性 ...
  • 分庫分表是大型互聯網應用經常採用的一種數據層優化方案,常見的分庫分表中間件如 sharding-jdbc、mycat 都已經比較成熟,基本上可以應對我們一般的分庫分表需求。 做過分庫分表的同學應該知道,在給業務系統做分庫分表改造過程中,難的不是如何使用這些組件進行分庫分表,而是如何將非分庫分表的系... ...
  • 因歷史遺留原因,接手的項目沒有代碼提醒/格式化,包括 eslint、pretttier,也沒有 commit 提交校驗,如 husky、commitlint、stylelint,與其期待自己或者同事的代碼寫得完美無缺,不如通過一些工具來進行規範和約束。 ### eslint eslint 是一個代碼 ...
  • # 引言 這幾天幫朋友忙,用了一周時間,高仿了一個釘釘審批流。這個東西會有不少朋友有類似需求,就分享出來,希望能有所幫助。為了方便朋友的使用,設計製作的時候,儘量做到節點配置可定製,減少集成成本。如果您的項目有審批流需求,這個項目可以直接拿過去使用。React初學者也可以把本項目當做研讀案例,學習並 ...
  • ![](https://img2023.cnblogs.com/blog/3076680/202308/3076680-20230804112759115-773698620.png) # 1. 配置服務 ## 1.1. 配置服務本身就是分散式資料庫 ### 1.1.1. 像ZooKeeper和et ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...