Coroutines On Android (part III): Real work(协程在Android上的使用三:真实案列)

前情提要:本文作者是谷歌公司的大牛,其实这一篇文章主要讲了如何用协程解决多线程并发的问题,有3中方法。1、是取消上一个任务;2、是排队执行任务;3、是重用上一个任务。非常值得看看。

 

This is part of a multi-part series about using Coroutines on Android. This post focuses on solving practical problems using coroutines by implementing one shot requests.

Other articles in this series:

Coroutines on Android (part I): Getting the background

Coroutines on Android (part II): Getting started

Solving real-world problems with coroutines

Part one and two of this series focused on how coroutines can be used to simplify code, provide main-safety on Android, and avoid leaking work. With that background, they look like a great solution to both background processing and a way to simplify callback based code on Android.

So far, we’ve focused on what coroutines are and how to manage them. In this post we’ll look at how to use them to accomplish some real tasks. Coroutines are a general purpose programming language feature at the same level as functions — so you can use them to implement anything that you could with functions and objects. However, there are two types of tasks that come up all the time in real code that coroutines are a great solution for:

  1. One shot requests are requests that are run each time they are called — they always complete after the result is ready.
  2. Streaming requests are requests that continue to observe changes and report them to caller — they don’t complete when the first result is ready.

(协程特别擅长解决2类问题:1、单次请求;2、连续请求)

Coroutines are a great solution to both of these tasks. In this post, we’ll look deeply into one shot requests and explore how to implement them using coroutines on Android.

One shot requests

A one shot request is performed once each time it’s called and completes as soon as a result is ready. This pattern is the same as a regular function call — it gets called, does some work, then returns. Due to the similarity to function calls they tend to be easier to understand than streaming requests.

A one shot request is performed each time it’s called. It stops executing as soon as a result is ready.(单次请求和普通方法差不多)

For an example of a one shot request, consider how your browser loaded this page. When you clicked the link to this post your browser sent a network request to the server to load the page. Once the page was transferred to your browser it stopped talking to the backend — it had all the data it needed. If the server modified the post, the new changes would not be shown in your browser — you would have to refresh the page.

(单次请求和请求网页差不多,获取到网页就完事了,网页内容数据在服务器再更改的话,浏览器也不会得到通知)

So, while they lack the live-push of streaming requests, one shot requests are pretty powerful. There’s a whole lot of things you can do in an Android app that can be solved by one shot requests like fetching, storing, or updating data. It’s also a good pattern for things like sorting a list.

Problem: Displaying a sorted list

Let’s explore one-shot requests by looking at how you might display a sorted list. To make the example concrete, let’s build an inventory存货 app for use by an employee at a store. It will be used to lookup products based on when they were last stocked — they’ll want to be able to sort the list both ascending and descending. It has so many products that sorting it may take almost a second — so we’ll use coroutines to avoid blocking the main thread!

In this app all of the products are stored in a Room database. This is a good use case to explore since it doesn’t need to involve a network request so we can focus on the pattern. Even though the example is simpler because it doesn’t use the network, it exposes the patterns needed to implement one shot requests.

To implement this request using coroutines, you will introduce coroutines to the ViewModelRepository, and Dao. Lets walk through each one at a time and see how to integrate them with coroutines.

class ProductsViewModel(val productsRepository: ProductsRepository): ViewModel() {
   private val _sortedProducts = MutableLiveData<List<ProductListing>>()
   val sortedProducts: LiveData<List<ProductListing>> = _sortedProducts

   /**
    * Called by the UI when the user clicks the appropriate sort button
    */
   fun onSortAscending() = sortPricesBy(ascending = true)
   fun onSortDescending() = sortPricesBy(ascending = false)

   private fun sortPricesBy(ascending: Boolean) {
       viewModelScope.launch {
           // suspend and resume make this database request main-safe
           // so our ViewModel doesn't need to worry about threading
           _sortedProducts.value =
                   productsRepository.loadSortedProducts(ascending)
       }
   }
}

