Blocking Operations Within Script

Script runs on a single thread, hence special care needs to be taken while performing blocking (CPU/IO) operations withing the script.

This section explains following two types of blocking operations and patterns/recommendations to be followed while performing those.

  1. CPU Bound
  2. IO Bound
DO NOT BLOCK

Calling CPU intensive or IO operations from the main script is dangerous and should be avoided at all cost.

Breaking this rule will cause all the background tasks started in script to halt and unexpected deadlocks.

DO NOT ACCESS/UPDATE MUTABLE STATE

Main sequencer script, and the techniques mentioned here for performing blocking tasks executes on different threads.

Hence, accessing/updating mutable state defined in script from these blocking functions is not thread safe.

CPU bound

For any CPU bound operations follow these steps:

  1. Create a new function and mark that with suspend keyword
  2. Wrap function body inside withContext(Dispatchers.Default)

Following example demonstrate writing CPU bound operation, In this example BigInteger.probablePrime(4096, Random()) is CPU bound and takes more than few seconds to finish.

Kotlin
// Calculating probablePrime is cpu bound operation and should be wrapped inside Default dispatcher
// Following function takes around 10 seconds to find a 4096 bit length prime number
suspend fun findBigPrime(): BigInteger =
        withContext(Dispatchers.Default) {
            BigInteger.probablePrime(4096, Random())
        }

Following shows, usage of the above compute heavy function in main sequencer script

Kotlin
script {

    loopAsync(100.milliseconds) {
        // loop represents the computation running on the main script thread.
    }

    onSetup("prime number") {
        // by default calling findBigPrime cpu intensive task suspends and waits for result
        // but this runs on different thread than the main script thread
        // which allows other background tasks started previously to run concurrenlty
        val bigPrime1: BigInteger = findBigPrime()

        // if you want to run findBigPrime in the background, then wrap it within async
        val bigPrimeDeferred: Deferred<BigInteger> = async { findBigPrime() }
        // ...
        // wait for compute intensive operation to finish which was previously started
        val bigPrime2: BigInteger = bigPrimeDeferred.await()

        // script continues...
    }
}

IO bound

For any IO bound operations follow these steps:

  1. Create a new function and mark that with suspend keyword
  2. Wrap function body inside withContext(Dispatchers.IO)

Following example demonstrate writing IO bound operation, In this example BufferredReader.readLine() is IO bound and takes more than few seconds to finish.

Kotlin
// Reading a line from a file is blocking IO operation and should be wrapped inside IO dispatcher
suspend fun BufferedReader.readMessage(): CharSequence? =
        withContext(Dispatchers.IO) {
            readLine()
        }

Following shows, usage of the above io heavy function in main sequencer script

Kotlin
script {
    loopAsync(100.milliseconds) {
        // loop represents the computation running on the main script thread.
    }

    onSetup("read file") {
        val reader = File("someFile.txt").bufferedReader()

        // by default calling readMessage (blocking io) task suspends and waits for result
        // but this runs on different thread than the main script thread
        // which allows other background tasks started previously to run concurrenlty
        val message1 = reader.readMessage()

        // if you want to run readMessage in the background, then wrap it within async
        val message2Deferred = async { reader.readMessage() }
        // ...
        // wait for blocking operation to finish which was previously started
        val message2: CharSequence? = message2Deferred.await()

        // script continues...
    }
}

How does it work behind the scenes?

withContext(Dispatchers.Default) or withContext(Dispatchers.IO) construct calls specified function on a provided dispatcher which is different from the main script.

Default or IO dispatchers maintains separate thread pool than the main sequencer script which usage single thread. This means, accessing/updating mutable variables defined in sequencer script is not thread safe from these functions and should be avoided.

You can read more about these patterns of blocking here