1

I'm learning Coroutines in Kotlin and I have a piece of code that looks like this (see below).

My friend says that the mutableMapOf is LinkedHashMap, which is not thread safe. The argument is that the suspend function may be run by different threads, and thus LinkedHashMap is unsuitable.

  1. Is it safe to use a simple mutable map here or is ConcurrentMap needed?
  2. When a suspend function is suspended, can it be resumed and executed by another thread?
  3. Even if (2) is possible, is there "happens-before/ happens-after" guarantee that ensures all the variables (and the underlying object contents) are deep synchronized from main memory before the new thread takes over?

Here's a simplified version of the code:

class CoroutineTest {

  private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)

  suspend fun simpleFunction(): MutableMap<Int,String> { 

    val myCallResults = mutableMapOf<Int,String>()

    val deferredCallResult1 = scope.async {
         //make rest call get string back
    }
    val deferredCallResult2 = scope.async {
         //make rest call get string back
    }
    ...
    myCallResults.put( 1, deferredCallResult1.await() )
    myCallResults.put( 2, deferredCallResult2.await() )
    ...
   
    return myCallResults
  }
}

Thanks in advance!
PS. I ran this code with much more async call results and had no problem; all call results are accounted for. But that can be inconclusive which is why I ask.

Ken White
  • 123,280
  • 14
  • 225
  • 444
Gary
  • 11
  • 4
  • 2 is certainly possible - that’s sort of the point of coroutines. The answer to 3 largely depends on your code - but generally no, there is no such guarantee. https://kotlinlang.org/docs/shared-mutable-state-and-concurrency.html – Boris the Spider Jun 16 '21 at 21:06
  • 1
    It is guaranteed. The link your posted is about **shared** mutable state. The map in this example is not shared. It is function-local. – Tenfour04 Jun 16 '21 at 21:07
  • I have looked into this example too, but that's using "launch". In this suspend function, I don't see any chance that 2 threads can concurrently modify the map. – Gary Jun 16 '21 at 21:08
  • 1
    You changed the code of the example and now it doesn't make too much sense. These `async()` calls do not run concurrently, they are sequential. This is because you `await()` on each before starting the next one. – broot Jun 16 '21 at 21:11
  • Missed that - in which case it’s a duplicate of https://stackoverflow.com/questions/58901729/visibility-in-kotlin-coroutines – Boris the Spider Jun 16 '21 at 21:12
  • Sorry, you are right, I should change back to the original version... – Gary Jun 16 '21 at 21:15
  • Thanks, the link is very useful. I read something similar in another blog too. – Gary Jun 16 '21 at 21:19

2 Answers2

4
  1. No, it is not safe to use a single mutableMapOf() from multiple coroutines.
  2. You understand suspending incorrectly. This is not function that is suspended. The coroutine running in the function could suspend. From this perspective suspending functions aren't really different than normal functions - they could be executed by many coroutines at the same time and all of them will work concurrently.

But... there is nothing wrong with your code for another reason. This mutable map is a local variable, so it is only available to the coroutine/thread that created it. Therefore, it is not accessed concurrently at all. It would be different if the map would be a property of CoroutineTest - then it might mean you need to use ConcurrentMap.

Updated

After reading all comments I believe I have a better understanding of your (or your friend) concerns, so I can provide a more accurate answer.