ProductsViewModel is responsible for receiving events from the UI layer, then asking the repository for the updated data. It uses LiveData to hold the currently sorted list to for display by the UI. When a new event comes in sortProductsBy starts a new coroutine to sort the list and updates the LiveData when the result is ready. The ViewModel is typically the right place to start most coroutines in this architecture, since it can cancel the coroutine in onCleared. If the user leaves the screen they usually have no use for outstanding work.

If you haven’t used LiveData much, check out this great post by @CeruleanOtter introducing how they work to store data for UIs.

ViewModels : A Simple Example

This is a general pattern for coroutines on Android. Since the Android framework doesn’t call suspend functions, you’ll need to coordinate with a coroutine in response to a UI event. The easiest way to do that is to just start a new coroutine when the event comes in — and the natural place to do that is in the ViewModel.

As a general pattern, start coroutines in the ViewModel.

The ViewModel uses a ProductsRepository to actually fetch the data. Here’s what that looks like:

class ProductsRepository(val productsDao: ProductsDao) {

  /**
    * This is a "regular" suspending function, which means the caller must
    * be in a coroutine. The repository is not responsible for starting or 
    * stopping coroutines since it doesn't have a natural lifecycle to cancel
    * unnecessary work.
    *
    * This *may* be called from Dispatchers.Main and is main-safe because
    * Room will take care of main-safety for us.
    */
   suspend fun loadSortedProducts(ascending: Boolean): List<ProductListing> {
       return if (ascending) {
           productsDao.loadProductsByDateStockedAscending()
       } else {
           productsDao.loadProductsByDateStockedDescending()
       }
   }
}

ProductsRepository provides a reasonable interface for interacting with products. In this app, since everything is in the local Room database, it just provides a nice interface for the @Dao that has two different functions for the different sort orders.

The repository is an optional part of the Android Architecture Components architecture — but if you do have it or a similar layer in your app, it should prefer to expose regular suspend functions. Since a repository doesn’t have a natural lifecycle — it’s just an object — it would have no way to cleanup work. As a result, any coroutines started in the repository will leak by default.

(Domain层获取数据用的挂起方法因为没有生命周期的概念,所以天然容易泄露协程,所以由上层也就是调用层负责取消协程。)

In addition to avoiding leaks, by exposing regular suspend functions, it’s easy to re-use the repository in different contexts. Anything that knows how to make a coroutine can call loadSortedProducts. For example, a background job scheduled by the WorkManager library could call this directly.

A repository should prefer to expose regular suspend functions that are main-safe.

Note: Some background save operations may want to continue after user leaves a screen — and it makes sense to have those saves run without a lifecycle. In most other cases the viewModelScope is a reasonable choice.

Moving on to ProductsDao, it looks like this:

@Dao
interface ProductsDao {
   // Because this is marked suspend, Room will use it's own dispatcher
   //  to run this query in a main-safe way.
   @Query("select * from ProductListing ORDER BY dateStocked ASC")
   suspend fun loadProductsByDateStockedAscending(): List<ProductListing>

   // Because this is marked suspend, Room will use it's own dispatcher
   //  to run this query in a main-safe way.
   @Query("select * from ProductListing ORDER BY dateStocked DESC")
   suspend fun loadProductsByDateStockedDescending(): List<ProductListing>
}        

ProductsDao is a Room @Dao that exposes two suspend functions. Because the functions are marked suspend, Room ensures they are main-safe. That means you can call them directly from Dispatchers.Main.

If you haven’t seen coroutines in Room yet, check out this great post by @FMuntenescu

Room 🔗 Coroutines

A bit of warning though, the coroutine that calls this will be on the main thread. So if you did something expensive with the results — like transforming them to a new list — you should make sure you’re not blocking the main thread.

Note: Room uses its own dispatcher to run queries on a background thread. Your code should not use withContext(Dispatchers.IO) to call suspending room queries. It will complicate the code and make your queries run slower.

