kotlinx.atomicfu

Introduction: The idiomatic way to use atomic operations in Kotlin
More: Author   ReportBugs   
Tags:

JetBrains incubator project GitHub license Download

The idiomatic way to use atomic operations in Kotlin.

  • Code it like AtomicReference/Int/Long, but run it in production efficiently as AtomicXxxFieldUpdater on Kotlin/JVM and as plain unboxed values on Kotlin/JS.
  • Use Kotlin-specific extensions (e.g. inline updateAndGet and getAndUpdate functions).
  • Compile-time dependency only (no runtime dependencies).
    • Post-compilation bytecode transformer that declares all the relevant field updaters for you on Kotlin/JVM.
    • Post-compilation JavaScript files transformer on Kotlin/JS.
  • Multiplatform:
    • Kotlin/Native is supported.
    • However, Kotlin/Native works as library dependency at the moment (unlike Kotlin/JVM and Kotlin/JS).
    • This enables writing common Kotlin code with atomics that compiles for JVM, JS, and Native.
  • Gradle for all platforms and Maven for JVM are supported.
  • Additional features include:

Example

Let us declare a top variable for a lock-free stack implementation:

import kotlinx.atomicfu.* // import top-level functions from kotlinx.atomicfu

private val top = atomic<Node?>(null)

Use top.value to perform volatile reads and writes:

fun isEmpty() = top.value == null  // volatile read
fun clear() { top.value = null }   // volatile write

Use compareAndSet function directly:

if (top.compareAndSet(expect, update)) ...

Use higher-level looping primitives (inline extensions), for example:

top.loop { cur ->   // while(true) loop that volatile-reads current value 
   ...
}

Use high-level update, updateAndGet, and getAndUpdate, when possible, for idiomatic lock-free code, for example:

fun push(v: Value) = top.update { cur -> Node(v, cur) }
fun pop(): Value? = top.getAndUpdate { cur -> cur?.next } ?.value

Declare atomic integers and longs using type inference:

val myInt = atomic(0)    // note: integer initial value
val myLong = atomic(0L)  // note: long initial value

Integer and long atomics provide all the usual getAndIncrement, incrementAndGet, getAndAdd, addAndGet, and etc operations. They can be also atomically modified via += and -= operators.

Dos and Don'ts

  • Declare atomic variables as private val or internal val. You can use just (public) val, but make sure they are not directly accessed outside of your Kotlin module (outside of the source set). Access to the atomic variable itself shall be encapsulated.
  • Only simple operations on atomic variables directly are supported.
    • Do not read references on atomic variables into local variables, e.g. top.compareAndSet(...) is Ok, while val tmp = top; tmp... is not.
    • Do not leak references on atomic variables in other way (return, pass as params, etc).
  • Do not introduce complex data flow in parameters to atomic variable operations, i.e. top.value = complex_expression and top.compareAndSet(cur, complex_expression) are not supported (more specifically, complex_expression should not have branches in its compiled representation). Extract complex_expression into a variable when needed.
  • Use the following convention if you need to expose the value of atomic property to the public:
private val _foo = atomic<T>(initial) // private atomic, convention is to name it with leading underscore
public var foo: T                     // public val/var
    get() = _foo.value
    set(value) { _foo.value = value }

Gradle build setup

Building with Gradle is supported for all platforms.

JVM

You will need Gradle 4.0 or later for the following snippets to work. Add and apply AtomicFU plugin:

buildscript {
    ext.atomicfu_version = '0.11.11'

    dependencies {
        classpath "org.jetbrains.kotlinx:atomicfu-gradle-plugin:$atomicfu_version"
    }
}

apply plugin: 'kotlinx-atomicfu'

Add compile-only dependency on AtomicFU library and run-time dependency for tests:

dependencies {
    compileOnly "org.jetbrains.kotlinx:atomicfu:$atomicfu_version"
    testRuntime "org.jetbrains.kotlinx:atomicfu:$atomicfu_version"
}

JS

Configure add apply plugin just like for JVM. The only change is that JS version of the library (atomicfu-js) shall be used in dependencies:

dependencies {
    compileOnly "org.jetbrains.kotlinx:atomicfu-js:$atomicfu_version"
    testRuntime "org.jetbrains.kotlinx:atomicfu-js:$atomicfu_version"
}

Native

This library is available for Kotlin/Native (atomicfu-native). It is a regular library and you should declare a normal dependency, no plugin is needed nor available. Only single-threaded code (JS-style) is currently supported.

Kotlin/Native supports only Gradle version 4.7 and you should use kotlin-platform-native plugin.

First of all, you'll need to enable Gradle metadata in your settings.gradle file:

