從超時處理說起,我們可以談些什麼

起源

有朋友說過一個場景:調用一個方法,但是該方法可能會一直阻塞下去,所以要設計超時機制,該如何設計?


首先想到的是 Future 接口,其可以在超時的時候拋出超時異常,從而執行超時後的一些處理邏輯。Future 的實現類爲 FutrueTask ,其繼承關係如下:

可見,每個 FutureTask 對象都實現了 Runnable 接口,也就是每個 FutureTask 都可以作爲創建一個新線程的 runnable 參數。

這樣子看,Futrue 的異步執行的本質是在當前線程新開一個線程去執行 task 任務,該 task 任務分爲兩塊內容:

  • 返回任務處理結果
  • 超時檢測

比較好奇超時檢測是如何實現的,所以決定再看看。

在這裏想到了 程序中使用Http請求的時候,應該也會涉及到超時處理,那麼它又是怎麼處理的呢?這個後續研究。

LockSupport

追蹤源碼可以看到,在 FutureTask 的帶有超時時間參數的 get 方法中,最終在 awaitDone 方法中可以看到:

else if (timed) {
                nanos = deadline - System.nanoTime();
                if (nanos <= 0L) {
                    removeWaiter(q);
                    return state;
                }
                LockSupport.parkNanos(this, nanos); // 這裏是關鍵的超時處理
            }

LockSupport 提供了線程的阻塞等機制,在其 parkNanos 方法實現中,可以看到下面的代碼:

    public static void parkNanos(Object blocker, long nanos) {
        if (nanos > 0) {
            Thread t = Thread.currentThread();
            setBlocker(t, blocker);
            UNSAFE.park(false, nanos);  // 這裏是關鍵
            setBlocker(t, null);
        }
    }

該方法中調用了 Unsafe 類中的 park() 方法。Unsafe 類主要負責執行一些不安全的操作,比如自主管理內存的資源等。該 park 方法的聲明如下:

public native void park(boolean b, long l);

可見,這是一個 native 方法,也即本地方法。

方法執行的時候的需要 棧內存 進行操作,那麼對於 native 方法來說,其運行時棧就是 jvm 內存結構中的 本地方法棧, 那麼 Unsafe 中對於內存的管理,則是針對 堆外內存 的管理。

native 方法:我們可以根據 JNI(java本地接口) 規範來編寫 native 方法的實現,通常使用 C/C++ 編寫,比較偏向底層。


那麼,這裏的 park() 方法的具體實現到底是什麼呢?

park方法實現

在 Linux 平臺上,是使用基於 POSIX 規範的API來實現的park方法。其實現的關鍵代碼如下:

int os::PlatformEvent::park(jlong millis) {
    // 忽略一堆
    while (_event < 0) {
      status = pthread_cond_timedwait(_cond, _mutex, &abst);  // 關鍵在這裏
      assert_status(status == 0 || status == ETIMEDOUT,
                    status, "cond_timedwait");
      // OS-level "spurious wakeups" are ignored unless the archaic
      // FilterSpuriousWakeups is set false. That flag should be obsoleted.
      if (!FilterSpuriousWakeups) break;
      if (status == ETIMEDOUT) break;
    }
}

可以看到,關鍵的超時等待是 pthread_cond_timedwait() 方法,該方法屬於 glibc 庫中的方法。

我們知道,linux 對外支持了許多系統調用,而這些系統調用其實不會被系統開發人員直接使用,大家使用的都是 glibc 中的方法,glibc 相當於對系統調用做了一層封裝。

該方法中最終調用了 futex 這個真正的系統調用,該系統調用中,使用了定時器 hrtimer 這個高精度定時器來實現超時處理。默認地,該定時器 x us計時一次(沒驗證,參考是50)。定時時間到了可以執行對象的中斷處理函數進行後續處理。

至於什麼時候執行 unpark,以及後續動作如何聯動處理,都是通過 信號量 機制來實現各種同步互斥的,這裏暫不關注。

到了這裏,需要對 sleep 這個java 方法進行對比研究,看看它底層是如何實現的。

java sleep 實現原理

同樣的,Thread.sleep() ,該方法也是 native 方法,其具體實現爲:

int os::sleep(Thread* thread, jlong millis, bool interruptible) {
    // 一堆代碼
    slp->park(millis); // 關鍵
}

可以看到,其本身也是調用 park 方法實現的。

那麼,疑問來了,LockSupport.parkNanosThread.sleep 的區別是什麼?存在即合理,所以肯定是有區別的。

再說 FutureTask

上文說的重點在於 定時的處理,現在需要將重點轉移到同步的問題上來。

LockSupportparkunpark 方法,都需要傳入需要阻塞或者解除阻塞的線程。當A線程調用 get 方法時,其內部的實現上是將 this 作爲參數傳入的,也即將當前的A線程阻塞起來。那麼什麼時候解鎖呢?有兩個觸發方式。

超時解鎖

再仔細看一下上文的 awaitDone 方法:下面已經刪除了一些條件判斷

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 (timed) { // 超時判斷
            nanos = deadline - System.nanoTime();
            if (nanos <= 0L) {
                removeWaiter(q);
                return state;
            }
            LockSupport.parkNanos(this, nanos);
        }
        else 
            LockSupport.park(this);
    }
}

假設在調用 parkNanos 期間任務沒有完成,則會阻塞當前線程,直到定時器計時結束後,再喚醒該線程。從而該線程繼續在 for 循環中判斷 state 狀態,隨即就會發現處於超時狀態,所以上層方法會拋出異常,如下:

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(); // 所以拋出超時異常
    return report(s);
}

還有另一種觸發條件就是任務完成的觸發。

任務完成觸發

我們肯定要先把 FutureTask 任務跑起來纔好執行 get 等方法,所以首先要調用 run 方法,run方法的實現中,最終會在 方法完成後執行如下的代碼:

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;
                q = next;
            }

該方法中會對線程執行 unpark 操作喚醒線程,所以可以實現任務完成後 get 方法就會立刻返回。所以 park 和 unpark 的組合使用,可以實現 sleep 實現不了的功能。

小小總結

通過上述研究,可以總結。這種情境下的超時處理不論如何都需要阻塞一個線程。

HttpClient 中的超時

那麼,HttpClient 中的超時又是如何處理的呢?其涉及到三種超時設置:

  • ConnectionRequestTimeout:如果配置了連接池,那麼如果池中沒有可用連接,則最多等 ConnectionRequestTimeout 時間
  • ConnectTimeout : 拿到連接以後,和通信的另一方建立連接的超時時間。即TCP通道的建立超時。
  • SocketTimeout:連接建立以後,數據讀取的超時時間。

第一種池子中獲取連接的超時和上文研究的超時比較像,而後兩種主要涉及到的是 系統底層 和 網絡層的超時處理,情況不太一樣,暫不繼續研究。

linux 定時器的實現

最後想說的是 Linux 中定時器的實現,還是一些數據結構優化問題,感興趣可以繼續看下延伸閱讀中的文章,其給出了一般意義上的定時器實現機制,對於嵌入式人員來說也有很大的參考價值。

實際上,定時器系統也是 linux 內核中一個很大的範疇。

總結

實際上本篇引申出了很多可以繼續研究的東西,比如:

  • wait 和 notify 的實現原理對比
  • linux內核定時器系統
  • JNI實現規範
  • 網絡超時的底層原理
  • POSIX規範
  • 無鎖化編程
  • 信號量同步互斥機制
  • 等等

延伸閱讀

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章