In my recent [post](previous post), I discussed how Kotlin’s approach to asynchronous programming (particularly with coroutines) differs from what I’m accustomed to in JavaScript and Python (and even C#). Now that I grasp the fundamentals of how suspend, continuation passing style, and compiler magic intertwine, it’s time to put theory into practice.
Kotlin’s asynchronous capabilities are remarkably flexible compared to JavaScript and Python, especially regarding dispatchers. These allow code execution in various contexts like event loops or thread pools. Notably, Kotlin offers at least two distinct thread pools: Dispatchers.Default
for CPU-intensive tasks (using threads equivalent to CPU cores) and Dispatchers.IO
for I/O-bound operations. For a deeper dive into dispatchers, refer to [this resource](this excellent article).
Let’s examine a common asynchronous scenario: initiating multiple non-blocking I/O operations (simulated HTTP requests fetching blog posts), awaiting their completion, and processing the results.
Here’s a Python implementation:
|
|
We begin by creating an event loop using asyncio.run()
to execute our asynchronous code. Our async function (coroutine function in Python) named get_post
simulates fetching a post. Invoking it produces a coroutine object without immediately running the code. Execution requires either awaiting the coroutine (e.g., await get_post()
) or launching it via create_task
. Awaiting suspends the calling function until the coroutine finishes, leading to sequential execution (as demonstrated in the commented retrieve_posts_sequentially()
function). Conversely, create_task
launches the coroutine without awaiting, enabling concurrent execution within the event loop thread. This is illustrated in the retrieve_posts()
function, where we launch all requests and then use await asyncio.gather()
to wait for their completion.
Now, let’s translate this to Kotlin:
|
|
We use the suspend
keyword (no async
keyword in Kotlin) to mark our asynchronous function (getPost
). To execute suspend functions, we utilize a coroutine, created here using the runBlocking()
function (a coroutine builder). Without a CoroutineContext
, runBlocking
executes the code in an event loop within the current thread. Directly calling getPost
(as shown in the commented block) would result in sequential execution, similar to the commented Python code. To achieve concurrency, we employ the async()
function (another coroutine builder), creating a new coroutine for each getPost
call. These coroutines run concurrently, and async()
returns a Deferred
object (akin to Python’s Task-Future) that completes alongside its coroutine. We use awaitAll()
to wait for all Deferred
objects to complete. Note that without a specific dispatcher, async()
inherits the parent coroutine’s dispatcher (the event loop dispatcher in this case).
In essence:
asyncio.run()
in Python is analogous torunBlocking()
in Kotlin: both initiate an event loop.asyncio.createTask()
mirrorsasync()
: both launch coroutines.asyncio.gather(list[Task])
corresponds toList[Deferred].awaitAll()
: both await the completion of multiple tasks/coroutines.
For completeness, let’s present the JavaScript equivalent:
|
|
In JavaScript, calling an async function directly returns a Promise. There’s no explicit await
needed for execution. To avoid suspension, we gather the returned Promises from getPost
calls. Then, we use await Promise.all()
to wait for all promises to resolve. The JavaScript runtime inherently manages the event loop.