Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Coroutinesを中心としたAndroidアプリでの並行数制限・排他制御

k-tomoyasu
December 19, 2024

 Coroutinesを中心としたAndroidアプリでの並行数制限・排他制御

2024/12/19 pixiv App Night発表資料

k-tomoyasu

December 19, 2024
Tweet

More Decks by k-tomoyasu

Other Decks in Programming

Transcript

  1. ϑΝΠϧμ΢ϯϩʔμʔ class FileDownloader(private val urlList: List<URL>) { suspend fun downloadFiles()

    = withContext(Dispatchers.IO) { urlList.forEach { download(it) } } } suspend fun download(url: URL) { // ダウンロード処理 }
  2. ฒߦͰμ΢ϯϩʔυ class FileDownloader(private val urlList: List<URL>) { suspend fun downloadFiles()

    = withContext(Dispatchers.IO) { urlList.forEach { launch { download(it) } } } } ʮ3ϑΝΠϧ·ͰʯฒߦͰμ΢ϯϩʔυͱ͍͏࢓༷Λຬ͍ͨͯ͠ͳ͍
  3. Semaphore • Semaphore(permits: Int)ͰڐՄ͢Δॲཧͷ਺Λࢦఆ͢Δ • ओͳϝιου • acquire() • ڐՄΛ1ͭऔಘɻऔಘͰ͖ͳ͍৔߹ɺڐՄ͕ղ์͞ΕΔ·Ͱ଴ػ

    • suspendؔ਺ʹͳ͓ͬͯΓ଴ػ࣌ʹεϨουΛϒϩοΫ͠ͳ͍ • release() • ڐՄΛ1ͭղ์͢Δ • withPermit() • acquireΛݺͼग़͠ɺ౉ͨ͠lambdaΛ࣮ߦ͢ΔͱࣗಈͰrelease(ྫ֎ൃੜ࣌΋release͞ΕΔ)
  4. SemaphoreͰฒߦॲཧ਺Λ੍ݶ class FileDownloader(private val urlList: List<URL>) { val maxDownloadsSemaphore =

    Semaphore(permits = 3) suspend fun downloadFiles() = withContext(Dispatchers.IO) { urlList.forEach { launch { maxDownloadsSemaphore.withPermit { download(it) } } } } } SemaphoreͰڐՄ͢Δॲཧͷ਺Λࢦఆ withPermitͰμ΢ϯϩʔυॲཧ ڐՄ͞Εͨॲཧ਺Λ௒͑Δ৔߹͸ɺ ॲཧ਺ͷۭ͖͕Ͱ͖Δ·Ͱதஅ͞ΕΔ
  5. ࢦఆͨ͠ཁૉΛઌ಄ʹҠಈ͢ΔQueue class UrlQueue(urlList: List<URL>) { private val urlList = urlList.toMutableList()

    // 先頭のURLを取り出す(ダウンローダーはpollを呼び出して順次ダウンロードする) fun poll(): URL? = urlList.removeFirstOrNull() // 指定したURLを先頭に移動する(優先ダウンロード) fun moveToHead(url: URL) { if (urlList.remove(url)) { urlList.add(0, url) } } }
  6. ෳ਺ͷcoroutine͔ΒQueueΛૢ࡞ class FileDownloader(private val urlList: List<URL>) { val maxDownloadsSemaphore =

    Semaphore(permits = 3) val urlQueue = UrlQueue(urlList) suspend fun downloadFiles() = withContext(Dispatchers.IO) { repeat(urlList.size) { launch { maxDownloadsSemaphore.withPermit { val url = urlQueue.poll() ?: return@launch download(url) } } } } fun prioritizeUrl(url: URL) { urlQueue.moveToHead(url) } } class UrlQueue(urlList: List<URL>) { private val urlList = urlList.toMutableList() fun poll(): URL? = urlList.removeFirstOrNull() fun moveToHead(url: URL) { if (urlList.remove(url)) { urlList.add(0, url) } } }
  7. ෳ਺ͷcoroutine͔ΒQueueΛૢ࡞ class FileDownloader(private val urlList: List<URL>) { val maxDownloadsSemaphore =

    Semaphore(permits = 3) val urlQueue = UrlQueue(urlList) suspend fun downloadFiles() = withContext(Dispatchers.IO) { repeat(urlList.size) { launch { maxDownloadsSemaphore.withPermit { val url = urlQueue.poll() ?: return@launch download(url) } } } } fun prioritizeUrl(url: URL) { urlQueue.moveToHead(url) } } class UrlQueue(urlList: List<URL>) { private val urlList = urlList.toMutableList() fun poll(): URL? = urlList.removeFirstOrNull() fun moveToHead(url: URL) { if (urlList.remove(url)) { urlList.add(0, url) } } } Queueͷૢ࡞͕εϨουηʔϑͰ͸ͳ͍
  8. σʔλڝ߹ͱڝ߹ঢ়ଶ • σʔλڝ߹(Data Race) • ಉ࣌ʹڞ༗σʔλʹΞΫηε͠ɺগͳ͘ͱ΋1͕ͭॻ͖ࠐΈΛߦ͏৔߹ʹൃ ੜ͢Δ • ى͖Δ͜ͱ: ༧ଌෆೳͳ໰୊͕ى͜Δ

    • ڝ߹ঢ়ଶ(Race Condition) • ࣮ߦॱং΍λΠϛϯάʹґଘͨ͠ҙਤ͠ͳ͍ग़ྗͷมԽ • ى͖Δ͜ͱɿಉ͡ೖྗͰ΋ҟͳΔ݁ՌΛग़ྗ͢Δɾ݁Ռͷ੔߹ੑ͕औΕͳ͍
  9. εϨουηʔϑͰͳ͍ॲཧ class UrlQueue(urlList: List<URL>) { private val urlList = urlList.toMutableList()

    fun poll(): URL? = urlList.removeFirstOrNull() … } Coroutine A の動作 Coroutine B の動作 urlList の状態 1 poll() を呼び出し先頭を読む(URL1) poll() を呼び出し先頭を読む (URL1) [URL1, URL2, URL3] 2 先頭を削除(URL1) [URL2, URL3] 3 URL1を返す 先頭を削除(URL2) [URL3] 4 URL1を返す
  10. εϨουηʔϑͰͳ͍ॲཧ class UrlQueue(urlList: List<URL>) { private val urlList = urlList.toMutableList()

    fun poll(): URL? = urlList.removeFirstOrNull() … } Coroutine A の動作 Coroutine B の動作 urlList の状態 1 poll() を呼び出し先頭を読む(URL1) poll() を呼び出し先頭を読む (URL1) [URL1, URL2, URL3] 2 先頭を削除(URL1) [URL2, URL3] 3 URL1を返す 先頭を削除(URL2) [URL3] 4 URL1を返す
  11. εϨουηʔϑͰͳ͍ॲཧ class UrlQueue(urlList: List<URL>) { private val urlList = urlList.toMutableList()

    fun poll(): URL? = urlList.removeFirstOrNull() … } Coroutine A の動作 Coroutine B の動作 urlList の状態 1 poll() を呼び出し先頭を読む(URL1) poll() を呼び出し先頭を読む (URL1) [URL1, URL2, URL3] 2 先頭を削除(URL1) [URL2, URL3] 3 URL1を返す 先頭を削除(URL2) [URL3] 4 URL1を返す
  12. ڝ߹ঢ়ଶ͕ൃੜ͠ɺظ଴ͱҟͳΔ݁Ռʹ class UrlQueue(urlList: List<URL>) { private val urlList = urlList.toMutableList()

    fun poll(): URL? = urlList.removeFirstOrNull() … } Coroutine A の動作 Coroutine B の動作 urlList の状態 1 poll() を呼び出し先頭を読む(URL1) poll() を呼び出し先頭を読む (URL1) [URL1, URL2, URL3] 2 先頭を削除(URL1) [URL2, URL3] 3 URL1を返す 先頭を削除(URL2) [URL3] 4 URL1を返す
  13. Mutex • ϩοΫΛऔಘ͠ɺಛఆͷίʔυ͕ಉ࣌ʹ࣮ߦ͞ΕΔ͜ͱΛ๷͙ • ओͳϝιου • lock() • ϩοΫΛऔΔɻϩοΫ͕औΕͳ͍৔߹ɺϩοΫղআ͞ΕΔ·Ͱ଴ػ •

    suspendؔ਺ʹͳ͓ͬͯΓɺ଴ػ࣌ʹεϨουΛϒϩοΫ͠ͳ͍ • unlock() • ϩοΫΛղআ • withLock() • lockΛݺͼग़͠ɺ౉ͨ͠lambdaΛ࣮ߦ͢ΔͱࣗಈͰunlock(ྫ֎ൃੜ࣌΋unlock)
  14. MutexͰഉଞ੍ޚ class UrlQueue(urlList: List<URL>) { private val urlList = urlList.toMutableList()

    private val safeAccessMutex = Mutex() suspend fun poll(): URL? = safeAccessMutex.withLock { urlList.removeFirstOrNull() } suspend fun moveToHead(url: URL) = safeAccessMutex.withLock { if (urlList.remove(url)) { urlList.add(0, url) } } }
  15. MutexͰഉଞ੍ޚ class UrlQueue(urlList: List<URL>) { private val urlList = urlList.toMutableList()

    private val safeAccessMutex = Mutex() suspend fun poll(): URL? = safeAccessMutex.withLock { urlList.removeFirstOrNull() } suspend fun moveToHead(url: URL) = safeAccessMutex.withLock { if (urlList.remove(url)) { urlList.add(0, url) } } } MutexͰϩοΫΛऔΓಉ࣌ʹϦιʔεʹ ΞΫηε͞Εͳ͍Α͏ʹ͢Δ(ഉଞ੍ޚ)
  16. ผղ: CoroutineScope.actor sealed class Message { data class Poll(val result:

    CompletableDeferred<URL?>) : Message() data class MoveToHead(val url: URL) : Message() } @OptIn(ObsoleteCoroutinesApi::class) fun CoroutineScope.urlQueueActor(urlList: List<URL>) = actor<Message> { val urlList = urlList.toMutableList() for (msg in channel) { when (msg) { is Message.Poll -> msg.result.complete(urlList.removeFirstOrNull()) is Message.MoveToHead -> { if (urlList.remove(msg.url)) { urlList.add(0, msg.url) } } } } }
  17. ผղ: CoroutineScope.actor sealed class Message { data class Poll(val result:

    CompletableDeferred<URL?>) : Message() data class MoveToHead(val url: URL) : Message() } @OptIn(ObsoleteCoroutinesApi::class) fun CoroutineScope.urlQueueActor(urlList: List<URL>) = actor<Message> { val urlList = urlList.toMutableList() for (msg in channel) { when (msg) { is Message.Poll -> msg.result.complete(urlList.removeFirstOrNull()) is Message.MoveToHead -> { if (urlList.remove(msg.url)) { urlList.add(0, msg.url) } } } } } actorͰड͚औΔϝοηʔδΛఆٛ
  18. ผղ: CoroutineScope.actor sealed class Message { data class Poll(val result:

    CompletableDeferred<URL?>) : Message() data class MoveToHead(val url: URL) : Message() } @OptIn(ObsoleteCoroutinesApi::class) fun CoroutineScope.urlQueueActor(urlList: List<URL>) = actor<Message> { val urlList = urlList.toMutableList() for (msg in channel) { when (msg) { is Message.Poll -> msg.result.complete(urlList.removeFirstOrNull()) is Message.MoveToHead -> { if (urlList.remove(msg.url)) { urlList.add(0, msg.url) } } } } } ϝοηʔδΛड৴ͯ͠ॲཧ ௚ྻͰॲཧ͞ΕΔͷͰεϨουηʔϑ
  19. ผղ: CoroutineScope.actor class UrlQueueActor(scope: CoroutineScope, urlList: List<URL>) { private val

    queueActor = scope.urlQueueActor(urlList) suspend fun poll(): URL? { val result = CompletableDeferred<URL?>() queueActor.send(Message.Poll(result)) return result.await() } suspend fun moveToHead(url: URL) { queueActor.send(Message.MoveToHead(url)) } } actorͷ໭Γ஋ͷChannelʹ ϝοηʔδΛૹ৴
  20. StateFlowͰμ΢ϯϩʔυ׬ྃ਺Λ௨஌ private val _downloadedCount = MutableStateFlow(0) val downloadedCount: StateFlow<Int> =

    _downloadedCount suspend fun downloadFiles() = withContext(Dispatchers.IO) { repeat(urlList.size) { launch { maxDownloadsSemaphore.withPermit { val url = urlQueue.poll() ?: return@launch download(url) _downloadedCount.value = _downloadedCount.value + 1 } } } } StateFlow.value͸ಡΈࠐΈɾॻ͖ࠐΈ͕ͦΕͧΕεϨουηʔϑʹͳ͍ͬͯΔͷͰɺ σʔλڝ߹͸ى͜Βͳ͍ɻ͔͠͠ಡΈࠐΈɾॻ͖ࠐΈ͕ΞτϛοΫͳૢ࡞Ͱ͸ͳ͍ͨΊɺ ࣮ߦॱʹΑͬͯ͸ڝ߹ঢ়ଶͱͳΔ
  21. StateFlowʹΑΔεϨουηʔϑͳঢ়ଶߋ৽ private val _downloadedCount = MutableStateFlow(0) val downloadedCount: StateFlow<Int> =

    _downloadedCount suspend fun downloadFiles() = withContext(Dispatchers.IO) { repeat(urlList.size) { launch { maxDownloadsSemaphore.withPermit { val url = urlQueue.poll() ?: return@launch download(url) _downloadedCount.update { it + 1 } } } } } StateFlow.update͸ಡΈࠐΈɾॻ͖ࠐΈ͕ ΞτϛοΫʹͳ͓ͬͯΓɺڝ߹ঢ়ଶΛճආͰ͖Δ
  22. ׬ྃ਺Λ௨஌͢ΔFlowΛฦ͢ύλʔϯ fun downloadFiles() = channelFlow { val downloadedCount = AtomicInteger(0)

    repeat(urlList.size) { launch { maxDownloadsSemaphore.withPermit { val url = urlQueue.poll() ?: return@launch download(url) send(downloadedCount.incrementAndGet()) } } } }.flowOn(Dispatchers.IO)
  23. ׬ྃ਺Λ௨஌͢ΔFlowΛฦ͢ύλʔϯ import kotlinx.atomicfu.atomic … fun downloadFiles() = channelFlow { val

    downloadedCount = atomic(0) repeat(urlList.size) { launch { maxDownloadsSemaphore.withPermit { val url = urlQueue.poll() ?: return@launch download(url) send(downloadedCount.incrementAndGet()) } } } }.flowOn(Dispatchers.IO) kotlinx.atomicfu.AtomicIntͰ εϨουηʔϑͳߋ৽