Fork/Join框架簡介

来源:https://www.cnblogs.com/gocode/archive/2020/03/21/introduction-to-fork_join-framework.html
-Advertisement-
Play Games

1 引子 Fork/Join框架是從Java1.7開始提供的一個並行處理任務的框架(本篇博客基於JDK1.8分析),它的基本思路是將一個大任務分解成若幹個小任務,並行處理多個小任務,最後再彙總合併這些小任務的結果便可得到原來的大任務結果。 從字面意思來理解Fork/Join框架,"Fork"表示“分 ...


1 引子

Fork/Join框架是從Java1.7開始提供的一個並行處理任務的框架(本篇博客基於JDK1.8分析),它的基本思路是將一個大任務分解成若幹個小任務,並行處理多個小任務,最後再彙總合併這些小任務的結果便可得到原來的大任務結果。

從字面意思來理解Fork/Join框架,"Fork"表示“分叉”,它把大任務分解成多個小任務並行處理,而“Join”表示“聯合”,它將這些小任務的結果合併在一起,最終得到大任務的結果。比如計算累加1+2+3+...+100000,可分解成100個子任務,每個子任務只對1000個自然數求和,最終合併這100個子任務的結果。

 Fork/Join框架會用到工作竊取(work-stealing)演算法,那麼什麼是工作竊取演算法呢?

工作竊取(work-stealing)演算法是指某個線程從其他隊列里竊取任務來執行。使用工作竊取演算法的主要目的是為了併發提高性能、提高執行效率,減少無效的等待。當我們需要處理一個大任務時,我們會將這個任務分解成多個相對獨立的子任務,為了減少線程競爭,我們將這些子任務放入到不同的隊列中,分別為每個隊列新建一個線程,一個線程消費一個隊列。在處理任務過程中,操作系統可能給有某些線程分配的時間片較多或某些線程所處理任務的工作量較小,它們會先一步將自己所對應隊列中的任務處理完了,而此時其他線程卻還沒處理完自己所屬的任務。此時執行快的線程可以到其他線程對應隊列中去“竊取”任務來處理,這樣就避免了無意義的乾等。此時就會出現多線程訪問同一個隊列,為了避免線程競爭,一般會使用雙端隊列,被竊取任務線程永遠從雙端隊列的頭部取出任務執行,而竊取任務的線程永遠從雙端隊列的尾部“竊取”任務執行。

 工作竊取演算法的優缺點

優點:充分利用線程進行並行計算,減少了線程間的競爭

缺點:在某些情況下還是存在競爭,比如雙端隊列里只有一個任務時,並且該演算法會消耗了更多的系統資源,如創建多個線程和多個雙端隊列。

2 使用Fork/Join框架

1) 步驟

如何使用“Fork/Join框架”?其實這還是比較簡單的,正如其名字一樣,先"Fork"分解任務,再“Join”合併結果。

第一步,切分任務 。首先我們需要有一個fork類來把大任務分割成子任務,有可能子任務還是很大,所以還需要不停地割,直到分割出的子任務足夠小。

第二步,執行任務併合並結果 。分割的子任務分別放在雙端隊列里,然後幾個啟動線程分別從雙端隊列里獲取任務執行。子任務執行完的結果都統一放在一個隊列里,啟動一個線程從隊列里拿數據,然後合併這些數據。

Fork/Join框架要使用兩個類來完成上面的兩個步驟。

①ForkJoinTask : 使用Fork/Join框架,我們應當提供一個ForkJoinTask任務,若沒有任務,Fork/Join就無從談起。我它提供了fork()、join()這兩個API進行任務的分解、結果的合併,它有兩個主要的直接子類,我們一般繼承這個兩個子類。ForkJoinTask是實現Future的抽象類,之前在FutureTask源碼完整解讀中有過對Future的介紹,它表示一個非同步任務的結果。

  • RecursiveTask :表示有返回結果的任務

  • RecursiveAction : 表示沒有結果的任務

②ForkJoinPool : ForkJoinTask任務需要執行器ForkJoinPool來執行。ForkJoinPool是ExecutorService的實現類,它代表一個可執行任務的執行器。

 

 任務分割出的子任務會添加到當前工作線程所維護的雙端隊列中,進入隊列的頭部。當一個工作線程的隊列里暫時沒有任務時,它會隨機從其他工作線程的隊列的尾部獲取一個任務。

2) 用例

