CoroutineWorker は何並列で実行されるのか?
はじめに
WorkManager に enqueue した Work は何並列で実行されるのか? の Coroutine 編です。
androidx.work:work-runtime-ktx
に含まれる CoroutineWorker で実装した Worker が何並列で実行されるのか見てみました。
androidx.work:work-*:2.2.0 、 Pixel 3a API 29 Emulator で確認済みです。
結果
- デフォルト:4
-
Dispatchers.IO
だと 20並列(試した範囲では)
※今回は WorkManager の Worker の並列度に注目していますが、 Dispatchers の並列度に依存する結果になっています
※用法用量を守って正しい Dispatchers を指定しましょう。
試したコード
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setSupportActionBar(toolbar)
fab.setOnClickListener {
// 10コ同時に enqueue する
val workManager = WorkManager.getInstance(this@MainActivity)
val requests = (1..10).map { MyWorker.createRequest(it) }
workManager.enqueue(requests)
}
}
}
class MyWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx, params) {
override suspend fun doWork(): Result {
Log.d("★", "param = ${inputData.param} on ${Thread.currentThread().name}")
Thread.sleep(1000)
return Result.success()
}
private val Data.param: Int get() = inputData.getInt(KEY_PARAM, 0)
companion object {
const val TAG = "MyWorker"
private const val KEY_PARAM = "KEY_PARAM"
fun createRequest(param: Int): WorkRequest =
OneTimeWorkRequest.Builder(MyWorker::class.java)
.setInputData(createData(param))
.addTag(TAG)
.build()
private fun createData(param: Int): Data =
Data.Builder()
.putInt(KEY_PARAM, param)
.build()
}
}
この実行結果
D/★: param = 2 on DefaultDispatcher-worker-2
D/★: param = 1 on DefaultDispatcher-worker-1
D/★: param = 3 on DefaultDispatcher-worker-4
D/★: param = 4 on DefaultDispatcher-worker-3
(約1秒)
D/★: param = 5 on DefaultDispatcher-worker-2
D/★: param = 6 on DefaultDispatcher-worker-1
D/★: param = 7 on DefaultDispatcher-worker-4
D/★: param = 8 on DefaultDispatcher-worker-3
(約1秒)
D/★: param = 9 on DefaultDispatcher-worker-2
D/★: param = 10 on DefaultDispatcher-worker-1
4並列でした。
( 手元の環境では、Coroutine ではない Worker よりも並列度が1つ多かったです。)
では、どの Dispatchers で実行されていたのでしょうか?
/**
* The coroutine context on which [doWork] will run. By default, this is [Dispatchers.Default].
*/
@Deprecated(message = "use withContext(...) inside doWork() instead.")
open val coroutineContext = Dispatchers.Default
@Suppress("DEPRECATION")
final override fun startWork(): ListenableFuture<Result> {
val coroutineScope = CoroutineScope(coroutineContext + job)
coroutineScope.launch {
try {
val result = doWork()
future.set(result)
} catch (t: Throwable) {
future.setException(t)
}
}
return future
}
というコードがあり、 Dispatchers.Default
だということがわかります。
Threading in CoroutineWorker
の記事によると、
override val coroutineContext = Dispatchers.IO
を実装する Worker で override すれば、スレッドを切り替えられると書かれています。
(だが、 deprecated...)
コード中にコメントを見るに、 withContext
を使って
override suspend fun doWork(): Result = withContext(Dispatchers.IO) {
Thread.sleep(1000)
Log.d("★", "param = ${inputData.param} on ${Thread.currentThread().name}")
Result.success()
}
と Dispatchers を指定するのが良さそうです。
実行結果↓
D/★: param = 1 on DefaultDispatcher-worker-1
D/★: param = 2 on DefaultDispatcher-worker-2
D/★: param = 3 on DefaultDispatcher-worker-3
D/★: param = 4 on DefaultDispatcher-worker-5
D/★: param = 5 on DefaultDispatcher-worker-6
D/★: param = 6 on DefaultDispatcher-worker-8
D/★: param = 7 on DefaultDispatcher-worker-7
D/★: param = 8 on DefaultDispatcher-worker-4
D/★: param = 9 on DefaultDispatcher-worker-9
D/★: param = 10 on DefaultDispatcher-worker-10
(約1秒)
少なくとも 10 並列はありそう。
どっと数を上げてみたところ、
D/★: param = 1 on DefaultDispatcher-worker-1
D/★: param = 2 on DefaultDispatcher-worker-2
D/★: param = 3 on DefaultDispatcher-worker-3
D/★: param = 4 on DefaultDispatcher-worker-5
D/★: param = 5 on DefaultDispatcher-worker-6
D/★: param = 6 on DefaultDispatcher-worker-4
D/★: param = 7 on DefaultDispatcher-worker-7
D/★: param = 8 on DefaultDispatcher-worker-10
D/★: param = 9 on DefaultDispatcher-worker-8
D/★: param = 10 on DefaultDispatcher-worker-13
D/★: param = 11 on DefaultDispatcher-worker-12
D/★: param = 12 on DefaultDispatcher-worker-14
D/★: param = 13 on DefaultDispatcher-worker-15
D/★: param = 14 on DefaultDispatcher-worker-16
D/★: param = 15 on DefaultDispatcher-worker-11
D/★: param = 16 on DefaultDispatcher-worker-19
D/★: param = 17 on DefaultDispatcher-worker-17
D/★: param = 18 on DefaultDispatcher-worker-20
D/★: param = 19 on DefaultDispatcher-worker-21
D/★: param = 20 on DefaultDispatcher-worker-22
(約1秒)
D/★: param = 21 on DefaultDispatcher-worker-11
D/★: param = 22 on DefaultDispatcher-worker-10
D/★: param = 23 on DefaultDispatcher-worker-15
D/★: param = 24 on DefaultDispatcher-worker-12
D/★: param = 25 on DefaultDispatcher-worker-7
D/★: param = 26 on DefaultDispatcher-worker-14
D/★: param = 27 on DefaultDispatcher-worker-4
D/★: param = 28 on DefaultDispatcher-worker-21
D/★: param = 29 on DefaultDispatcher-worker-3
D/★: param = 30 on DefaultDispatcher-worker-17
...
となりました。
Dispatchers.IO
だと20並列で実行できました。
詳細は追い切れていませんが、 CoroutineWorker ではない Worker のときは CPU 数に応じた並列度だったので、 並列度は実行環境に依存する可能性が大いに考えられます。
(並列度を決定する処理を見つけたら紹介します。)
とはいえ、20並列で CPU をがっつり使うような処理を行って良いような気がしません。。。
どの Dispatchers を指定すれば良いのか?
ちゃんと方針が示されていました。
Improve app performance with Kotlin coroutines の記事中の Use coroutines for main-safety には、
との記載があるので、 用法用量を守って正しい Context を指定しましょう。
余談
Dispatchers.Default
を初期化する処理を追ってみると、
public actual object Dispatchers {
...
@JvmStatic
public actual val Default: CoroutineDispatcher = createDefaultDispatcher()
...
}
internal const val COROUTINES_SCHEDULER_PROPERTY_NAME = "kotlinx.coroutines.scheduler"
internal val useCoroutinesScheduler = systemProp(COROUTINES_SCHEDULER_PROPERTY_NAME).let { value ->
when (value) {
null, "", "on" -> true
"off" -> false
else -> error("System property '$COROUTINES_SCHEDULER_PROPERTY_NAME' has unrecognized value '$value'")
}
}
internal actual fun createDefaultDispatcher(): CoroutineDispatcher =
if (useCoroutinesScheduler) DefaultScheduler else CommonPool
というコードがあったので、 SystemProperty の設定次第では並列度が変わる可能性もありますね。