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.
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.
- CPU Bound
- IO Bound
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.
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:
- Create a new function and mark that with
suspend
keyword - 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
-
source
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 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:
- Create a new function and mark that with
suspend
keyword - 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
-
source
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 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