實現需求:求出1+2+3+....1000的結果。這裡的任務需要有結果,應該選擇使用RecursiveTask任務 。另外我們需要考慮如何分割任務,此時我計劃讓每個子任務執行100個數的相加,因此將分割的閥值設為100,那麼Fork/Join框架會將這個主任務分割成10個子任務,最終將10個子任務的結果合併在一起。

package juc;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.Future;
import java.util.concurrent.RecursiveTask;

public class ForkJoinDemo {
    public static void main(String[] args) {
        AccumulationTask task = new AccumulationTask(0, 1000);
        ForkJoinPool forkJoinPool = new ForkJoinPool();
        Future<Integer> future = forkJoinPool.submit(task);
        try {
            long start = System.currentTimeMillis();
            int r = future.get();
            System.out.println("執行‘0+1+2+3+...+1000'計算用時" + (System.currentTimeMillis() - start) + "毫秒,其結果是:" + r);
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }

    }
    static class AccumulationTask extends RecursiveTask<Integer> {
        private static final int FORK_THRESHOLD = 100;
        private final int start;
        private final int end;
        public AccumulationTask(int start, int end) {
            this.start = start;
            this.end = end;
        }
        @Override
        protected Integer compute() {
            boolean isContinueFork = (end - start) > FORK_THRESHOLD;
            int sum = 0;
            if (isContinueFork) {   //大於閥值需要繼續分割任務
                //二分法,分成左右兩段
                int m = (start + end) / 2;
                AccumulationTask leftTask = new AccumulationTask(start, m);
                AccumulationTask rightTask = new AccumulationTask(m + 1, end);
                //fork方法:分別執行左右兩段任務(若任務不夠小,將遞歸調用compute)
                leftTask.fork();
                rightTask.fork();
                //等待左右兩段任務執行完,再獲取子任務的結果
                int leftResult = leftTask.join();
                int rightResult = rightTask.join();
                sum = leftResult + rightResult;
            } else {//任務足夠小了,可以直接計算
                for (int i = this.start; i <= this.end; i++) {
                    sum += i;
                }
            }
            return sum;
        }
    }
}

從上面的示例可看出使用Fork/Join框架的關鍵在於實現compute方法 。在compute方法中,我們首先要確定任務是否需要繼續分割,如果任務足夠小、滿足預先設定的閥值就可直接執行任務。如果任務仍然很大,就必須繼續分割成兩個子任務,每個子任務在調用fork方法時,又會進入compute方法,看看當前子任務是否需要繼續分割成子任務,如果不需要繼續分割,則執行當前子任務並返回結果,使用join方法會等待子任務執行完並得到其結果。

3 Fork/Join框架的異常處理

ForkJoinTask任務在執行過程中可能會(在執行自身任務的線程中)拋出異常,我們無法在主線程中直接捕獲異常,但ForkJoinTask提供了isCompletedAbnormally方法來判定任務是否拋出過異常或任務被取消(isCompletedNormally方法返回任務是否正常完成的布爾值)。另外ForkJoinTask還提供了getException方法來獲取異常。

getException返回Throwable對象,如果任務被取消了就返回CancellationException,如果任務未完成或未拋出異常就返回null 。

  簡單地獲取異常

if (leftTask.isCompletedAbnormally()) {
    System.out.println(leftTask.getException());
}

   捕獲異常修改原來的用例

