原始碼|批量執行invokeAll()&&多選一invokeAny()

monkeysayhi發表於2017-12-14

ExecutorService中定義了兩個批量執行任務的方法,invokeAll()和invokeAny(),在批量執行或多選一的業務場景中非常方便。invokeAll()在所有任務都完成(包括成功/被中斷/超時)後才會返回,invokeAny()在任意一個任務成功(或ExecutorService被中斷/超時)後就會返回。

AbstractExecutorService實現了這兩個方法,本文將先後分析invokeAll()和invokeAny()兩個方法的原始碼實現。

JDK版本:oracle java 1.8.0_102

invokeAll()

invokeAll()在所有任務都完成(包括成功/被中斷/超時)後才會返回。有不限時和限時版本,從更簡單的不限時版入手。

不限時版

public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
    throws InterruptedException {
    if (tasks == null)
        throw new NullPointerException();
    ArrayList<Future<T>> futures = new ArrayList<Future<T>>(tasks.size());
    boolean done = false;
    try {
        for (Callable<T> t : tasks) {
            RunnableFuture<T> f = newTaskFor(t);
            futures.add(f);
            execute(f);
        }
        for (int i = 0, size = futures.size(); i < size; i++) {
            Future<T> f = futures.get(i);
            if (!f.isDone()) {
                try {
                    f.get(); // 無所謂先執行哪個任務的get()方法
                } catch (CancellationException ignore) {
                } catch (ExecutionException ignore) {
                }
            }
        }
        done = true;
        return futures;
    } finally {
        if (!done)
            for (int i = 0, size = futures.size(); i < size; i++)
                futures.get(i).cancel(true);
    }
}
複製程式碼

8-12行,先將所有任務都提交到執行緒池(當然,任何ExecutorService均可)中。

嚴格來說,不是“提交”,而是“執行”。執行可能是同步或非同步的,取決於執行緒池的策略。不過由於我們僅討論非同步情況(同步同理),用“提交”一詞更容易理解。下同。

13-22行,for迴圈的目的是阻塞呼叫invokeAll的執行緒,直到所有任務都執行完畢。當然我們也可以使用其他方式實現阻塞,不過這種方式是最簡單的:

  • 15行如果f.isDone()返回true,則當前任務已結束,繼續檢查下一個任務;否則,呼叫f.get()讓執行緒阻塞,直到當前任務結束。
  • 17行無所謂先執行哪一個FutureTask例項的get()方法。由於所有任務併發執行,總體阻塞時間取決於於是耗時最長的任務,從而實現了invodeAll的阻塞呼叫。
  • 18-20行沒有捕獲InterruptedException。如果有任務被中斷,主執行緒將丟擲InterruptedException,以響應中斷。

最後,為防止在全部任務結束之前過早退出,23行、25-29行相配合,如果done不為true(未執行到40行就退出了)則取消全部任務。

限時版

public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,
                                     long timeout, TimeUnit unit)
    throws InterruptedException {
    if (tasks == null)
        throw new NullPointerException();
    long nanos = unit.toNanos(timeout);
    ArrayList<Future<T>> futures = new ArrayList<Future<T>>(tasks.size());
    boolean done = false;
    try {
        for (Callable<T> t : tasks)
            futures.add(newTaskFor(t));

        final long deadline = System.nanoTime() + nanos;
        final int size = futures.size();

        // Interleave time checks and calls to execute in case
        // executor doesn't have any/much parallelism.
        for (int i = 0; i < size; i++) {
            execute((Runnable)futures.get(i));
            nanos = deadline - System.nanoTime();
            if (nanos <= 0L) // 及時檢查是否超時
                return futures;
        }

        for (int i = 0; i < size; i++) {
            Future<T> f = futures.get(i);
            if (!f.isDone()) {
                if (nanos <= 0L) // 及時檢查是否超時
                    return futures;
                try {
                    f.get(nanos, TimeUnit.NANOSECONDS);
                } catch (CancellationException ignore) {
                } catch (ExecutionException ignore) {
                } catch (TimeoutException toe) {
                    return futures;
                }
                nanos = deadline - System.nanoTime();
            }
        }
        done = true;
        return futures;
    } finally {
        if (!done)
            for (int i = 0, size = futures.size(); i < size; i++)
                futures.get(i).cancel(true);
    }
}
複製程式碼