Suspend functions in Room are main-safe and run on a custom dispatcher.(Room有自己的协程调度器)

The one shot request pattern

That’s the complete pattern for making a one shot request using coroutines in Android Architecture Components. We added coroutines to the ViewModelRepository, and Room and each layer has a different responsibility.

  1. ViewModel launches a coroutine on the main thread — it completes when it has a result.
  2. Repository exposes regular suspend functions and ensures they are main-safe.
  3. The database and network expose regular suspend functions and ensures they are main-safe.

The ViewModel is responsible for starting coroutines and ensuring that they get cancelled if the user leaves the screen. It doesn’t do expensive things— instead relying on other layers to do the heavy work. Once it has the result it sends it to the UI using LiveData.

Since the ViewModel doesn’t do heavy work it starts the coroutine on the main thread. By starting on main it can respond to user events faster if the result is available immediately (e.g. from an in-memory cache).

The Repository exposes regular suspend functions to access data. It typically doesn’t start it’s own long lived coroutines since it doesn’t have any way to cancel them. Whenever the Repository has to do expensive things like transform a list it should use withContext to expose a main-safe interface.

(数据仓库也就是Domain层想做任何耗时任务,都需要自己切换线程)

The data layer (network or database) always exposes regular suspend functions. It is important that these suspend functions are main-safe when using Kotlin coroutines, and both Room and Retrofit follow this pattern.

In a one shot request, the data layer only exposes suspend functions. A caller has to call them again if they want a new value. This is just like the refresh button on your web browser.

It’s worth taking a moment to make sure you understand these patterns for one shot requests. It’s the normal pattern for coroutines on Android, and you’ll use it all of the time.

Our first bug report!

After testing that solution, you launch it to production and everything is going well for weeks until you get a really strange bug report:

Subject: 🐞 — wrong sort order!

Report: When I click the sort order buttons really really really really quickly, sometimes the sort is wrong. This doesn’t happen all the time 🙃.

You take a look and scratch your head. What could possibly go wrong? The algorithm seems fairly simple:

  1. Start the sort the user requested.
  2. Run the sort in the Room dispatcher.
  3. Show the result of the sort.

You’re tempted to close the bug “wontfix — don’t press the buttons so fast” but you’re worried something may be broken. After adding logging statements and writing a test to call lots of sorts at once— you finally figure it out!

It turns out the result shown isn’t actually the “result of the sort,” it’s actually the result of the “last sort to complete.” When the user spams群发 the button — they start multiple sorts at the same time and they can finish in any order!

When starting a new coroutine in response to a UI event, consider what happens if the user starts another before this one completes.

This is a concurrency bug and it doesn’t really have anything to do with coroutines. We’d have the same bug if we used callbacks, Rx, or even an ExecutorService the same way.

(这是一个并发引起的bug,2个线程同时操作了排序,结果就乱了)

There are many many ways to fix this in both the ViewModel and the Repository. Let’s explore some patterns for ensuring that one shot requests complete in the order the user expects.

The best solution: Disable the button

The fundamental problem is that we’re doing two sorts. We can fix that by making it only do one sort! The easiest way to do that is to disable the sort buttons to stop the new events.

This may seem like a simple solution, but it’s a really good idea. The code to implement this is simple, easy to test, and as long as it makes sense in the UI it’ll completely fix the problem!

To disable the buttons, tell the UI that a sort request is happening inside of sortPricesBy like this:

// Solution 0: Disable the sort buttons when any sort is running

class ProductsViewModel(val productsRepository: ProductsRepository): ViewModel() {
   private val _sortedProducts = MutableLiveData<List<ProductListing>>()
   val sortedProducts: LiveData<List<ProductListing>> = _sortedProducts
  
   private val _sortButtonsEnabled = MutableLiveData<Boolean>()
   val sortButtonsEnabled: LiveData<Boolean> = _sortButtonsEnabled
  
   init {
       _sortButtonsEnabled.value = true
   }