public class ForkJoinDemo {
    public static void main(String[] args) {
        AccumulationTask task = new AccumulationTask(0, 1000);
        if(task.isCompletedAbnormally()){
            System.out.println(task.getException());
        }
        ForkJoinPool forkJoinPool = new ForkJoinPool();
        Future<Integer> future = forkJoinPool.submit(task);
        try {
            long start = System.currentTimeMillis();
            int r = future.get();
          System.out.println("執行‘0+1+2+3+...+1000'計算用時" + (System.currentTimeMillis() - start) + "毫秒,其結果是:" + r);
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
    }

    static class AccumulationTask extends RecursiveTask<Integer> {
        private static final int FORK_THRESHOLD = 100;
        private final int start;
        private final int end;

        public AccumulationTask(int start, int end) {
            this.start = start;
            this.end = end;
        }

        @Override
        protected Integer compute() {
            boolean isContinueFork = (end - start) > FORK_THRESHOLD;
            int sum = 0;
            if (isContinueFork) {   //大於閥值進需要繼續分割任務
                //二分法,分成左右兩段
                int m = (start + end) / 2;
                AccumulationTask leftTask = new AccumulationTask(start, m);
                AccumulationTask rightTask = new AccumulationTask(m + 1, end);
                //fork方法:分別執行左右兩段任務(若任務不夠小,將遞歸調用compute)
                leftTask.fork();
                rightTask.fork();
                //等待左右兩段任務執行完,再獲取子任務的結果
                int leftResult = leftTask.join();
                int rightResult = rightTask.join();
                sum = leftResult + rightResult;
            } else {
                //任務足夠小了,可以直接計算
                    for (int i = this.start; i <= this.end; i++) {
                        sum += i;
                        if (i == 999) throw new IllegalStateException();
                    }
            }
            return sum;
        }
    }
}
捕獲異常後

4 Fork/Join框架的實現原理

ForkJoinPool中有一個重要的成員變數workQueues ,它是靜態內部類WorkQueue類型的數組。WorkQueue類中有一個ForkJoinTask類型數組array和一個ForkJoinWorkerThread成員變數owner, array數組負責將存放程式提交給ForkJoinPool的任務,而owner負責執行當前WorkQueue中的任務。

static final class WorkQueue{
    // Instance fields
    volatile int scanState;    // versioned, <0: inactive; odd:scanning
    int stackPred;             // pool stack (ctl) predecessor
    int nsteals;               // number of steals
    int hint;                  // randomization and stealer index hint
    int config;                // pool index and mode
    volatile int qlock;        // 1: locked, < 0: terminate; else 0
    volatile int base;         // index of next slot for poll
    int top;                   // index of next slot for push
    ForkJoinTask<?>[] array;   // the elements (initially unallocated)
    final ForkJoinPool pool;   // the containing pool (may be null)
    final ForkJoinWorkerThread owner; // owning thread or null if shared
    volatile Thread parker;    // == owner during call to park; else null
    volatile ForkJoinTask<?> currentJoin;  // task being joined in awaitJoin
    volatile ForkJoinTask<?> currentSteal; // mainly used by helpStealer
    //...省略
  }

1) ForkJoinTask的fork方法實現原理

當我們調用ForkJoinTask的fork方法時,程式首先判斷當前線程的類型,若是ForkJoinWorkerThread線程,它會調用ForkJoinWorkerThread的pushTask方法務提交到當前線程t的workQueue對應的隊列中;若是普通線程,它調用externalPush()方法將任務提交到隊列中;最後返回這任務本身

public final ForkJoinTask<V> fork() {
    Thread t;
    if ((t = Thread.currentThread()) instanceof ForkJoinWorkerThread)
        ((ForkJoinWorkerThread)t).workQueue.push(this);
    else
        ForkJoinPool.common.externalPush(this);
    return this;
}

push方法是ForkJoinPool的靜態內部類WorkQueue的成員方法,它的基本邏輯是: 將當前任務放入到ForkJoinTask類型數組array中,然後調用signalWork執行任務,若數組容量不夠還將調用growArray對數組array擴容。

final void push(ForkJoinTask<?> task) {
    ForkJoinTask<?>[] a; ForkJoinPool p;
    int b = base, s = top, n;
    if ((a = array) != null) {    // ignore if queue removed
        int m = a.length - 1;     // fenced write for task visibility
        U.putOrderedObject(a, ((m & s) << ASHIFT) + ABASE, task);//將task放入到成員變數ForkJoinTask類型數組array中
        U.putOrderedInt(this, QTOP, s + 1);//更新下次入隊位置的索引
        if ((n = s - b) <= 1) {
            if ((p = pool) != null)
                //隊列中最多只有一個任務了,可以喚醒一個線程或創建一個新線程來執行任務
                p.signalWork(p.workQueues, this);
        }
        else if (n >= m)//數組array容量不夠,需要擴容
            growArray();
    }
}

而ForkJoinPool的成員方法externalPush的基本邏輯與上面的push方法有些類似,但也有些不同:先確定WorkQueue的槽位,再將當前任務放到WorkQueue的成員變數ForkJoinTask數組array中,再調用signalWork執行任務。若workQueues是空的,將調用externalSubmit來初始化workQueues及相關屬性。