10-11行,先將所有任務封裝為FutureTask,新增到futures列表中。

18-23行,每提交一個任務,就立刻判斷是否超時。這樣的話,如果在任務全部提交到執行緒池中之前,就已經達到了超時時間,則能夠儘快檢查出超時,結束提交併退出。

對於限時版,將封裝任務與提交任務拆開是必要的。

28-29行,每次在呼叫限時版f.get()進入阻塞狀態之前,先檢查是否超時。這裡也是希望超時後,能夠儘快發現並退出。

其他同不限時版。

invokeAny()

invokeAny()在任意一個任務成功(或ExecutorService被中斷/超時)後就會返回。也分為不限時和限時版本,但為了進一步保障效能,invokeAny()的實現思路與invokeAll()略有不同。

public <T> T invokeAny(Collection<? extends Callable<T>> tasks)
    throws InterruptedException, ExecutionException {
    try {
        return doInvokeAny(tasks, false, 0);
    } catch (TimeoutException cannotHappen) {
        assert false;
        return null;
    }
}
public <T> T invokeAny(Collection<? extends Callable<T>> tasks,
                       long timeout, TimeUnit unit)
    throws InterruptedException, ExecutionException, TimeoutException {
    return doInvokeAny(tasks, true, unit.toNanos(timeout));
}
複製程式碼

內部呼叫了doInvokeAny()。

學習5-8行的寫法,程式碼自解釋。

doInvokeAny()

簡化如下:

private <T> T doInvokeAny(Collection<? extends Callable<T>> tasks,
                          boolean timed, long nanos)
    throws InterruptedException, ExecutionException, TimeoutException {
...
    ArrayList<Future<T>> futures = new ArrayList<Future<T>>(ntasks);
    ExecutorCompletionService<T> ecs =
        new ExecutorCompletionService<T>(this);
...
    try {
        ExecutionException ee = null;
        final long deadline = timed ? System.nanoTime() + nanos : 0L;
        Iterator<? extends Callable<T>> it = tasks.iterator();

        futures.add(ecs.submit(it.next()));
        --ntasks;
        int active = 1;

        for (;;) {
            Future<T> f = ecs.poll();
            if (f == null) {
                if (ntasks > 0) {
                    --ntasks;
                    futures.add(ecs.submit(it.next()));
                    ++active;
                }
                else if (active == 0)
                    break;
                else if (timed) {
                    f = ecs.poll(nanos, TimeUnit.NANOSECONDS);
                    if (f == null)
                        throw new TimeoutException();
                    nanos = deadline - System.nanoTime();
                }
                else
                    f = ecs.take();
            }
            if (f != null) {
                --active;
                try {
                    return f.get();
                } catch (...) {
                    ee = ...;
                }
            }
        }
...
        throw ee;
    } finally {
        for (int i = 0, size = futures.size(); i < size; i++)
            futures.get(i).cancel(true);
    }
}
複製程式碼

要點:

  • ntasks維護未提交的任務數,active維護已提交未結束的任務數。
  • 內部使用ExecutorCompletionService維護已完成的任務。
  • 如果沒有任務成功結束,則返回捕獲的最後一個異常
  • 第一個任務是必將被執行的,其他任務按照迭代器順序增量提交

增量提交有什麼好處呢?節省資源,如果在提交過程中就有任務完成了,那麼沒必要繼續提交任務耗費時間和空間;降低延遲,如果有任務完成,與全量提交相比,能更早被發現。

14行先向執行緒池提交一個任務(迭代器第一個),ntasks--,active=1:

futures.add(ecs.submit(it.next()));
--ntasks;
int active = 1;
複製程式碼

這裡是真“提交”了,不是“執行”。

然後18-45行迴圈檢查是否有任務成功結束。

