Параллельность котлин сопрограммы и кэшированное значение - PullRequest
5 голосов
/ 19 сентября 2019

В настоящее время я пытаюсь создать слой кэширования вокруг веб-запроса.До сих пор я писал:

class Repository(private val webServices: WebServices) {
  private var cachedItems: List<Item>? = null

  suspend fun getItems(): List<Item> {
    cachedItems?.let { return it }

    val items = withContext(Dispatchers.IO) { webServices.getItems() }
    cachedItems = items
    return items
  }
}

Меня беспокоит то, что произойдет, когда getItems() вызывается двумя вызывающими одновременно.В идеале я бы хотел, чтобы выполнялся только один веб-запрос.Каков рекомендуемый подход для решения этой проблемы при использовании сопрограмм?

Ответы [ 5 ]

1 голос
/ 19 сентября 2019

Вот простое решение.

class Repository(private val webServices: WebServices) {
  private val cachedItems = async(Dispatchers.IO, start = CoroutineStart.LAZY) {
    webServices.getItems()
  }

  suspend fun getItems(): List<Item> {
    return cachedItems.await()
  }
}
1 голос
/ 19 сентября 2019

Прежде всего, я думаю, вы должны просто использовать кеш-библиотеку, например Caffeine .Как правило, не рекомендуется изобретать велосипед, особенно с кэшированием, в котором много движущихся частей.Я бы не рекомендовал lazy, если вы хотите кеш , поскольку lazy не поддерживает удаление кеша и тому подобные вещи.

Что касается сопрограмм, то вы хотите сделать одинпотоковый диспетчер, который вы можете создать из Java Executor следующим образом: Executors.newSingleThreadExecutor().asCoroutineDispatcher().Вы также можете использовать actor s для этой цели, чтобы получить тот же результат.

Если вы хотите узнать, как работают сопрограммы в целом, я рекомендую эту статью разработчиками Kotlin итакже большой разговор Романа Елизарова на тему!

0 голосов
/ 19 сентября 2019

Это мое решение для вашего случая:

class Repository(private val webServices: WebServices) {
    private val mutex = Mutex()

    private val items = mutableListOf<String>()

    suspend fun getItems(): List<String> {
        if (items.isNotEmpty()) {
            return items
        }

        if (!mutex.isLocked) {
            mutex.lock()
            val newItems = webServices.getItems()
            items.clear()
            items.addAll(newItems)
            mutex.unlock()
        } else {
            // not beauty code, it's still open question for me - how to improve it
            mutex.lock()
            mutex.unlock()
        }
        return items
    }
}

Есть полезная статья о сопрограммах и параллелизме.И это документация для Mutex класс

0 голосов
/ 19 сентября 2019

Самый простой способ - использовать lazy, который синхронизируется под крышками примерно так:

class Repository(private val webServices: WebServices) {
  private val cachedItems by lazy { webServices.getItems() }

  suspend fun getItems(): List<Item> {
    return withContext(Dispatchers.IO) { cachedItems }
  }
}
0 голосов
/ 19 сентября 2019

Я не знаю, как это взаимодействует с сопрограммами.Но в обычном кодировании:

Это всегда проблема с отложенной выборкой.Мне известны два основных подхода: либо вызов использует какую-либо форму синхронизации, либо рисковать множественными выборками в качестве наказания за избежание блокировки (как в вашем примере).

Первый случай болеесложный для написания, но не всегда синхронизируемый;Вы можете проверить кэшированное значение и, если оно найдено, использовать его;иначе вы можете получить блокировку и снова проверить , извлекая значение только в том случае, если оно все еще отсутствует.Такая двойная проверка блокировки довольно сложна, но после того, как значение было выбрано, опубликовано и устаревшие значения кэша истекли, ему больше не нужно использовать блокировку.

Но вместо записи этогоСам Kotlin сделал это за вас - вы можете просто объявить свойство как делегированное ленивым , и позволить stdlib выполнить всю тяжелую работу!Он даже позволяет вам указать, какой режим безопасности потока использовать.

...