   /**
    * Called by the UI when the user clicks the appropriate sort button
    */
   fun onSortAscending() = sortPricesBy(ascending = true)
   fun onSortDescending() = sortPricesBy(ascending = false)

   private fun sortPricesBy(ascending: Boolean) {
       viewModelScope.launch {
           // disable the sort buttons whenever a sort is running
           _sortButtonsEnabled.value = false
           try {
               _sortedProducts.value =
                       productsRepository.loadSortedProducts(ascending)
           } finally {
               // re-enable the sort buttons after the sort is complete
               _sortButtonsEnabled.value = true
           }
       }
   }
}

Disabling the buttons while a sort runs using _sortButtonsEnabled in sortPricesBy.

Ok that one was not too bad. Just disable the buttons inside of sortPricesBy around the call to the repository.

And in most cases it’s the right way to fix this problem. But what if we wanted to leave the buttons enabled and fix the bug? That’s a bit harder, and we’ll spend the rest of this post exploring a few different options.

Important: This code shows a major advantage of starting on main — the buttons disable instantly in response to a click. If you switched dispatchers, a fast-fingered user on a slow phone could send more than one click!

Concurrency patterns

The next few sections explore advanced topics — and if you’re just starting with coroutines you don’t need to understand them right away. Simply disabling the button is the best solution to most problems you’ll run across.

For the rest of this post, we’ll explore ways to use coroutines to leave the button enabled but ensure that one shot requests are executed in an order that doesn’t surprise the user. We can do that by avoiding accidental concurrency by controlling when the coroutines run (or don’t run).

There are three basic patterns that you can use for a one shot request to ensure that exactly one request runs at a time.

  1. Cancel previous work before starting more.
  2. Queue the next work and wait for the previous requests to complete before starting another one.
  3. Join previous work if there’s already a request running just return that one instead of starting another request.

(有3种办法解决并发问题,1、取消前面的任务;2、排队执行任务;3、加入前面的任务,后面的不执行)

As you look through these solutions you’ll notice that they have some complexity to their implementations. To focus in on how to use these patterns instead of implementation details I’ve created a gist with implementations of all three patterns as reusable abstractions.

Solution #1: Cancel the previous work

In the case of sorting, getting a new event from the user often means you can cancel the last sort. After all, what’s the point of continuing if the user has already told you they don’t want the result?

To cancel the previous request, we’ll need to keep track of it somehow. The function cancelPreviousThenRun in the gist does exactly that.

Lets take a look at how it can be used to fix the bug:(源码见文末注释1)

// Solution #1: Cancel previous work

// This is a great solution for tasks like sorting and filtering that
// can be cancelled if a new request comes in.

class ProductsRepository(val productsDao: ProductsDao, val productsApi: ProductsService) {
   var controlledRunner = ControlledRunner<List<ProductListing>>()

   suspend fun loadSortedProducts(ascending: Boolean): List<ProductListing> {
       // cancel the previous sorts before starting a new one
       return controlledRunner.cancelPreviousThenRun {
           if (ascending) {
               productsDao.loadProductsByDateStockedAscending()
           } else {
               productsDao.loadProductsByDateStockedDescending()
           }
       }
   }
}

Using cancelPreviousThenRun to ensure that only one sort runs at a time.

Looking at the example implementation for cancelPreviousThenRun in the gist is a good way to see how to keep track of in-progress work.

