Don't Assume Atomicity in Single Call

Don't Assume Atomicity in Single Call

Key Idea

Single operations on shared data structures are not inherently atomic. Without proper synchronization, issues like race conditions, data corruption, and unpredictable behavior can occur.

Example: Kotlin's List<>

Kotlin's List<> and MutableList<> are not thread-safe. Even simple calls like list.get(index) or list.add(element) can lead to issues in multithreaded environments.

Unsafe Example:

val sharedList = mutableListOf<Int>()

fun unsafeRead(index: Int): Int {
    return sharedList[index] // Not thread-safe
}

fun unsafeWrite(value: Int) {
    sharedList.add(value) // Not thread-safe
}

Best Practices

  • Use Thread-Safe Variants: Replace List<> with CopyOnWriteArrayList or similar.
  • Synchronize Access: Use ReentrantLock or synchronized to protect shared data.
  • Prefer Immutability: Use immutable collections to eliminate synchronization needs.

Takeaway

Do not assume atomicity in single calls. Without thread-safe mechanisms, such assumptions lead to fragile and error-prone systems.

Example in Kotlin

GT-Sandbox-Snapshot: Havoc happening when adding to a list in kotlin from multiple threads.

Code

package com.glassthought.sandbox

import kotlin.concurrent.thread

fun main() {
  // Shared mutable list
  val sharedList = mutableListOf<Int>()

  // Number of threads and iterations per thread
  val threadCount = 10
  val iterations = 10000

  // Threads performing concurrent writes
  val threads = List(threadCount) { threadIndex ->
    thread {
      repeat(iterations) {
        sharedList.add(threadIndex * iterations + it) // Unsafe write
      }
    }
  }

  // Wait for all threads to finish
  threads.forEach { it.join() }

  // Analyze the result
  println("Expected size: ${threadCount * iterations}")
  println("Actual size: ${sharedList.size}")
  println("Null elements in the list count: ${sharedList.size - sharedList.filterNotNull().size}")
}

Command to reproduce:

gt.sandbox.checkout.commit ae6f6f8d4f229f078e71 \
&& cd "${GT_SANDBOX_REPO}" \
&& cmd.run.announce "./gradlew run --quiet"

Recorded output of command:

Expected size: 100000
Actual size: 27429
Null elements in the list count: 18

Song