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.

Note

All the CSW related DSL’s provided in the scripting environment does not block main script thread hence they can be used directly.

Exception to this here is, callback based API’s for an ex. onEvent where you want to perform CPU/IO intensive tasks. In this case, you need to follow one of the pattern mentioned below.

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 blockingCpu utility function

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
source// Calculating probablePrime is cpu bound operation and should be wrapped inside blockingCpu utility function
// Following function takes around 10 seconds to find a 4096 bit length prime number
suspend fun findBigPrime(): BigInteger =
        blockingCpu {
            BigInteger.probablePrime(4096, Random())
        }

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

Kotlin
sourcescript {

    loopAsync(Duration.milliseconds(100)) {
        // 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 concurrently
        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 blockingIo utility function

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

Kotlin
source// Reading a line from a file is blocking IO operation and should be wrapped inside blockingIo utility function
suspend fun readMessage(bufferedReader: BufferedReader): CharSequence? =
        blockingIo {
            bufferedReader.readLine()
        }

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

Kotlin
sourcescript {
    loopAsync(Duration.milliseconds(100)) {
        // 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 concurrently
        val message1 = readMessage(reader)

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

        // script continues...
    }
}

Recommendations/Best Practices

  • Create separate kotlin (.kt) file/files and maintain all the CPU/IO bound blocking tasks their
  • Creating separate file/files makes sure you accidentally don’t access/modify mutable state present within the script
  • Call these functions from script
  • Wrap calling these function inside async block if you want to run them in parallel

How does it work behind the scenes?

blockingCpu or blockingIo construct underneath uses different thread pools than the main script 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