final void externalPush(ForkJoinTask<?> task) {
    WorkQueue[] ws; WorkQueue q; int m;
    int r = ThreadLocalRandom.getProbe();//探針值,用於計算q在workQueues中的索引槽位
    int rs = runState; //運行狀態
    if ((ws = workQueues) != null && (m = (ws.length - 1)) >= 0 &&  //workQueues非空,且workQueues可放入任務(長度大於1)
        //與HashMap類似,m&r是用來確定數組的索引(取餘,這裡的r相當於HashMap中Node的hash屬性),
        //SQMASK=Ob1111110,(SQMASK十進位為126,而126<64*2)它的作用是只用workQueues的64個槽位,
        //而SQMASK的二進位最低位為0,又相當於強制將"m & r'的最低位設為0(二進位最低位為零時表示偶數),
        //因此"m & r & SQMASK"的結果是小於等於64的偶數。
        (q = ws[m & r & SQMASK]) != null && r != 0 && rs > 0 &&
        U.compareAndSwapInt(q, QLOCK, 0, 1)) {  //鎖定q,這裡CAS更新成功後,q.qlock為1,其他線程就不能CAS更新q.qlock了
        ForkJoinTask<?>[] a; int am, n, s;
        if ((a = q.array) != null &&
            (am = a.length - 1) > (n = (s = q.top) - q.base)) {
            int j = ((am & s) << ASHIFT) + ABASE;
            U.putOrderedObject(a, j, task);//將task放入到成員變數ForkJoinTask類型數組array中
            U.putOrderedInt(q, QTOP, s + 1);//更新下次入隊位置的索引
            U.putIntVolatile(q, QLOCK, 0);//無條件更新q.qlock,解除對q的鎖定
            if (n <= 1)  //隊列中最多只有一個任務了,可以喚醒一個線程或創建一個新線程來執行任務
                signalWork(ws, q);
            return;
        }
        U.compareAndSwapInt(q, QLOCK, 1, 0);//q.array無法容納新任務時,也要解除對q的鎖定
    }
    //完整版本的externalPush,可處理不常見的情況,併在向ForkJoinPoll中首次提交第一個任務時執行輔助初始化。 
    //它還會檢測外部線程的首次提交,如果索引處的WorkQueue為空或存線上程競爭,則會創建一個新的共用隊列。
    externalSubmit(task);// workQueues是空的,需要初始化workQueues及相關屬性,並提交任務
}

2) ForkJoinTask的join方法實現原理

Join方法的主要作用是阻塞當前線程並等待獲取結果。讓我們一起看看ForkJoinTask的join方法的實現:

首先調用doJoin方法來執行並獲取任務運行狀態,若是非正常完成,就報告異常,若正常完成就返回結果。

public final V join() {
    int s;
    if ((s = doJoin() & DONE_MASK) != NORMAL)
        reportException(s);
    return getRawResult();
}

ForkJoinTask使用成員變數state表示任務狀態,它可能有四種狀態,已完成(NORMAL)、被取消(CANCELLED)、等待信號(SIGNAL)和出現異常(EXCEPTIONAL)。DONE_MASK是表示任務狀態的位(用來取state二進位形式的最高4位),SMASK任務的標簽數的掩碼(可調用compareAndSetForkJoinTaskTag將state置為1)。

    static final int DONE_MASK   = 0xf0000000;  // mask out non-completion bits 
    static final int NORMAL      = 0xf0000000;  // must be negative
    static final int CANCELLED   = 0xc0000000;  // must be < NORMAL
    static final int EXCEPTIONAL = 0x80000000;  // must be < CANCELLED
    static final int SIGNAL      = 0x00010000;  // must be >= 1 << 16
    static final int SMASK       = 0x0000ffff;  // short bits for tags

若任務狀態是已完成,則可直接返回其結果;若任務被取消,則拋出CancellationException異常;若執行任務過程中拋出過異常,則直接拋出該異常。

private void reportException(int s) {
    if (s == CANCELLED)
        throw new CancellationException();
    if (s == EXCEPTIONAL)
        rethrow(getThrowableException());
}
public final Void getRawResult() { return null; }//RecursiveAction不需要結果,返回null
public final V getRawResult() {//RecursiveAction
    //RecursiveAction在執行exec方法時主要執行"result = compute();"代碼,將計算結果賦值給成員result
    return result;
}

