Is it always safe to just convert @Synchronized to `Mutex.withLock`?

I’m working on converting my thread-based code to coroutine-based.

In many cases, I need to convert this:

  fun doSomething() {
	/*business logic*/


 private val mutex = Mutex()
  suspend fun doSomething() {
	mutex.withLock {
	  /*business logic*/

However, I’m struggling to easily attain a deep understanding of Mutex. I just want to know if this is always safe. Will mutex only allow one thread at the same time in the same exact way as @Synchronized? I know Mutex is designed for coroutines. But as I am migrating, many functions will still be used by multiple threads. Basically, is Mutex exactly the same as @Synchronized with the added perk that it works for coroutines, or are there other considerations I need to make?

I have already considered the possibility that objects can be used as locks in different places. So I need to make sure when I am converting @Synchronized to Mutex that I consider every place where the object may be used as a synchronization monitor first.

But aside from that, is there anything else I need to worry about or is it safe?

It will allow only a single coroutine at a time. It doesn’t apply any restrictions on threads. But generally yes, Mutex does a very similar thing in regards to coroutines, as synchronized does in regards to threads.

However, there is a big difference. Mutex is not reentrant, while @Synchronized is. We can enter synchronized block multiple times, but we can’t do the same using a Mutex. For this reason it is probably not a good idea to simply replace @Synchronized methods with Mutex. If any of such methods call another one, you will get a deadlock.

1 Like

That’s extremely useful info. Thanks.

I need further clarification on this. Assuming two threads can’t share the same coroutine at the same time, wouldn’t it follow that two threads cannot enter at the same time?

Mutex.withLock and Mutex.lock are both suspending functions, so my understanding is that can only be called inside of a coroutine.

So given that a coroutine can only be running on one thread at one time, then only one thread can be running inside the Mutex at a time, right?

I suppose a coroutine can switch threads in the case of, say Dispatchers.IO. But even if the thread can switch, my understanding is that it would still be only one thread inside the locked code at any given instant.

Wow, I had no idea. This is a huge difference. How would you recommend migrating synchronized methods that need to be re-entered? Or if you have a class that contains multiple synchronized methods that can call each other or themselves inside the synchronized blocks (like recursive functions), how would this migrate to Mutex?

Answering my own question about reentry from this link I found: Reentrant lock · Issue #1686 · Kotlin/kotlinx.coroutines · GitHub

Yes, you are correct. Because only a single coroutine can enter withLock() and only a single thread can run a coroutine at a time, that means only a single thread can be running inside withLock() at a time (assuming we don’t fork inside it). My point is: withLock() doesn’t really care about threads, but coroutines, so the above is simply a side-effect. Also, we can enter withLock() using thread A, then switch to thread B, then switch again and exit using thread C - Mutex won’t care about it.

1 Like

Thanks, I’m much more clear on this now.

What do you mean by fork? I searched google fork Forking in java and Forking in coroutines but couldn’t find anything that seems related. I’m assuming java cannot fork in the same way python can with os.fork, can it?

Sorry, I used more generic meaning of “fork”. I mean doing things like async(), launch() inside withLock() - this will make multiple coroutines run inside withLock(). Of course, it is not a surprise that launching new coroutines… launches new coroutines :wink:

Oh interesting! Had not thought of that.

I wonder if there is anyway to protect against that. Perhaps some fancy mechanism that throws an exception whenever you try to launch a new coroutine inside of mutex-locked block.