本文主要使用kotlin,討論Android開發中的執行緒池用法。
我們想使用執行緒的時候,可以直接建立子執行緒並啟動
Thread { Log.d("rfDev", "rustfisher said: hello") }.start()
不想每次都建立新的子執行緒
如果有大量的非同步任務,不想每次都建立子執行緒。有沒有什麼把子執行緒統一管理的方法?
遇到這樣的情況,我們可以考慮執行緒池。執行緒池解決兩個問題:需要執行大量非同步任務的時候,減輕每個非同步任務的呼叫開銷,提高效能。另外它還能夠限制和管理子執行緒。每個ThreadPoolExecutor都維護了一些統計資料,例如已執行的任務數量。
有大量非同步任務的時候,可以考慮使用執行緒池。
預置執行緒池
程式碼參考 Android API 29
ThreadPoolExecutor提供了很多引數,方便開發者調控。執行緒池的設計者建議開發者使用以下幾個工廠方法,Android中主要有5種
newCachedThreadPool()
不限制數量的執行緒池,能自動回收執行緒newFixedThreadPool(int nThreads)
固定數量的執行緒池newSingleThreadExecutor()
單一的子執行緒newScheduledThreadPool(int corePoolSize)
能執行延時任務或者週期性任務newWorkStealingPool()
工作竊取執行緒池
實際上我們在Android Studio裡輸入Executors.new
的時候,會跳出很多個提示選項。
Executors.new 的智慧提示
可快取執行緒池
用Executors.newCachedThreadPool
獲得一個可快取執行緒池物件,然後讓它執行任務。
val tp: ExecutorService = Executors.newCachedThreadPool()
tp.submit { Log.d(TAG, "rustfisher: cached執行緒池執行任務 3") }
可快取執行緒池會在需要的時候建立新的子執行緒。當原有的執行緒可用的時候,會複用現有執行緒。
這個機制適用於執行多個短期非同步任務。任務比較小,但是數量大。
呼叫execute
方法會先嚐試複用已有的可用執行緒。如果當前沒有執行緒,會新建一個執行緒並把它新增到池裡。
超過60秒沒有使用的執行緒會被停止並移除。因此即便長時間不用這個執行緒池,也不會造成多大的開銷。
定長執行緒池
使用newFixedThreadPool(int nThreads)
示例
val fixedTp: ExecutorService = Executors.newFixedThreadPool(4)
fixedTp.submit { Log.d(TAG, "rustfisher 定長執行緒池執行任務") }
靜態方法裡傳入了一個int引數nThreads
,表示最大執行緒數量。
如果當前所有執行緒都在忙,又有新的任務新增進來。那麼任務會在佇列中等待,直到有可用的執行緒來處理任務。
如果有的執行緒遇到錯誤而停止了,要執行任務的話,會建立新的執行緒補上位置。
池裡的執行緒會一直存活,直到執行緒池停止(ExecutorService#shutdown
)。
單一執行緒池
val singleTp: ExecutorService = Executors.newSingleThreadExecutor()
singleTp.submit { Log.d(TAG, "單一執行緒池執行任務") }
只擁有1個子執行緒。任務佇列不限制任務數量。如果執行緒遇到問題停止了,接下來又要處理任務時,會新建一個執行緒來處理。
它能保證任務會按順序處理,同一時間只能處理1個任務。
單一執行緒池建立後,不能動態修改執行緒數量。不像newFixedThreadPool(1)
的定長執行緒池可以修改執行緒數。
計劃任務執行緒池
val scheduleTp: ScheduledExecutorService = Executors.newScheduledThreadPool(3)
計劃任務執行緒池能夠執行延遲任務和週期任務。
延遲任務
需要設定延時與時間單位
scheduleTp.schedule({ Log.d(TAG, "計劃任務1 runnable") }, 300, TimeUnit.MILLISECONDS)
scheduleTp.schedule(Callable { Log.d(TAG, "計劃任務2 callable") }, 400, TimeUnit.MILLISECONDS)
週期任務
主要涉及到2個方法scheduleAtFixedRate
和scheduleWithFixedDelay
。
假設任務時間小於週期時間,則按給定週期時間來進行。這兩個方法表現一致。
假設任務執行時間大於週期時間,這兩個方法有點不同
scheduleAtFixedRate
執行完上一個任務後,用時超過了週期時間,會立刻執行下一個任務。scheduleWithFixedDelay
在上一個任務執行完畢後,還會等待週期時間,再去執行下一個任務。
工作竊取執行緒池
Android SDK 大於等於24,有一種新的執行緒池,暫且稱為“工作竊取執行緒池”,或者叫“靈活排程執行緒池”。
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
Executors.newWorkStealingPool()
}
執行緒池維護足夠的執行緒來支援給定的並行度(parallelism level),可能會用多個佇列來減少爭用。
並行度對應的是活躍的執行緒最大數,或者能處理任務的執行緒最大數。
執行緒的實際數量可能會動態增減。工作竊取執行緒池不保證按提交順序來處理任務。
執行任務
執行任務的時候可以傳入Runnable和Callable,前面用的都是Runnable。
用Callable的例子
tp.submit(Callable { "OK" })
無返回值任務的呼叫
無返回值任務用Callable和Runnable都行。
val tp: ExecutorService = Executors.newCachedThreadPool()
tp.submit { Log.d(TAG, "rustfisher: cached執行緒池submit runnable") }
tp.execute { Log.d(TAG, "rustfisher: cached執行緒池execute runnable") }
tp.submit(Callable { Log.d(TAG, "rustfisher: cached執行緒池submit callable") })
tp.shutdown() // 最後記得用完後停掉執行緒池
有返回值任務的呼叫
有返回值的任務需要Callable介面。
submit
呼叫submit
方法時會返回一個Future物件。通過Future的get()
方法可拿到返回值。這裡需要注意get()
是阻塞的,完成任務後,能拿到返回值。
val tp: ExecutorService = Executors.newCachedThreadPool()
val future = tp.submit(Callable {
return@Callable "callable的返回值"
})
Log.d(TAG, "future get之前 isDone: ${future.isDone}, isCancelled: ${future.isCancelled}")
val res = future.get()
Log.d(TAG, "future get之後 isDone: ${future.isDone}, isCancelled: ${future.isCancelled}")
Log.d(TAG, "future get: $res")
執行log
future get之前 isDone: false, isCancelled: false
future get之後 isDone: true, isCancelled: false
future get: callable的返回值
invokeAll
對於列表裡的任務,可以使用invokeAll(Collection<? extends Callable<T>> tasks)
,返回一個Future的列表。
作為對比,給其中一個任務加上延時。
invokeAll示例
val tp: ExecutorService = Executors.newFixedThreadPool(5)
val callList = arrayListOf<Callable<String>>(
Callable {
Log.d(TAG, "task1 ${Thread.currentThread()}")
return@Callable "rust"
},
Callable {
Log.d(TAG, "task2 ${Thread.currentThread()}")
Thread.sleep(1500) // 加上延時
return@Callable "fisher"
},
Callable {
Log.d(TAG, "task3 ${Thread.currentThread()}")
return@Callable "列表裡面的任務"
},
)
Log.d(TAG, "invokeAll 準備提交任務")
val futureList = tp.invokeAll(callList)
Log.d(TAG, "invokeAll 已提交任務")
futureList.forEach { f ->
Log.d(TAG, "任務列表執行結果 ${f.get()}") // 這裡會阻塞 別在ui執行緒裡get
}
執行log,可以看到提交任務後,經過延時,拿到了執行結果。注意看invokeAll
前後的時間。invokeAll
會阻塞當前執行緒。使用的時候必須小心,不要在ui執行緒中呼叫。
2021-09-11 14:40:07.062 16914-16914/com.rustfisher.tutorial2020 D/rfDevTp: invokeAll 準備提交任務
2021-09-11 14:40:07.063 16914-19230/com.rustfisher.tutorial2020 D/rfDevTp: task1 Thread[pool-4-thread-1,5,main]
2021-09-11 14:40:07.063 16914-19231/com.rustfisher.tutorial2020 D/rfDevTp: task2 Thread[pool-4-thread-2,5,main]
2021-09-11 14:40:07.063 16914-19232/com.rustfisher.tutorial2020 D/rfDevTp: task3 Thread[pool-4-thread-3,5,main]
2021-09-11 14:40:08.563 16914-16914/com.rustfisher.tutorial2020 D/rfDevTp: invokeAll 已提交任務
2021-09-11 14:40:08.563 16914-16914/com.rustfisher.tutorial2020 D/rfDevTp: 任務列表執行結果 rust
2021-09-11 14:40:08.563 16914-16914/com.rustfisher.tutorial2020 D/rfDevTp: 任務列表執行結果 fisher
2021-09-11 14:40:08.563 16914-16914/com.rustfisher.tutorial2020 D/rfDevTp: 任務列表執行結果 列表裡面的任務
提交了3個任務,在3個不同的子執行緒中執行。
invokeAny
invokeAny(Collection<? extends Callable<T>> tasks)
也是接收Callable集合。
然後返回最先執行結束的任務的值,其它未完成的任務將被正常取消掉不會有異常。
invokeAny示例
val tp: ExecutorService = Executors.newCachedThreadPool()
val callList = arrayListOf<Callable<String>>(
Callable {
Thread.sleep(1000) // 設計延時
return@Callable "rust"
},
Callable {
Thread.sleep(400)
return@Callable "fisher"
},
Callable {
Thread.sleep(2000)
return@Callable "列表裡面的任務"
},
)
Log.d(TAG, "invokeAny 提交任務")
val res = tp.invokeAny(callList)
Log.d(TAG, "執行結果 $res")
2021-09-11 14:04:55.253 14066-14066/com.rustfisher.tutorial2020 D/rfDevTp: invokeAny 提交任務
2021-09-11 14:04:55.654 14066-14066/com.rustfisher.tutorial2020 D/rfDevTp: 執行結果 fisher
觀察log可以看到,最後執行的是“fisher”這個任務。
停止執行緒池
使用完畢後,記得終止執行緒池
/*ExecutorService*/ shutdown()
shutdownNow()
shutdown()
在已提交的任務後面建立一個停止命令,並且不再接受新的任務。如果執行緒池已經停止了,呼叫這個方法將不生效。
shutdownNow()
方法嘗試停止所有執行中的任務,停下等待中的任務。並且返回等待執行的任務列表List<Runnable>
。