enableFeaturePreview('GRADLE_METADATA')

Then, you'll need to apply the corresponding plugin and add appropriate dependencies in your build.gradle file:

buildscript {
    repositories {
        jcenter()
        maven { url 'https://plugins.gradle.org/m2/' }
        maven { url 'https://dl.bintray.com/jetbrains/kotlin-native-dependencies' }
    }

    dependencies {
        classpath "org.jetbrains.kotlin:kotlin-native-gradle-plugin:$kotlin_native_version"
    }

}

apply plugin: 'kotlin-platform-native'

repositories {
    jcenter()
}

dependencies {
    implementation 'org.jetbrains.kotlinx:atomicfu-native:0.11.11'
}

sourceSets {
    main {
        component {
            targets = ["ios_arm64", "ios_arm32", "ios_x64", "macos_x64", "linux_x64", "mingw_x64"] 
            outputKinds = [EXECUTABLE]
        }
    }
}

Since Kotlin/Native does not generally provide binary compatibility between versions, you should use the same version of Kotlin/Native compiler as was used to build AtomicFU. Add an appropriate kotlin_native_version to your gradle.properties file. See gradle.properties in AtomicFU project.

Common

If you write a common code that should get compiled or different platforms, add org.jetbrains.kotlinx:atomicfu-common to your common code dependencies:

dependencies {
    compile "org.jetbrains.kotlinx:atomicfu-common:$atomicfu_version"
}

Maven build setup

Declare AtomicFU version:

<properties>
     <atomicfu.version>0.11.11</atomicfu.version>
</properties>

Declare provided dependency on the AtomicFU library (the users of the resulting artifact will not have a dependency on AtomicFU library):

    <dependencies>
        <dependency>
            <groupId>org.jetbrains.kotlinx</groupId>
            <artifactId>atomicfu</artifactId>
            <version>${atomicfu.version}</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>

Configure build steps so that Kotlin compiler puts classes into a different classes-pre-atomicfu directory, which is then transformed to a regular classes directory to be used later by tests and delivery.

    <build>
        <plugins>
            <!-- compile Kotlin files to staging directory -->
            <plugin>
                <groupId>org.jetbrains.kotlin</groupId>
                <artifactId>kotlin-maven-plugin</artifactId>
                <version>${kotlin.version}</version>
                <executions>
                    <execution>
                        <id>compile</id>
                        <phase>compile</phase>
                        <goals>
                            <goal>compile</goal>
                        </goals>
                        <configuration>
                            <output>${project.build.directory}/classes-pre-atomicfu</output>
                            <!-- "VH" to use Java 9 VarHandle, "BOTH" to produce multi-version code -->
                            <variant>FU</variant>  
                        </configuration>
                    </execution>
                </executions>
            </plugin>
            <!-- transform classes with AtomicFU plugin -->
            <plugin>
                <groupId>org.jetbrains.kotlinx</groupId>
                <artifactId>atomicfu-maven-plugin</artifactId>
                <version>${atomicfu.version}</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>transform</goal>
                        </goals>
                        <configuration>
                            <input>${project.build.directory}/classes-pre-atomicfu</input>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

Additional features

AtomicFU provides some additional features that you can optionally use.

VarHandles with Java 9

AtomicFU can produce code that uses Java 9 VarHandle instead of AtomicXxxFieldUpdater. Configure transformation variant in Gradle build file:

atomicfu {
    variant = "VH"
}

It can also create JEP 238 multi-release jar file with both AtomicXxxFieldUpdater for JDK<=8 and VarHandle for for JDK9+ if you set variant to "BOTH".

Testing lock-free data structures on JVM

You can optionally test lock-freedomness of lock-free data structures using LockFreedomTestEnvironment class. See example in LockFreeQueueLFTest. Testing is performed by pausing one (random) thread before or after a random state-update operation and making sure that all other threads can still make progress.

In order to make those test to actually perform lock-freedomness testing you need to configure an additional execution of tests with the original (non-transformed) classes for Maven:

    <build>
        <plugins>
            <!-- additional test execution with surefire on non-transformed files -->
            <plugin>
                <artifactId>maven-surefire-plugin</artifactId>
                <executions>
                    <execution>
                        <id>lockfree-test</id>
                        <phase>test</phase>
                        <goals>
                            <goal>test</goal>
                        </goals>
                        <configuration>
                            <classesDirectory>${project.build.directory}/classes-pre-atomicfu</classesDirectory>
                            <includes>
                                <include>**/*LFTest.*</include>
                            </includes>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

For Gradle there is nothing else to add. Tests are always run using original (non-transformed) classes.

Support Me
Apps
About Me
Google+: Trinea trinea
GitHub: Trinea