我們再來看看doJoin()方法是怎麼做的。doJoin()方法使用了過多的三元運算符,不太容易理解,下麵我將三元運算替換成if-else。將方法“翻譯”後可以很容易地看出doJoin方法的主要邏輯:首先需要查看任務的狀態,若任務已完成(可能是任務取消或拋出異常等非正常完成),則直接返回任務狀態;若任務還沒執行完,則從工作隊列中取出並執行此任務。若此任務能立刻執行完成(可能是任務取消或拋出異常等非正常完成)就返回此狀態,反之就調用ForkJoinPool.awaitJoin等待任務執行完成。

    private int doJoin() {
        int s; Thread t; ForkJoinWorkerThread wt; ForkJoinPool.WorkQueue w;
        return (s = status) < 0 ? s :
                ((t = Thread.currentThread()) instanceof ForkJoinWorkerThread) ?
                        (w = (wt = (ForkJoinWorkerThread)t).workQueue).
                                tryUnpush(this) && (s = doExec()) < 0 ? s :
                                wt.pool.awaitJoin(w, this, 0L) :
                        externalAwaitDone();
    }
    //翻譯後的doJoin方法
    private int doJoin() {
        int s;Thread t; ForkJoinWorkerThread wt; ForkJoinPool.WorkQueue w;
        if ((s = status) < 0) {//任務已完成,直接返回state
            return s;
        } else {
            if ((t = Thread.currentThread()) instanceof ForkJoinWorkerThread) { //當前線程是ForkJoinWorkerThread線程時
                wt = (ForkJoinWorkerThread) t;
                w = wt.workQueue;
                //tryUnpush取出這個任務
                //doExec準備執行exec方法(exec又調用compute方法),並(若完成)記錄狀態,但是doExce方法不會等待任務執行完成
                if (w.tryUnpush(this) && (s = doExec()) < 0){ 
                    return s;
                }else{
                    return  wt.pool.awaitJoin(w, this, 0L)//等待任務執行完成
                }
            }else{
                return externalAwaitDone();//當前線程是普通線程,調用externalAwaitDone阻塞當前線程,等待任務完成
            }
        }
    }

參考:《Java併發編程的藝術》方騰飛

 


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

-Advertisement-
Play Games
更多相關文章
  • 子類在覆蓋父類時,如果父類的方法拋出異常,那麼子類的覆蓋方法,只能拋出父類的異常或者該異常的子類,或者不拋。 如果父類方法拋出多個異常,那麼子類在覆蓋該方法時,只能拋出父類異常的子集。 如果父類或者介面的方法中沒有異常拋出,那麼子類在覆蓋方法時,也不可以拋出異常,如果子類方法發生異常,一定要進行tr ...
  • python爬蟲-UnicodeDecodeError: 'utf-8' codec can't decode byte 0x8b in position 1: invalid start byte ...
  • 異常的基本處理方式、異常的重解釋(在 catch 語句塊中將捕獲到的異常再次拋出)、自定義/標準庫異常類的使用(註意:賦值相容性原則)、異常的另類寫法(try...catch...)、異常函數聲明的方式(通過 throw 關鍵字指定可拋出的具體異常類型) ...
  • 下載 第一步:打開官網:http://www.jetbrains.com/idea/,點擊頁面中的“DOWNLOAD” 第二步:根據自己的需要選擇下載的IntelliJ IDEA版本。Community(社區版)是開源免費版本,但是功能也相對減少,旗艦版需要激活,但是功能更多。此處我的電腦是Wind ...
  • 目錄 寫在最前 準備工作 Jdk下載 Eclipse下載 tomcat下載 安裝 Jdk安裝 環境配置 Tomcat安裝 環境配置 Eclipse安裝 Eclipse配置tomcat環境 Hello world 轉換成Java 寫在最前 本篇文章僅供參考,希望大家在通讀全文後對過程有一定瞭解,再進行 ...
  • 【原創】Java併發編程系列2:線程概念與基礎操作 偉大的理想只有經過忘我的鬥爭和犧牲才能勝利實現。 本篇為【Dali王的技術博客】Java併發編程系列第二篇,講講有關線程的那些事兒。主要內容是如下這些: 線程概念 線程基礎操作 線程概念 進程代表了運行中的程式,一個運行的Java程式就是一個進程。 ...
  • BigDecimal介紹及BigDecimal實現四捨五入 1. BigDecimal是什麼? 我們知道float最大精度是7 8位有效數字,而double的最大精度是16 17位有效數字,那麼大於16位的我們怎麼來表示呢?這就需要用到BigDecimal,用來對超過16位有效位的數進行精確的運算。 ...
  • 100個不同類型的python語言趣味編程題 在求解的過程中培養編程興趣,拓展編程思維,提高編程能力。 第一部分:趣味演算法入門;第八題: 8.冒泡排序:對N個整數(數據由鍵盤輸入)進行升序排序 冒泡排序的思想:首先從表頭開始往後掃描數組,在掃描的過程中逐對比較相鄰的倆個元素的大小。若相鄰的兩個元素中 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...