高優非同步任務解決雙重非同步集合點阻塞問題

FunTester發表於2024-03-12

在效能測試的實踐當中,非同步任務是離不開的。Java 非同步程式設計提高了應用程式的效能和響應性,透過避免執行緒阻塞提高了資源利用率,並簡化了併發程式設計的複雜性。改善使用者體驗,避免死鎖和執行緒阻塞等問題。非同步程式設計利用 CompletableFuture、Future 等工具和 API 簡化了開發流程,提高了系統的穩定性和可靠性。

緣起

我也參照了 Go 語言的 go 關鍵字,自定義了 fun 關鍵字Java 自定義非同步功能實踐 。但是在使用過程中,遇到了一個略顯尷尬的問題,就是如果當一個非同步任務中,又增加一個非同步任務,且使用集合點設定。那麼就會阻塞執行緒池,導致大量任務阻塞的情況。

比如一個學校,200 個班級,每個班級有一個班主任,要給 30 個學生髮作業,之後再報告作業分發已完成。按照之前的思路,我會包裝兩個非同步且設定集合點的任務,虛擬碼如下:

static void main(String[] args) {
    200.times {
        fun {
            sleep(1.0)// 模擬業務處理
            pushHomework()// 佈置作業
        }
    }

}

/**
 * 佈置作業
 */
static void pushHomework() {
    FunPhaser phaser = new FunPhaser()// 建立同步屏障
    30.times {
        fun {
            sleep(1.0)// 模擬業務處理
            output("佈置作業")
        } , phaser
    }
    phaser.await()// 等待所有作業佈置完成
}

最終的結果就是,等於最大執行緒數的任務會阻塞在 pushHomework() 方法中,而 pushHomework() 方法需要完成的非同步任務又全都等待線上程池的等待佇列中。

初解

一開始我的思路採取優先順序策略。如果區分任務的優先順序,讓有可能阻塞在等待佇列的高優任務優先執行即可。所以我先想使用 java.util.concurrent.PriorityBlockingQueue 當做 java.util.concurrent.BlockingQueue 的實現當做非同步執行緒池的等待佇列。

但也無法解決問題,因為依然存在阻塞的問題,只不過機率變小了而已。看來不得不使用單獨的非同步執行緒池來實現了。

關於執行緒池的選擇有兩種選擇:

  1. 選擇最大執行緒數較小的執行緒池,只是作為輔助功能,防止阻塞。在普通非同步任務執行時,優先執行高優任務,利用普通執行緒池優先執行高優任務。
  2. 選擇最小執行緒數較大的執行緒池,大機率是快取執行緒池。單獨用來執行高優任務。同時也可以利用普通的執行緒池執行高優任務。

關於我的選擇,也沒有選擇。根據實際的情況使用吧。高優任務的多少、需要限制的頻率等等因素。我自己的專案用的是第二種,原因是我用到高優任務的機會不多,我可以在指令碼中控制高優任務的數量。

方案

首先是執行緒池的實現程式碼:

priorityPool = createFixedPool(POOL_SIZE, "P")

建立時機放在了普通執行緒池中:

    /**
     * 獲取非同步任務連線池
     * @return
     */
    static ThreadPoolExecutor getFunPool() {
        if (asyncPool == null) {
            synchronized (ThreadPoolUtil.class) {
                if (asyncPool == null) {
                    asyncPool = createPool(POOL_SIZE, POOL_SIZE, ALIVE_TIME, new LinkedBlockingDeque<Runnable>(Constant.MAX_WAIT_TASK), getFactory("F"))
                    daemon()
                }
                priorityPool = createFixedPool(POOL_SIZE, "P")
//                priorityPool = createPool(1, POOL_MAX, ALIVE_TIME, new LinkedBlockingQueue<Runnable>(10), getFactory("P"), new ThreadPoolExecutor.DiscardOldestPolicy())
            }
        }
        return asyncPool
    }

下面是呼叫執行高優的非同步任務的方法:

/**
 * 執行高優非同步任務
 * @param runnable
 */
