How does thread synchronization work in Kotlin?
Asked Answered
T

2

6

I have been experimenting with Kotlin synchronization and I do not understand from the docs on how the locking mechanism works on thread synchronization over common resources and thus attempted to write this piece of code which further complicates my understanding.

fun main() {
    val myList = mutableListOf(1)

    thread {
        myList.forEach {
            while (true) {
                println("T1 : $it")
            }
        }
    }

    thread {
        synchronized(myList) {
            while (true) {
                myList[0] = 9999
                println("**********\n**********\n**********\n")
            }
        }
    }
}

myList is the common resource in question.

The first thread is a simple read operation that I intend to keep the resource utilized in read mode. The second is another thread which requests a lock in order to modify the common resource.

Though the first thread does not contain any synchronization, I would expect it to internally handle this so that a while a function like map or forEach is in progress over a resource, another thread should not be able to lock it otherwise the elements being iterated over may change while the map/forEach is in progress (even though that operation may be paused for a bit while another thread has a lock over it).

The output I see instead shows that both the threads are executing in parallel. Both of them are printing the first element in the list and the stars respectively. But in the second thread, even though the stars are being printed, myList[0] is never set to 9999 because the first thread continues to print 1.

Trapezium answered 15/5, 2020 at 20:53 Comment(2)
It does not "internally handle this." The first thread does nothing to protect anything at all.Cottonmouth
You need to learn about and apply concurrent reasoning by happens-before. Java doesn't offer "eventual consistency" out of the box, which you seem to imply. Without happens-before, there is no consistency at all.Phonography
W
6

Threading and synchronisation are JVM features, not specific to Kotlin. If you can follow Java, there are many resources out there which can explain them fully. But the short answer is: they're quite low-level, and tricky to get right, so please exercise due caution. And if a higher-level construction (work queues/executors, map/reduce, actors...) or immutable objects can do what you need, life will be easier if you use that instead!

But here're the basics. First, in the JVM, every object has a lock, which can be used to control access to something. (That something is usually the object the lock belongs to, but need not be...) The lock can be taken by the code in a particular thread; while it's holding that lock, any other thread which tries to take the lock will block until the first thread releases it.

And that's pretty much all there is! The synchronised keyword (actually a function) is used to claim a lock; either that belonging to a given object or (if none's given) 'this' object.

Note that holding a lock prevents other threads holding the lock; it doesn't prevent anything else. So I'm afraid your expectation is wrong. That's why you're seeing the threads happily running simultaneously.

Ideally, every class would be written with some consideration for how it interacts with multithreading; it could document itself as 'immutable' (no mutable state to worry about), 'thread-safe' (safe to call from multiple threads simultaneously), 'conditionally thread-safe' (safe to call from multiple threads if certain patterns are adhered to), 'thread-compatible' (taking no special precautions but callers can do their own synchronisation to make it safe), or 'thread-hostile' (impossible to use from multiple threads). But in practice, most don't.

In fact, most turn out to be thread-compatible; and that applies to much of the Java and Kotlin collection classes. So you can do your own synchronisation (as per your synchronized block); but you have to take care to synchronise every possible access to the list -- otherwise, a race condition could leave your list in an inconsistent state.

(And that can mean more than just a dodgy value somewhere. I had a server app with a thread that got stuck in a busy-loop -- chewing up 100% of a CPU but never continuing with the rest of the code -- because I had one thread update a HashMap while another thread was reading it, and I'd missed the synchronisation on one of those. Most embarrassing.)

So, as I said, if you can use a higher-level construction instead, your life will be easier!

Walterwalters answered 15/5, 2020 at 21:30 Comment(5)
A higher-level construction is indeed what I'm trying to get to. But since functions like map, forEach, and any are injected into the Collections, I am not sure I understand a way to override them. And that also worries me that, if supposedly an update brings in a new function to iterate over the collection. People using my implementation of a List along with that new function that is injected from the Collection will not work happily with synchronization.Trapezium
I'm afraid I don't understand your concern. If you're using a higher-level construct of some kind, that will be instead of your basic collection (or controlling access to it). There are implementations which do have some concurrency guarantees, such as CopyOnWriteArrayList and ConcurrentHashMap, so you're free to use those… (contd)Walterwalters
… But they have their own overheads and quirks, which is why the Collections interfaces themselves don't specify anything about concurrency. Ultimately, it's hard to advise without knowing more about what you're trying to achieve.Walterwalters
For an analogy, I am creating my own implementation of a synchronized list. This implements the List interface. Now, even if I have my own implementation, there are so many places from which utility functions are injected into the List that I cannot override each one of them in my implementation. So i am not sure how to go by a higher level construction.Trapezium
What do you mean by ‘injected’? If you're extending e.g. AbstractList, then you'll have methods from there; but you should only be doing that if you're happy with its approach. Otherwise, you should probably extend nothing (and if needed keep a reference to another List you can delegate to as needed). You may also get default methods from an interface you're implementing, and there are extension methods — but none of those have direct access to any of your state, so they shouldn't be a problem.Walterwalters
G
2

Second thread is not changing the value of the first list element, as == means compare, not assign. You need to use = tio change the value e.g. myList[0] = 9999. However in your code it's not guaranteed that the change from the second thread will become visible in the first thread as thread one is not synchronising on myList.

If you are targeting JVM you should read about JVM memory model e.g. what is @Volatile. You current approach does not guarantee that first thread will ever see changes from the second one. You can simplify your code to below broken example:

var counter = 1

fun main() {
    thread {
        while (counter++ < 1000) {
            println("T1: $counter")
        }
    }
    thread {
        while (counter++ < 1000) {
            println("T2: $counter")
        }
    }
}

Which can print strange results like:

T2: 999
T1: 983
T2: 1000

This can be fixed in few ways e.g. by using synchronisations.

Garmon answered 15/5, 2020 at 21:0 Comment(2)
I corrected the assignment operator. I incorrectly made use of the equality check where I should have used the assignment.Trapezium
What I am really trying to figure out here is that the operations like forEach, map, any are pretty long-running operations. So what happens if a synchronized process requests a lock while such a map/forEach/any function is in progress? Is the lock granted and these functions pause for a while or is the lock not granted until these functions have completed execution?Trapezium

© 2022 - 2024 — McMap. All rights reserved.