FutureTask源碼完整解讀

来源:https://www.cnblogs.com/gocode/archive/2020/03/13/analysis-source-code-of-FutureTask.html
-Advertisement-
Play Games

1 簡介 上一篇博客“非同步任務服務簡介”對FutureTask做過簡要介紹與分析,這裡再次對FutureTask做一次深入的分析(基於JDK1.8)。 FutureTask同時實現了Future 、Runnable介面,因此它可以交給執行器Executor去執行這個任務,也可以由調用線程直接執行ru ...


1 簡介

上一篇博客“非同步任務服務簡介”對FutureTask做過簡要介紹與分析,這裡再次對FutureTask做一次深入的分析(基於JDK1.8)。

FutureTask同時實現了Future 、Runnable介面,因此它可以交給執行器Executor去執行這個任務,也可以由調用線程直接執行run方法。

根據FutureTask.run方法的執行狀態,可將其分為以下3種狀態

①未啟動: run方法還未被執行,FutureTask處於未啟動狀態。

②已啟動: run方法在執行過程中,FutureTask處於已啟動狀態

③已完成:run方法正常完成返回或被取消或執行過程中因異常拋出而非正常結束,FutureTask處於已完成狀態。

當FutureTask處於未啟動或已啟動狀態時,執行FutureTask.get()方法將導致調用線程阻塞;當FutureTask處於已完成狀態時,執行FutureTask.get()方法將導致調用線程立即返回結果或拋出異常。

當FutureTask處於未啟動狀態時,執行FutureTask.cancel()方法將導致此任務永遠不會被執行;當FutureTask處於已啟動狀態時,執行FutureTask.cancel(true)方法將以中斷執行此任務線程的方式來試圖停止任務;當FutureTask處於已啟動狀態時,執行 FutureTask.cancel(false)方法將不會對正在執行此任務的線程產生影響(讓正在執行的任務運行完成);當FutureTask處於已完成狀態時,執行FutureTask.cancel方法將返回false (已完成的任務任務無法取消)。

2 用法示例

FutureTask因其自身繼承於Runnable介面,因此它可以交給執行器Executor去執行;另外它也代表非同步任務結果,它還可以通過ExecutorService.submit返回一個FutureTask。另外FutureTask也可單獨使用。為了更好的理解FutureTask ,下麵結合ConcurrentHashMap演示一個任務緩存。緩存中有多個任務,使用多線程去執行這些任務,一個任務最多被一個線程消費,若多個線程試圖執行這一個任務,只允許一個線程來執行,其他線程必須等待它執行完成。

import java.util.concurrent.*;

public class FutureTaskTest {
    private final ConcurrentMap<String, Future<String>> taskCache = new ConcurrentHashMap<>();
    public  String executionTask(final String taskName)
            throws ExecutionException, InterruptedException {
        while (true) {
            Future<String> future = taskCache.get(taskName);// 從緩存中獲取任務
            if (future == null) {//不存在此任務,新構建一個任務放入緩存,並啟動這個任務
                Callable<String> task = () ->{
                    System.out.println("執行的任務名是"+taskName);
                    return taskName;
                } ; // 1.2創建任務
                FutureTask<String> futureTask = new FutureTask<String>(task);
                future = taskCache.putIfAbsent(taskName, futureTask);// 嘗試將任務放入緩存中
                if (future == null) {
                    future = futureTask;
                    futureTask.run();//執行任務
                }
            }
            try { //若任務在緩存中了,可以直接等待任務的完成
                return future.get();// 等待任務執行完成
            } catch (CancellationException e) {
                taskCache.remove(taskName, future);
            }
        }
    }