static void executeSyncPriority(Runnable runnable) {
    if (priorityPool == null) getFunPool()
    priorityPool.execute(runnable)
}

還有一個呼叫方法,用來普通執行緒池優先執行高優任務:

/**
 * 執行高優任務
 */
static void executePriority() {
    def locked = priorityLock.compareAndSet(false, true)//如果沒有鎖,則加鎖
    if (locked) {//如果加鎖成功
        while (priorityPool.getQueue().size() > 0) {
            def poll = priorityPool.getQueue().poll()
            def queue = (LinkedBlockingDeque<Runnable>) getFunPool().getQueue()
            if (poll != null) {
                queue.offerFirst(poll)
            }

        }
        priorityLock.set(false)//解鎖
    }
}

這裡用到了一個原子類,當做高優之行時候的鎖 private static AtomicBoolean priorityLock = new AtomicBoolean(false) ,避免在這塊浪費過多效能。這裡沒有 try-catch-finally ,此處沒有使用,確實發生異常機率較小。

我重新修改了任務佇列的實現,用的是 java.util.concurrent.LinkedBlockingDeque ,這樣我就可以將高優任務直接插入到佇列的最前頭,可以優先執行高優任務。

對於非同步關鍵字,我也進行了一些改動:

/**
 * 使用自定義同步器{@link FunPhaser}進行多執行緒同步
 *
 * @param f
 * @param phaser
 * @param log
 */
public static void fun(Closure f, FunPhaser phaser, boolean log) {
    if (phaser != null) phaser.register();
    ThreadPoolUtil.executeSync(() -> {
        try {
            ThreadPoolUtil.executePriority();
            f.call();
        } finally {
            if (phaser != null) {
                phaser.done();
                if (log) logger.info("async task {}", phaser.queryTaskNum());
            }
        }
    });
}

執行高優任務的關鍵字,我也進行了同樣的封裝,只不過換了個關鍵字和執行緒池:

/**
 * 提交高優任務
 *
 * @param f
 * @param phaser
 * @param log
 */
public static void funny(Closure f, FunPhaser phaser, boolean log) {
    if (phaser != null) phaser.register();
    ThreadPoolUtil.executeSyncPriority(() -> {
        try {
            f.call();
        } finally {
            if (phaser != null) {
                phaser.done();
                if (log) logger.info("priority async task {}", phaser.queryTaskNum());
            }
        }
    });
}

驗證

我們修改一下開始的指令碼:

static void main(String[] args) {
    setPoolMax(2)
    6.times {
        fun {
            sleep(1.0)// 模擬業務處理
            pushHomework()// 佈置作業
        }
    }

}

/**
 * 佈置作業
 */
static void pushHomework() {
    FunPhaser phaser = new FunPhaser()// 建立同步屏障
    4.times {
        fun {
            sleep(1.0)// 模擬業務處理
            output("佈置作業")
        } , phaser
    }
    phaser.await()// 等待所有作業佈置完成
}

執行的話,執行緒池的 F 執行緒全都全都是 TIME_WAITING 狀態。當把 pushHomework() 方法改成高優關鍵字 funny 之後問題便可迎刃而解。

控制檯輸出如下:

22:47:17:160 P-1  佈置作業
22:47:17:160 P-1  佈置作業
22:47:17:160 P-1  priority async task 3
22:47:17:160 P-1  priority async task 4
22:47:18:178 F-2  佈置作業
22:47:18:179 F-2  priority async task 3
22:47:19:183 F-2  佈置作業

可以看出,已經開始有了 F 執行緒執行高優任務了。

  • 2021 年原創合集
  • 2022 年原創合集
  • 2023 年原創合集
  • 介面功能測試專題
  • 效能測試專題
  • Java、Groovy、Go、Python
  • 單元&白盒&工具合集
  • 測試方案&BUG&爬蟲&UI 自動化
  • 測試理論雞湯
  • 社群風采&影片合集
如果覺得我的文章對您有用,請隨意打賞。您的支援將鼓勵我繼續創作!
打賞支援
暫無回覆。

相關文章