// see the complete implementation at
// https://gist.github.com/objcode/7ab4e7b1df8acd88696cb0ccecad16f7
suspend fun cancelPreviousThenRun(block: suspend () -> T): T {
   // If there is an activeTask, cancel it because it's result is no longer needed
   activeTask?.cancelAndJoin()
   
   // ...

In a nutshell it always keeps track of the currently active sort in the member variable activeTask. Whenever a sort starts, it will immediately cancelAndJoin on whatever is currently in activeTask. This has the effect of cancelling any in progress sorts before starting a new one.

It’s a good idea to use abstractions similar to ControlledRunner<T> to encapsulate logic like this it instead of mixing ad-hoc concurrency with application logic.

Consider building abstractions to avoid mixing ad-hoc concurrency patterns with application code.

Important: This pattern is not well suited for use in global singletons, since unrelated callers shouldn’t cancel each other.(取消前一个任务不适合单例模式,因为在单例中不应该取消别人发起的任务)

Solution #2: Queue the next work

There’s one solution to concurrency bugs that always works.

Just queue up requests so only one thing can happen at a time! Just like a queue or a line at a store, requests will execute one at a time in the order they started.

For this particular problem of sorting, cancelling is probably better than queuing, but it’s worth talking about because it always works. (源码见文末注释2)

// Solution #2: Add a Mutex

// Note: This is not optimal for the specific use case of sorting
// or filtering but is a good pattern for network saves.

class ProductsRepository(val productsDao: ProductsDao, val productsApi: ProductsService) {
   val singleRunner = SingleRunner()

   suspend fun loadSortedProducts(ascending: Boolean): List<ProductListing> {
       // wait for the previous sort to complete before starting a new one
       return singleRunner.afterPrevious {
           if (ascending) {
               productsDao.loadProductsByDateStockedAscending()
           } else {
               productsDao.loadProductsByDateStockedDescending()
           }
       }
   }
}

Whenever a new sort comes in, it uses a instance of SingleRunner to ensure that only one sort is running at a time.

It uses a Mutex, which is a single ticket (or lock), that a coroutine must get in order to enter the block. If another coroutine tried while one was running, it would suspend itself until all pending coroutines were done with the Mutex.

(互斥锁是个好东西,它就像一个单张门票,只有一个协程可以运行,其他要排队获取门票)

A Mutex lets you ensure only one coroutine runs at a time — and they will finish in the order they started.

Solution 3: Join previous work

The third solution to consider is joining the previous work. It’s a good idea if the new request would re-start the exact same work that has already been half completed.

This pattern doesn’t make very much sense with the sort function, but it’s a natural fit for a network fetch that loads data.

For our product inventory app, the user will need a way to fetch a new product inventory from the server. As a simple UI, we’ll provide them with a refresh button that they can press to start a new network request.

Just like the sort buttons, simply disabling the button while the request is running is a complete solution to the problem. But if we didn’t — or couldn’t — do that, we could instead join the existing request.

Lets look at some code using joinPreviousOrRun from the gist for an example of how this might work:

class ProductsRepository(val productsDao: ProductsDao, val productsApi: ProductsService) {
   var controlledRunner = ControlledRunner<List<ProductListing>>()

   suspend fun fetchProductsFromBackend(): List<ProductListing> {
       // if there's already a request running, return the result from the 
       // existing request. If not, start a new request by running the block.
       return controlledRunner.joinPreviousOrRun {
           val result = productsApi.getProducts()
           productsDao.insertAll(result)
           result
       }
   }
}

This inverts使…反转 the behavior of cancelPreviousAndRun. Instead of discarding the previous request by cancelling it — it will discard the new request and avoid running it. If there’s already a request running, it waits for the result of current “in flight” request and returns that instead of running a new one. The block will only be executed if there was not already a request running.

You can see how this works at the start of joinPreviousOrRun — it just returns the previous result if there’s anything in activeTask:(源码见文末注释1)

// see the complete implementation at
// https://gist.github.com/objcode/7ab4e7b1df8acd88696cb0ccecad16f7#file-concurrencyhelpers-kt-L124

suspend fun joinPreviousOrRun(block: suspend () -> T): T {
    // if there is an activeTask, return it's result and don't run the block
    activeTask?.let {
        return it.await()
    }
    // ...

This pattern scales well for requests like fetching products by id. You could add a map from id to Deferred then use the same join logic to keep track of previous requests for the same product.

Join previous work is a great solution to avoiding repeated network requests.(重用前一个任务是个解决重复请求的好方法)

What’s next?

In this post we explored how to implement a one shot request using Kotlin coroutines. To start out we implemented a complete pattern showing how to start a coroutine in the ViewModel and then expose regular suspend functions from a Repository and Room Dao.

For most tasks, this is all you need to do in order to use Kotlin coroutines on Android. This pattern can be applied to many common tasks like sorting a list like we showed here. You can also use it to fetch, save, or update data on the network

We then looked at a subtle bug that can come up and possible solutions. The easiest (and often best) way to fix this is in the UI — just disable the sort buttons while a sort is in progress.

And wrapping up we looked at some advanced concurrency patterns and how to implement them in Kotlin coroutines. The code for this is a bit complex, but it does provide a good introduction to some advanced coroutines topics.

In the next post, we’ll take a look at streaming requests and explore how to use the liveData builder!

注释1 

/**
 * A controlled runner decides what to do when new tasks are run.
 *
 * By calling [joinPreviousOrRun], the new task will be discarded and the result of the previous task
 * will be returned. This is useful when you want to ensure that a network request to the same
 * resource does not flood.
 *
 * By calling [cancelPreviousThenRun], the old task will *always* be cancelled and then the new task will
 * be run. This is useful in situations where a new event implies that the previous work is no
 * longer relevant such as sorting or filtering a list.
 */
class ControlledRunner<T> {
    /**
     * The currently active task.
     *
     * This uses an atomic reference to ensure that it's safe to update activeTask on both
     * Dispatchers.Default and Dispatchers.Main which will execute coroutines on multiple threads at
     * the same time.
     */
    private val activeTask = AtomicReference<Deferred<T>?>(null)

    /**
     * Cancel all previous tasks before calling block.
     *
     * When several coroutines call cancelPreviousThenRun at the same time, only one will run and
     * the others will be cancelled.
     *
     * In the following example, only one sort operation will execute and any previous sorts will be
     * cancelled.
     *
     * ```
     * class Products {
     *    val controlledRunner = ControlledRunner<Product>()
     *
     *    fun sortAscending(): List<Product> {
     *        return controlledRunner.cancelPreviousThenRun { dao.loadSortedAscending() }
     *    }
     *
     *    fun sortDescending(): List<Product> {
     *        return controlledRunner.cancelPreviousThenRun { dao.loadSortedDescending() }
     *    }
     * }
     * ```
     *
     * @param block the code to run after previous work is cancelled.
     * @return the result of block, if this call was not cancelled prior to returning.
     */
    suspend fun cancelPreviousThenRun(block: suspend() -> T): T {
        // fast path: if we already know about an active task, just cancel it right away.
        activeTask.get()?.cancelAndJoin()

        return coroutineScope {
            // Create a new coroutine, but don't start it until it's decided that this block should
            // execute. In the code below, calling await() on newTask will cause this coroutine to
            // start.
            val newTask = async(start = LAZY) {
                block()
            }

            // When newTask completes, ensure that it resets activeTask to null (if it was the
            // current activeTask).
            newTask.invokeOnCompletion {
                activeTask.compareAndSet(newTask, null)
            }

            // Kotlin ensures that we only set result once since it's a val, even though it's set
            // inside the while(true) loop.
            val result: T

            // Loop until we are sure that newTask is ready to execute (all previous tasks are
            // cancelled)
            while(true) {
                if (!activeTask.compareAndSet(null, newTask)) {
                    // some other task started before newTask got set to activeTask, so see if it's
                    // still running when we call get() here. If so, we can cancel it.

                    // we will always start the loop again to see if we can set activeTask before
                    // starting newTask.
                    activeTask.get()?.cancelAndJoin()
                    // yield here to avoid a possible tight loop on a single threaded dispatcher
                    yield()
                } else {
                    // happy path - we set activeTask so we are ready to run newTask
                    result = newTask.await()
                    break
                }
            }
            
            // Kotlin ensures that the above loop always sets result exactly once, so we can return
            // it here!
            result
        }
    }

    /**
     * Don't run the new block if a previous block is running, instead wait for the previous block
     * and return it's result.
     *
     * When several coroutines call jonPreviousOrRun at the same time, only one will run and
     * the others will return the result from the winner.
     *
     * In the following example, only one network operation will execute at a time and any other
     * requests will return the result from the "in flight" request.
     *
     * ```
     * class Products {
     *    val controlledRunner = ControlledRunner<Product>()
     *
     *    fun fetchProducts(): List<Product> {
     *        return controlledRunner.joinPreviousOrRun {
     *            val results = api.fetchProducts()
     *            dao.insert(results)
     *            results
     *        }
     *    }
     * }
     * ```
     *
     * @param block the code to run if and only if no other task is currently running
     * @return the result of block, or if another task was running the result of that task instead.
     */
    suspend fun joinPreviousOrRun(block: suspend () -> T): T {
        // fast path: if there's already an active task, just wait for it and return the result
        activeTask.get()?.let {
            return it.await()
        }
        return coroutineScope {
            // Create a new coroutine, but don't start it until it's decided that this block should
            // execute. In the code below, calling await() on newTask will cause this coroutine to
            // start.
            val newTask = async(start = LAZY) {
                block()
            }

            newTask.invokeOnCompletion {
                activeTask.compareAndSet(newTask, null)
            }

            // Kotlin ensures that we only set result once since it's a val, even though it's set
            // inside the while(true) loop.
            val result: T

            // Loop until we figure out if we need to run newTask, or if there is a task that's
            // already running we can join.
            while(true) {
                if (!activeTask.compareAndSet(null, newTask)) {
                    // some other task started before newTask got set to activeTask, so see if it's
                    // still running when we call get() here. There is a chance that it's already
                    // been completed before the call to get, in which case we need to start the
                    // loop over and try again.
                    val currentTask = activeTask.get()
                    if (currentTask != null) {
                        // happy path - we found the other task so use that one instead of newTask
                        newTask.cancel()
                        result = currentTask.await()
                        break
                    } else {
                        // retry path - the other task completed before we could get it, loop to try
                        // setting activeTask again.

                        // call yield here in case we're executing on a single threaded dispatcher
                        // like Dispatchers.Main to allow other work to happen.
                        yield()
                    }
                } else {
                    // happy path - we were able to set activeTask, so start newTask and return its
                    // result
                    result = newTask.await()
                    break
                }
            }

            // Kotlin ensures that the above loop always sets result exactly once, so we can return
            // it here!
            result
        }
    }
}

注释2

/**
 * A helper class to execute tasks sequentially in coroutines.
 *
 * Calling [afterPrevious] will always ensure that all previously requested work completes prior to
 * calling the block passed. Any future calls to [afterPrevious] while the current block is running
 * will wait for the current block to complete before starting.
 */
class SingleRunner {
    /**
     * A coroutine mutex implements a lock that may only be taken by one coroutine at a time.
     */
    private val mutex = Mutex()

    /**
     * Ensure that the block will only be executed after all previous work has completed.
     *
     * When several coroutines call afterPrevious at the same time, they will queue up in the order
     * that they call afterPrevious. Then, one coroutine will enter the block at a time.
     *
     * In the following example, only one save operation (user or song) will be executing at a time.
     *
     * ```
     * class UserAndSongSaver {
     *    val singleRunner = SingleRunner()
     *
     *    fun saveUser(user: User) {
     *        singleRunner.afterPrevious { api.post(user) }
     *    }
     *
     *    fun saveSong(song: Song) {
     *        singleRunner.afterPrevious { api.post(song) }
     *    }
     * }
     * ```
     *
     * @param block the code to run after previous work is complete.
     */
    suspend fun <T> afterPrevious(block: suspend () -> T): T {
        // Before running the block, ensure that no other blocks are running by taking a lock on the
        // mutex.

        // The mutex will be released automatically when we return.

        // If any other block were already running when we get here, it will wait for it to complete
        // before entering the `withLock` block.
        mutex.withLock {
            return block()
        }
    }
}

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值