首先,19行通過及時返回的poll()方法,嘗試取出一個已完成的任務:

Future<T> f = ecs.poll();
複製程式碼

根據f的結果,分成兩種情況討論。

ExecutorCompletionService預設使用LinkedBlockingQueue作為任務佇列。對LinkedBlockingQueue不熟悉的可參照原始碼|併發一枝花之BlockingQueue

case1:如果有任務完成

如果有任務完成,則f不為null,進入40-49行,active--,並嘗試取出任務結果:

if (f != null) {
    --active;
    try {
        return f.get();
    } catch (...) {
        ee = ...;
    }
}
複製程式碼
  • 如果能夠成功取出,即當前任務已成功結束,直接返回。
  • 如果丟擲異常,則當前任務異常結束,使用ee記錄異常。

顯然,如果已完成的任務是異常結束的,invokeAny()不會退出,而是繼續檢視其它任務。

FutureTask#get()的用法參照原始碼|使用FutureTask的正確姿勢

case2:如果沒有任務完成

如果沒有任務完成,則f為null,進入23-39行,判斷是繼續提交任務、退出還是等待任務結果:

if (f == null) {
    if (ntasks > 0) { // check1
        --ntasks;
        futures.add(ecs.submit(it.next()));
        ++active;
    }
    else if (active == 0) // check2
        break;
    else if (timed) { // check3
        f = ecs.poll(nanos, TimeUnit.NANOSECONDS);
        if (f == null)
            throw new TimeoutException();
        nanos = deadline - System.nanoTime();
    }
    else // check4
        f = ecs.take();
}
複製程式碼
  • check1:如果還有剩餘任務(ntasks > 0),那就繼續提交,同時ntasks--,active++。
  • check2:如果沒有剩餘任務了,且也沒有已提交未結束的任務(active == 0),則表示全部任務均已執行結束,但沒有一個任務是成功的,可以退出迴圈。退出迴圈後,將在47行丟擲ee記錄的最後一個異常。
  • check3:如果可以沒有剩餘任務,但還有已提交未結束的任務,且開啟了超時機制,則嘗試使用超時版poll()等待任務完成。但是,如果這種情況下超時了,就表示整個invokeAny()方法超時了,所以poll()返回null的時候,要主動丟擲TimeoutException。
  • check4:如果可以沒有剩餘任務,但還有已提交未結束的任務,且未開啟超時機制,則使用無限阻塞的take()方法,等待任務完成。

這種一堆if-else的程式碼很醜。可修改如下:

if (f == null) { // check1
   if (ntasks > 0) {
        --ntasks;
        futures.add(ecs.submit(it.next()));
        ++active;
        continue;
    }
    if (active == 0) { // check2
        assert ntasks == 0; // 防止自己改吧改吧把它這句判斷挪到了前面
        break;
    }
   if (timed) { // check3
       f = ecs.poll(nanos, TimeUnit.NANOSECONDS);
       if (f == null) {
           throw new TimeoutException();
       }
       nanos = deadline - System.nanoTime();
   } else { // check4
       f = ecs.take();
   }
}
複製程式碼

修改依據:

  • check1、check2、check3/check4沒有並列的判斷關係
  • check3、check4有並列的判斷關係,非此即彼
  • 結構更清爽

總結

不會寫總結。。。

但是會寫吐槽啊!!!

猴子現在每次寫部落格都經歷著從“臥槽似乎很簡單啊,寫個毛”到“臥槽這跟想象的不一樣啊!臥槽巨帥!”的心態崩塌,各位巨巨寫的程式碼是真好看,效能還棒棒噠,羨慕崇拜打雞血。哎,,,保持謙卑,並羨慕臉遙拜各位巨巨。


本文連結:原始碼|批量執行invokeAll()&&多選一invokeAny()
作者:猴子007
出處:monkeysayhi.github.io
本文基於 知識共享署名-相同方式共享 4.0 國際許可協議釋出,歡迎轉載,演繹或用於商業目的,但是必須保留本文的署名及連結。

相關文章