Yes, after suspending a coroutine it can resume from another thread, so coroutines make possible that some part of a function will be executed by one thread and other part will be executed by another thread. In your example it is possible that put(1 and put(2 will be invoked from two different threads.

However, saying that LinkedHashMap is not thread-safe doesn't mean, that it has to be always accessed by the same thread. It can be accessed by multiple threads, but not at the same time. One thread need to finish performing changes to the map and only then another thread can perform its modifications.

Now, in your code async { } blocks can work in parallel to each other. They can also work in parallel to the outer scope. But the contents of each of them works sequentially. put(2 line can only be executed after put(1 fully finishes, so the map is not accessed by multiple threads at the same time.

As stated earlier, it would be different if the map would be stored e.g. as a property, simpleFunction() would modify it and this function would be invoked multiple times in parallel - then each invocation would try to modify it at the same time. It would be also different if async operations would modify myCallResults directly. As I said, async blocks run in parallel to each other, so they could modify the map at the same time. But since you only return a result from async blocks and then modify the map from a single coroutine (from outer scope), the map is accessed sequentially, not concurrently.

broot
  • 21,588
  • 3
  • 30
  • 35
  • Thank you for your reply. One of the other comments said that another thread may run the function; and if so, how can the mutable map be made available to another thread? – Gary Jun 16 '21 at 21:13
  • What is your case exactly? Current code seems like `simpleFunction()` just needs to prepare some kind of data/results and return it. In that case note that even if you run your function 10 times concurrently, each invocation will create a separate map and work only on its copy - so there will be no problems. If you need to share map between threads then it really depends what is your specific case. – broot Jun 16 '21 at 21:21
  • the simpleFunction() is meant to run only once. There are many async calls inside that function which fetch data from rest calls and then finally return everything out. Apologies for changing the original post – Gary Jun 16 '21 at 21:26
  • If these async calls won't modify `myCallResults` directly, but you will `await()` for their results as you do right now, everything should be fine. If you would put items to `myCallResults` directly from inside of `async { }` then it might in fact cause problems. – broot Jun 16 '21 at 21:29
  • Very good tip! I'll remember not to modify that inside the `async { }` part – Gary Jun 16 '21 at 21:33
  • After reading all your comments here, I think I have a better understanding of your concerns, so I updated my answer. – broot Jun 16 '21 at 22:02
  • Thank you Broot, your answer makes it crystal clear. I want to focus on this part of what you said: `In your example it is possible that put(1 and put(2 will be invoked from two different threads.` Let's say that put(2 is indeed running from a different thread T. Is it guaranteed that the when T runs, the mutable map has the content from put(1 ? My friend thinks that the different thread T can have its own cached copy of mutable map. – Gary Jun 16 '21 at 23:04
  • Let's say we execute `simpleFunction()`, it starts with thread1. It suspends at first `await()` and then it is resumed with thread2. Next, it suspends at second `await()` and resumes with thread3. In that case map was created by thread1, first item was added by thread2 and second item was added by thread3. All these threads share exactly the same instance of the map. And they can't run concurrently, it would mean that the execution is resumed even before it is suspended. Yes, you are guaranteed that `put(2` will be executed after `put(1`. – broot Jun 16 '21 at 23:59
  • You should not really focus on threads, you should not care about them. What is important is that there is a coroutine and it executes the contents of outer code sequentially - line after line. Yes, it could jump from thread to thread, but in most cases this should not bother you. – broot Jun 17 '21 at 00:00
4

Since the map is local to the suspend function, it is safe to use a non-thread-safe implementation. It is possible that different threads will be working with the map between different suspend function calls (in this case the await() calls), but there is guaranteed happens-before/happens-after within the suspend function.

If your map were declared outside the suspend function and accessed via a property, then there could be simultaneous calls to this function and you would be concurrently modifying it, which would be a problem.

Tenfour04
  • 83,111
  • 11
  • 94
  • 154
  • Oops. Forgot to hit Post when I wrote this a few minutes ago. The other answer may be more helpful. – Tenfour04 Jun 16 '21 at 21:18
  • Does the happens-before/ happens-after guarantee that all the objects involved are deep loaded from main memory (as in java)? Some think that only the variable reference is made avalable to another thread and so the object content may still be cached. – Gary Jun 16 '21 at 21:22
  • I've been searching and I can't find a definitive statement about that, but I haven't read into the actual implementation of coroutines on the JVM. To me it stands to reason that a happens-before guarantee also extends to memory, the way `volatile` guarantees that in Java. – Tenfour04 Jun 17 '21 at 01:52