    public static void main(String[] args)    {
     final   FutureTaskTest taskTest = new FutureTaskTest();
        for (int i = 0; i < 7; i++) {
            int finalI = i;
            new Thread(()->{
                try {
                    taskTest.executionTask("taskName" + finalI);
                } catch (ExecutionException | InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
            new Thread(()->{
                try {
                    taskTest.executionTask("taskName" + finalI);
                    taskTest.executionTask("taskName" + finalI);
                } catch (ExecutionException | InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

列印輸出

執行的任務名是taskName0

執行的任務名是taskName5

執行的任務名是taskName4

執行的任務名是taskName6

執行的任務名是taskName3

執行的任務名是taskName1

執行的任務名是taskName2

3 實現原理

1) 成員變數

它有一個成員變數state表示狀態

private volatile int state;

它有這些可能取值

private static final int NEW          = 0;//剛開始的狀態或任務在運行中
private static final int COMPLETING   = 1;//臨時狀態,任務即將結束,正在設置結果
private static final int NORMAL       = 2;//任務正常完成
private static final int EXCEPTIONAL  = 3;//因拋出異常而結束任務
private static final int CANCELLED    = 4;//任務被取消
private static final int INTERRUPTING = 5;//任務正在被中斷
private static final int INTERRUPTED  = 6;//任務被中斷(中斷的最終狀態)

state可能有這幾種狀態轉換

/** NEW -> COMPLETING -> NORMAL      正常結束任務時的狀態轉換流程
     * NEW -> COMPLETING -> EXCEPTIONAL   任務執行過程中拋出了異常時的狀態轉換流程
     * NEW -> CANCELLED                 任務被取消時的狀態轉換流程
     * NEW -> INTERRUPTING -> INTERRUPTED  任務執行過程中出現中斷時的狀態轉換流程
     */

 

其他成員變數

private Callable<V> callable;
private Object outcome; // non-volatile, protected by state reads/writes
private volatile Thread runner;
private volatile WaitNode waiters;

成員變數callable表示要執行的任務,

成員變數outcome表示任務的結果或任務非正常結束的異常

成員變數runner表示執行此任務的線程

成員變數waiter表示等待任務執行結果的等待棧(數據結構是單向鏈表) 。WaitNode是一個簡單的靜態內部,一個成員變數thread表示等待結果的線程,另一個成員變數next表示下一個等待節點(線程)。

static final class WaitNode {
    volatile Thread thread;
    volatile WaitNode next;
    WaitNode() { thread = Thread.currentThread(); }
}

2) 構造方法

FutureTask的構造方法會初始化callable和state ,它有兩個構造方法, 分別接受Callable和Runnable類型的待執行任務。但對於Runnable類型參數,它會調用Executors.callable將Runnable轉換為Callable類型實例,以便於統一處理。

public FutureTask(Callable<V> callable) {
    if (callable == null)
        throw new NullPointerException();
    this.callable = callable;
    this.state = NEW;       // ensure visibility of callable
}
public FutureTask(Runnable runnable, V result) {
    this.callable = Executors.callable(runnable, result);
    this.state = NEW;       // ensure visibility of callable
}

Executors.callable方法也很簡單,它就返回了一個Callable的實現類RunnableAdapter類型的對象。

public static <T> Callable<T> callable(Runnable task, T result) {
    if (task == null)
        throw new NullPointerException();
    return new RunnableAdapter<T>(task, result);
}
static final class RunnableAdapter<T> implements Callable<T> {
    final Runnable task;
    final T result;
    RunnableAdapter(Runnable task, T result) {
        this.task = task;
        this.result = result;
    }
    public T call() {
        task.run();
        return result;
    }
}

3) 主要API

(1) run與runAndReset

run方法是Funture最重要的方法,FutureTask的一切都是從run方法開始的,它是執行callable任務的方法。

public void run() {
        if (state != NEW ||
                !UNSAFE.compareAndSwapObject(this, runnerOffset,
                        null, Thread.currentThread()))
            //將當前線程設置為執行任務的線程,CAS失敗就直接返回
            return;
        try {
            Callable<V> c = callable;
            if (c != null && state == NEW) {
                V result;
                boolean ran;
                try {
                    result = c.call();//執行任務
                    ran = true;
                } catch (Throwable ex) {
                    //運行時有異常,設置異常
                    result = null;
                    ran = false;
                    setException(ex);
                }
                if (ran)
                    set(result);//設置結果
            }
        } finally {
            // runner must be non-null until state is settled to
            // prevent concurrent calls to run()
            runner = null; //state已是最終狀態,不再變化,將runer設為null,防止run方法被併發調用
            // state must be re-read after nulling runner to prevent
            // leaked interrupts
            int s = state; //清空運行線程runner後再重新獲取state,防止遺漏掉對中斷的處理
            if (s >= INTERRUPTING)
                handlePossibleCancellationInterrupt(s);
        }
    }

其主要邏輯是:

①檢查狀態,設置運行任務的線程

②調用callable的call方法去執行任務,並捕獲運行中可能出現的異常

③如果任務正常完成,調用set設置任務的結果,將state設為NORMAL, 將結果保存到outcome ,喚醒所有等待結果的線程

④若執行任務過程中發生了異常,調用setException設置異常,將state設為EXCEPTIONAL ,將此異常也保存到outcome ,喚醒所有等待結果的線程

⑤最後將運行線程runner清空,若狀態可能是任務被取消的中斷還要處理此中斷。

 

set 、setException方法分別用來設置結果、設置異常,但這僅是它們的主要邏輯,它們還會進行其他的處理。

它們會將結果或異常設置到成員變數outcome上,還會更新狀態state,最後調用finishCompletion從等待棧表中移除並喚醒所有(節點)線程(任務已完成,無需要等待,可以直接獲取結果,等待棧已沒有存在的意義了)。

protected void setException(Throwable t) {
    if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
        outcome = t;
        UNSAFE.putOrderedInt(this, stateOffset, EXCEPTIONAL); // final state
        finishCompletion();
    }
}
protected void set(V v) {
    if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
        outcome = v;
        UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state
        finishCompletion();
    }
}

 

run方法中有對中斷的處理,我們來看看handlePossibleCancellationInterrupt方法怎麼處理中斷的.

這裡就是簡單地使當前線程讓出時間片,讓其他線程先執行任務,即線程禮讓。

private void handlePossibleCancellationInterrupt(int s) {
        if (s == INTERRUPTING)
            while (state == INTERRUPTING)
                Thread.yield(); // wait out pending interrupt
    }

 

runAndReset方法是FutureTask類自己添加的protected級別的方法(供子類調用), 這個方法主要用來執行可多次執行且不需要結果的任務,只有在任務運行和重設成功時才返回true 。定時任務執行器ScheduledThreadPoolExecutor的靜態內部ScheduledFutureTask的run方法調用了這個API.

和run方法相比,runAndSet方法與之邏輯大致相同,只是runAndSet沒用調用set方法設置結果(本身不需要結果,也是出於防止state被修改的目的)

protected boolean runAndReset() {
        if (state != NEW ||
                !UNSAFE.compareAndSwapObject(this, runnerOffset,
                        null, Thread.currentThread()))
            //任務已啟動或CAS設置運行任務的的線程失敗,直接返回false
            return false;
        boolean ran = false;
        int s = state;
        try {
            Callable<V> c = callable;
            if (c != null && s == NEW) {
                try {
                    c.call(); // don't set result  沒有調用set(V)方法,不設置結束
                    ran = true;
                } catch (Throwable ex) {
                    setException(ex);
                }
            }
        } finally {
            // runner must be non-null until state is settled to
            // prevent concurrent calls to run()
            runner = null;
            // state must be re-read after nulling runner to prevent
            // leaked interrupts
            s = state;
            if (s >= INTERRUPTING)
                handlePossibleCancellationInterrupt(s);
        }
        return ran && s == NEW; //任務成功運行且state還是NEW時返回true,反之返回false
    }

(2) get方法

get方法用於獲取任務的最終結果,它有兩個版本,其中一個是超時版本。兩個版本的最主要的區別在於,非超時版本可以不限時長地等待結果返回 ,另外非超時版本不會拋出TimeoutException超時異常。get方法超時版本的基本邏輯:若任務未完成就等待任務完成,最後調用report報告結果,report會根據狀態返回結果或拋出異常。

public V get() throws InterruptedException, ExecutionException {
    int s = state;
    if (s <= COMPLETING)
        s = awaitDone(false, 0L);//awaitDone第一個參數為false,表示可以無限時長等待
    return report(s);
}
public V get(long timeout, TimeUnit unit)
    throws InterruptedException, ExecutionException, TimeoutException {
    if (unit == null)
        throw new NullPointerException();
    int s = state;
    if (s <= COMPLETING &&//還未完成
        (s = awaitDone(true, unit.toNanos(timeout))) <= COMPLETING)//等待完成
        throw new TimeoutException();//到了限定時間,任務仍未完成,拋出超時異常TimeoutException
    return report(s);//報告結果
}

get方法的核心實現在於調用awaitDone方法,awaitDone用於等待任務的結果,若任務未完成awaitDone會阻塞當前線程。

awaitDone方法的基本邏輯:①若執行任務時出現了中斷,則拋出InterruptedException異常;②若此時任務已完成,就返回最新的state,③若任務即將完成就使當前線程讓出CPU時間片,讓其他線程先執行;④若任務還在執行中,就將當前線程加入到等待棧中,然後讓當前線程休眠直到超出限定時間或等待任務完成時run方法調用finishCompletion喚醒線程(run方法中的set或setException調用finishCompletion,而finishCompletion又會調用LockSupport.unpark).

private int awaitDone(boolean timed, long nanos)
        throws InterruptedException {
    final long deadline = timed ? System.nanoTime() + nanos : 0L;
    WaitNode q = null;
    boolean queued = false;
    for (;;) {
        if (Thread.interrupted()) { //被中斷了,就在等待棧表中移除這個線程,並拋出中斷異常
            removeWaiter(q);
            throw new InterruptedException();
        }

        int s = state;
        if (s > COMPLETING) { //任務完成了,將當前線程從等待隊列中清空,返回最新的狀態
            if (q != null)
                q.thread = null;
            return s;
        }
        //任務即將完成,當前線程禮讓,讓其他線程執行
        else if (s == COMPLETING) // cannot time out yet
            Thread.yield();
        else if (q == null) 
            //初始化當前線程對應的節點
            q = new WaitNode();
        else if (!queued)
            //如果之前入棧失敗,再次嘗試入棧(CAS更新),將當前節點設為等待棧的棧頂
            queued = UNSAFE.compareAndSwapObject(this, waitersOffset,
                    q.next = waiters, q);
        else if (timed) { //如果設置了超時時間
            nanos = deadline - System.nanoTime();
            if (nanos <= 0L) { 
                //如果任務執行時長已經超出了給定的時間,從等待棧中移除當前節點(線程)
                removeWaiter(q);
                return state;
            }
            //讓當前線程休眠等待給定的時間(或等到run方法中的set或setException調用finishCompletion來喚醒)
            LockSupport.parkNanos(this, nanos);
        }
        else//未設置超時時間
      //讓當前線程無限時長休眠等待,直到任務完成時run方法中的set或setException調用finishCompletion來喚醒此線程
            LockSupport.park(this);
    }
}

 

上面的awaitDone方法中調用removeWaiter來移除等待棧表的中斷和超時節點。

其內部實現不容易理解,但主要邏輯還是很清楚的:從頭到尾遍歷鏈表,將鏈表中的中斷/超時節點移除出鏈表,若有線程競爭就重頭開始再次遍歷鏈表檢查並移除無效節點。

private void removeWaiter(WaitNode node) {
    if (node != null) {
        node.thread = null; //先將節點對應的線程清空,下麵的"q.thread != null"就能判斷節點是否超時或中斷節點。
        retry:
        for (;;) {          // restart on removeWaiter race
            //q表示當前遍歷到的節點,pred表示q的前驅節點,s表示q的後繼節點 
            for (WaitNode pred = null, q = waiters, s; q != null; q = s) {//遍歷完鏈表才能退出內迴圈
                s = q.next;
                //q.thread!=null 表示這不是超時或中斷的節點,它是效節點,不能被從棧表中移除
                //(removeWaiter的開頭將超時或中斷的節點在thread賦空,可見node.thread=null代碼)
                if (q.thread != null)
                    pred = q; //得到下次迴圈時q的前驅節點
                else if (pred != null) { //q.thread== null 且pred!=null,需要將無效節點q從棧表中移除
                    //將q的前驅、後繼節點直接鏈接在一起,q本身被移除出棧表了
                    pred.next = s;
                    //這裡是從前向後遍歷鏈表的,無競爭情況下,不可能沒檢查到當前節點的前面還有無效節點,
                    //那麼一定有其他線程修改了當前節點q的前驅,些時有線程競爭,需要從鏈表的頭部重新遍歷檢查
                    if (pred.thread == null) // check for race
                        continue retry;
                }
                // pred==null且q.thread=null
                //q的前驅節點為空,表明q是鏈表的頭節點
                //q.thread==null,表明q是無效節點
                //無效節點不能作為鏈表的頭節點,所以要更新頭節點,將q的後繼節點s作為鏈表新的頭節點
                else if (!UNSAFE.compareAndSwapObject(this, waitersOffset,  //CAS更新頭節點
                        q, s))
                    //CAS更新失敗,重試
                    continue retry;
            }
            break;
        }
    }
}

 

get方法需要調用report方法來報告結果,而report方法的基本邏輯也簡單:若是任務正常結束就返回這個任務的結果,若是任務被取消,就拋出CancellationException異常,若是在執行任務過程中發生了異常就將其統一封裝成ExecutionException並拋出。

private V report(int s) throws ExecutionException {
    Object x = outcome;
    if (s == NORMAL)
        return (V)x;
    if (s >= CANCELLED)
        throw new CancellationException();
    throw new ExecutionException((Throwable)x);
}

(3) cancel方法

cancel方法用於取消任務,我們可以看看cancel(boolean)方法如何實現的

public boolean cancel(boolean mayInterruptIfRunning) {
    if (!(state == NEW &&
            UNSAFE.compareAndSwapInt(this, stateOffset, NEW,
                    mayInterruptIfRunning ? INTERRUPTING : CANCELLED)))
        //①不是NEW狀態,表示任務至少是COMPLETING(即將結束)狀態,返回false
        //②CAS更新state為INTERRUPTING或CANCELLED失敗,返回false
        //只有state狀態更新成功,才能取消任務(防止被併發調用)
        return false;
    try {    // in case call to interrupt throws exception
        if (mayInterruptIfRunning) {//允許中斷就設置中斷標誌
            try {
                Thread t = runner;
                if (t != null)
                    t.interrupt();//設置中斷標誌
            } finally { // final state 設置中斷的最終狀態
                //INTERRUPTING -> INTERRUPTED ,將state由“正在中斷”更新為”已經中斷“
                UNSAFE.putOrderedInt(this, stateOffset, INTERRUPTED);
            }
        }
    } finally {
        //從等待棧上喚醒並移除所有的線程(節點)
        finishCompletion();
    }
    return true;
}

其基本邏輯:

①任務已結束或被取消,返回false

②若mayInterruptIfRunning為true,調用interrupt設置中斷標誌,將state設置為INTERRUPTED,若mayInterruptIfRunning為false,將state設為CANCELLED.

③調用finishCompletion喚醒並移除等待棧中的所有線程

 

finishCompletion()主要是處理任務結束後的掃尾工作,其主要邏輯是:將等待棧waiters賦空,喚醒並移除等待棧上的所有節點(線程),最後再將任務callable賦空。

private void finishCompletion() {
    // assert state > COMPLETING;
    for (WaitNode q; (q = waiters) != null;) {
        //任務取消後,等待棧表沒有存在的意義了,將等待棧waiters賦為null,
        if (UNSAFE.compareAndSwapObject(this, waitersOffset, q, null)) {
            for (;;) { //遍歷鏈表(棧),將所有節點移除,並喚醒節點對應的線程
                Thread t = q.thread;
                if (t != null) {
                //將節點上的線程清空
                    q.thread = null; 
                    LockSupport.unpark(t);//喚醒此線程
                }
                WaitNode next = q.next;
                if (next == null)//鏈表到尾了,退出遍歷
                    break;
                q.next = null; // unlink to help gc 將節點next屬性清空,方便垃圾回收
                q = next;//向後移動一個節點
            }
            break;
        }
    }
    done();//空方法,留給子類重寫
    callable = null; //賦空,減少痕跡       // to reduce footprint 
}
 

(4) 其他輔助方法

isCancelled方法返回任務是否被取消的布爾值

isDone方法返回任務是否完成的布爾值(非正常結束也行)

isCancelled 、isDone都是直接根據state確定任務的狀態。

public boolean isCancelled() {
    return state >= CANCELLED;
}

public boolean isDone() {
    return state != NEW;
}

 


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

-Advertisement-
Play Games
更多相關文章
  • 參考鏈接:https://blog.csdn.net/qq_20777797/article/details/77916029 https://www.xiabingbao.com/css3/2017/07/03/css3-infinite-scroll.html 需求是一共有兩個,1、單張豎圖持續 ...
  • 前言 對於剛開始學習前端的伙伴倆說,問得最多的問題就是 ——前端技術現在如此繁雜,我到底應該如何學習。這個話題太大了,幾句話回答不好;也由於這個問題確實困擾了很多前端開發人員,所以我也就著手系統的輸出這篇文章。 雖然這篇文章花了很長時間,肯定也有其局限性;希望各位不吝指出。 入題 我們儼然能感受到前 ...
  • Node.js os 模塊提供了一些基本的系統操作函數。 os.tmpdir()返回操作系統的預設臨時文件夾。 os.endianness()返回 CPU 的位元組序,可能的是 "BE" 或 "LE"。 os.hostname()返回操作系統的主機名。 os.type()返回操作系統名 os.plat ...
  • 由於GET請求直接被嵌入在路徑中,URL是完整的請求路徑,包括了?後面的部分,因此你可以手動解析後面的內容作為GET請求的參數。 node.js 中 url 模塊中的 parse 函數提供了這個功能。 var http=require("http"); var url=require("url"); ...
  • 對於Vue內部來說,不管是根組件還是子組件,都存在this.$router和this.$route兩個屬性,它們的區別如下: $router 指向當前的VueRouter實例,也就是new Vue({router:router})這裡傳入的router實例對象,可以使用上一節里列出的VueRoute ...
  • 從程式員往架構師轉型的路上,蔡學鏞老師總結的“四維架構設計方法論”對我頗有幫助,讓我對架構設計有了更加立體化、系統化的認知,現將學習心得分享出來給需要的小伙伴參考。這套方法論通過空間(X、Y、Z)三個維度及時間T維度將問題域解構成可以輕鬆應對的小方塊,分而治之。同時,空間(X、Y、Z)三個維度聯動,... ...
  • 記錄大話設計學習過程。 代理模式:代理者為其他對象提供代理,以控制對真實對象的訪問。 用戶調用代理者,代理者通過真實的對象引用讓對象去做事情。但是代理者可以附加一些功能,然後才讓真實對象去做事情。 代理模式運用案例:WebService生成代理訪問服務,虛擬代理、安全代理(控制真實對象訪問時的許可權) ...
  • 功能介紹:關鍵詞搜索工具 批量關鍵詞自動搜索採集 自動去除垃圾二級泛解析功能變數名稱 可設置是否保存功能變數名稱或者url 持續更新中 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...