0% found this document useful (0 votes)
31 views

L11-Asynchronous_Programming_in_Rust

The document discusses asynchronous programming in Rust, focusing on non-blocking I/O, futures, and the async/.await syntax. It highlights the advantages of using futures for state management in concurrent programming, as well as the overhead associated with traditional threading. The document also introduces the Future trait and executors, emphasizing the importance of non-blocking operations to maintain efficiency in asynchronous code execution.

Uploaded by

Chang Jingyan
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
31 views

L11-Asynchronous_Programming_in_Rust

The document discusses asynchronous programming in Rust, focusing on non-blocking I/O, futures, and the async/.await syntax. It highlights the advantages of using futures for state management in concurrent programming, as well as the overhead associated with traditional threading. The document also introduces the Future trait and executors, emphasizing the importance of non-blocking operations to maintain efficiency in asynchronous code execution.

Uploaded by

Chang Jingyan
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 63

Asynchronous Programming

in Rust
CS3211 Parallel and Concurrent Programming
Outline
• Non-blocking I/O
• Asynchronous programming paradigm
• From Futures to async/.await

CS3211 L11 - Asynchronous Programming 2


Last week
• Safety – due to ownership and borrowing
• Memory safety
• Thread safety
• Rust code does not compile if it is not safe

CS3211 L11 - Asynchronous Programming 3


Recap: ownership and borrowing
• Ownership
• Shared (immutable) borrow
• Mutable borrow

CS3211 L11 - Asynchronous Programming 4


Closures
• Can be called like a function
• But they are not just simple functions
• Can be passed as parameters and returned to and from functions

CS3211 L11 - Asynchronous Programming 5


Overheads of threads
• Context switching cost: threads give up the rest of the CPU time slice
when blocking functions are called, and a switch happens
• Registers get restored, virtual address space gets switched, cache gets
stepped on, etc.
• Big cost for high-performance situations (servers) with multiple threads, each
handling a connection
• Memory overhead
• Each thread has its own stack space that needs to get managed by the OS
• Trying to have 5000 concurrent connections?
• 5000 threads = 5000 stack segments = 40GB at 8MB/stack!

CS3211 L11 - Asynchronous Programming 6


Time slice and threads (and a bit of networking)

CS3211 L11 - Asynchronous Programming 7


Time slice and threads

CS3211 L11 - Asynchronous Programming 8


Time slice and threads

CS3211 L11 - Asynchronous Programming 9


Mitigate the disadvantages of threads
• Context switches are expensive
• Use lightweight threads (green threads)
• Usually they need a runtime (like in Go)

• Is there a way we can have concurrency with less penalties?


• Non-blocking I/O

CS3211 L11 - Asynchronous Programming 10


Introducing non-blocking I/O
• For example, the read() sys call would block if there is more data to
be read but not available
• Thread gets pulled off the CPU and it cannot do anything else in the
meantime
• Instead, we could have read() return a special error value instead
of blocking
• If the client hasn’t sent anything yet, the thread can do other useful work, e.g.
reading from other descriptors
• Non-blocking I/O enables concurrency with one thread!

CS3211 L11 - Asynchronous Programming 11


Non-blocking I/O
• epoll is a kernel-provided mechanism that
notifies us of what file descriptors are ready for
I/O
• Scenario: we are a server having conversations
with multiple clients at the same time (multiple
fds)
• epoll notifies the thread when a client said
something
• read() from each of those file descriptors,
continue those conversations
• Rinse and repeat  event loop

CS3211 L11 - Asynchronous Programming 12


State management
Non-blocking I/O is nice in theory, but
• Actual applications:
managing state seems hard
• Was I waiting for the client to send me
something, or was I in the middle of sending
something to the client?
• What was the client asking for before I got
distracted?
• Charlie the Client asked me for her emails, but I
needed to get them from Dan the Database
• Now Dan the Database responded with some
info, but I can’t remember what I was supposed
to do with it
• Managing one connection in each thread is
easy because each conversation is an
independent train of thought

CS3211 L11 - Asynchronous Programming 13


State management
• Key problem: manage the state
associated with each conversation
• Imagine trying to cook 10 dishes at
the same time. Need to remember…
• How long each thing has been on the
stove
• How long things have been in the oven
• How long things have been marinating
for
• What the next step is for each dish

CS3211 L11 - Asynchronous Programming 14


State management
• Rust (and a handful of other languages)
take state management to the next level
• Futures allow us to keep track of in-
progress operations along with associated
state, in one package
• Think of a future as a helper friend that
oversees each operation, remembering any
associated state

CS3211 L11 - Asynchronous Programming 15


Futures visualized

CS3211 L11 - Asynchronous Programming 16


Futures visualized

CS3211 L11 - Asynchronous Programming 17


Futures visualized

CS3211 L11 - Asynchronous Programming 18


Futures visualized

CS3211 L11 - Asynchronous Programming 19


Futures visualized

CS3211 L11 - Asynchronous Programming 20


Futures visualized

CS3211 L11 - Asynchronous Programming 21


Futures visualized

CS3211 L11 - Asynchronous Programming 22


Futures visualized

CS3211 L11 - Asynchronous Programming 23


Futures visualized

CS3211 L11 - Asynchronous Programming 24


Futures visualized

CS3211 L11 - Asynchronous Programming 25


Futures visualized

CS3211 L11 - Asynchronous Programming 26


Futures visualized

CS3211 L11 - Asynchronous Programming 27


Futures visualized

CS3211 L11 - Asynchronous Programming 28


Futures
• Represents a value that will exist sometime in the future
• Calculation that hasn’t happened yet
• Is probably going to happen at some point
• Just keep asking
• Event loop = runtime for Futures
• Keeps polling Future until it is ready
• Runs your code whenever it can be run
• User-space scheduler for futures

CS3211 L11 - Asynchronous Programming 31


Rust: zero-cost abstraction
• Code that you can’t write better by hand
• Abstraction layers disappear at compile-time

• Example: iterators API is a zero-cost abstraction


• in release mode, the machine code
• One can’t tell if it was done using iterators or implemented by hand
• Compiler optimize the cache locality that would be difficult to implement by hand

CS3211 L11 - Asynchronous Programming 32


Futures.rs – zero-cost abstraction for futures
• The code in the binary has no allocation and no runtime overhead in
comparison to writing it by hand (non-assembly code )
• Building async state machines
• The Future trait
• Event reactor

CS3211 L11 - Asynchronous Programming 33


The Future trait
• The executor thread should call poll() on the future to start it off
• It will run code until it can no longer progress

CS3211 L11 - Asynchronous Programming 34


The Future trait
• The executor thread should call poll() on the future to start it off
• It will run code until it can no longer progress
• If the future is complete, returns Poll::Ready(T)
• If the future needs to wait for some event, returns Poll::Pending, and
allows the single thread to work on another future

CS3211 L11 - Asynchronous Programming 35


The Future trait
• The executor thread should call poll() on the future to start it off
• It will run code until it can no longer progress
• If the future is complete, returns Poll::Ready(T)
• If the future needs to wait for some event, returns Poll::Pending, and
allows the single thread to work on another future

CS3211 L11 - Asynchronous Programming 36


The Future trait
• When poll() is called, Context structure passed in
• Context includes a wake() function
• Called when future can make progress again
• Implemented internally using system calls

CS3211 L11 - Asynchronous Programming 37


The Future trait
• When poll() is called, Context structure passed in
• Context includes a wake() function
• Called when future can make progress again
• Implemented internally using system calls)
• After wake() is called, the executor can use Context to see which
Future can be polled to make new progress

CS3211 L11 - Asynchronous Programming 38


The Future trait
• When poll() is called, Context structure passed in
• Context includes a wake() function
• Called when future can make progress again
• Implemented internally using system calls)
• After wake() is called, the executor can use Context to see which
Future can be polled to make new progress

CS3211 L11 - Asynchronous Programming 39


The Future trait
• When poll() is called, Context structure passed in
• Context includes a wake() function
• Called when future can make progress again
• Implemented internally using system calls)
• After wake() is called, the executor can use Context to see which
Future can be polled to make new progress

CS3211 L11 - Asynchronous Programming 40


The Future trait
• When poll() is called, Context structure passed in
• Context includes a wake() function
• Called when future can make progress again
• Implemented internally using system calls)
• After wake() is called, the executor can use Context to see which
Future can be polled to make new progress

CS3211 L11 - Asynchronous Programming 41


The Future trait
• When poll() is called, Context structure passed in
• Context includes a wake() function
• Called when future can make progress again
• Implemented internally using system calls)
• After wake() is called, the executor can use Context to see which
Future can be polled to make new progress

CS3211 L11 - Asynchronous Programming 42


The Future trait
• When poll() is called, Context structure passed in
• Context includes a wake() function
• Called when future can make progress again
• Implemented internally using system calls)
• After wake() is called, the executor can use Context to see which
Future can be polled to make new progress

CS3211 L11 - Asynchronous Programming 43


Executors
• An executor loops over futures that can currently make progress
• Calls poll() on them to give them attention until they need to wait again
• When no futures can make progress, the executor goes to sleep until one or
more futures calls wake()
• A popular executor in the Rust ecosystem is Tokio
• Wraps around mio.rs and futures.rs
• Executors can be single-threaded or multi-threaded
• Running on multiple cores machine
• Futures can truly run in parallel
• Need to protect shared data using synchronization primitives (although the
ownership model kind of already forces you to do this anyway)

CS3211 L11 - Asynchronous Programming 44


Workflow of a future

CS3211 L11 - Asynchronous Programming 45


Futures should not block!
• If code within a future causes the thread to sleep, the executor
running that code is going to sleep
• Asynchronous code needs to use non-blocking versions of everything,
including mutexes, system calls that would normally block, or
anything.
• Executor runtimes like Tokio provide these non-blocking
implementations for your favorite synchronization primitives

CS3211 L11 - Asynchronous Programming 46


Composition with futures
• Pretty much no one implements futures manually (unless you’re a
low-level library implementor)
• Instead, futures are combined with various combinators
• Sequential
• Concurrently

CS3211 L11 - Asynchronous Programming 47


Not great ergonomics
• This code works
• It’s better than manually dealing with callbacks and state machines as you
would in C/C++ with interfaces like epoll
• But can we do better?
• The syntax is a little clunky… too much typing for Rust
• Code becomes messier as complexity increases
• Sharing mutable data (e.g. in local variables) can be painful: if there can only
be one mutable reference at a time, only one closure can touch that data

CS3211 L11 - Asynchronous Programming 48


Example of poor ergonomics
• Lines 21, 24, 25, 29:
asynchronous functions
returning Futures
• Lines 27: synchronous
(normal) function

• Line 21: complicated syntax

CS3211 L11 - Asynchronous Programming 49


Example of poor ergonomics
• Lines 26: why does
get_recipient need to
take a message?
• To make this chain of futures
work, since the next futures
need both the message and
recipient as input
• Like a pipeline

CS3211 L11 - Asynchronous Programming 50


Syntactic sugar
• Line 41: async function
returns Future
• Line 44, 45, 47: .await waits
for a future and gets its
value
• Line 46: normal function
usage

CS3211 L11 - Asynchronous Programming 51


Async/.await
• An async function is a function that returns a Future
• Any Futures used in the function are chained together by the compiler
• .await waits for a future and gets its value
• .await can only be called in an async fn or block
• Structure of the code is similar to what we are used to!
• The Rust compiler transforms this code into a Future with a poll()
method
• Just as efficient as what you could implement by hand

CS3211 L11 - Asynchronous Programming 52


Example: a simple server
• Lines 78, 81, 82: convert any blocking functions to asynchronous
versions
• They return Futures

CS3211 L11 - Asynchronous Programming 53


Example: asynchronous programming
• Lines 78, 81, 82: .await the Futures
• .await can only be used in an async function or block
• The compiler will complain if you forget .await

CS3211 L11 - Asynchronous Programming 54


Example: asynchronous programming
• main() now returns a Future
• Futures don’t actually do anything unless an executor executes them
• Need to run main() and submit the returned Future to the executor
• Line 73: #[tokio::main] macro submits the future to the executor

CS3211 L11 - Asynchronous Programming 55


Async functions generate/return futures
• If you run this function, it will not actually do any work with any
messages
• This is still a function and you can still run it…
• But its purpose is now to produce a future that does the stuff that
was written inside the function

CS3211 L11 - Asynchronous Programming 56


Async functions generate/return futures
• If you do not run this function, it will not do any work with any
messages
• The purpose is now to produce a future that does the stuff that was
written inside the function

CS3211 L11 - Asynchronous Programming 57


Async functions generate/return futures
• If you run this function, it will not actually do any work with any
messages
• This is still a function and you can still run it…
• But its purpose is now to produce a future that does the stuff that
was written inside the function

CS3211 L11 - Asynchronous Programming 58


State management
• There are 5 places where we might be paused, not actively executing:
• Before anything has happened yet (i.e. Future has been created but not yet
poll()ed)
• Await-ing for loadMessage
• Await-ing for get_recipient
• Await-ing for addToInbox
• Future has completed
• Use an enum to store the state for these possibilities

CS3211 L11 - Asynchronous Programming 59


State management
• Attempting to implement poll() for this Future
• Look at the current state and execute the appropriate code from the async fn

CS3211 L11 - Asynchronous Programming 60


Conceptually: state management

CS3211 L11 - Asynchronous Programming 61


Implications
• Async functions have no stack
• Sometimes called stackless coroutines
• The executor thread still has a stack (used to run normal/synchronous functions), but
it isn’t used to store state when switching between async tasks
• All state is self-contained in the generated Future
• No recursion
• The Future returned by an async function needs to have a fixed size known at
compile time
• Rust async functions are nearly optimal in terms of memory usage and
allocations
• Low overhead: the performance is as good as (or possibly better) what you could get
tuning everything by hand

CS3211 L11 - Asynchronous Programming 62


Usage of async code
• Taking a step back - problems to solve:
• Memory usage from having so many stacks
• Unnecessary context switching cost
• Async code makes sense when…
• You need an extremely high degree of concurrency
• Not as much reason to use async if you don’t have that many threads
• Work is primarily I/O bound
• Context switching is expensive only if you’re using a tiny fraction of the time slice
• If you’re doing a lot of work on the CPU for an extended period of time, you might
prevent the executor from running other tasks

CS3211 L11 - Asynchronous Programming 63


Similar tools in other languages
• Rust lets us write asynchronous code in the synchronous style that
we’re used to
• Javascript: very similar toolbox with Promises and async/await
• Involves much more dynamic memory allocation, not as efficient
• Golang: goroutines are asynchronous tasks, but unlike Rust they are
not stackless
• Resizable stacks - possible because Go is garbage collected
• Runtime knows where all pointers are and can reallocate memory
• C++20 just got stackless coroutines
• Still lots of sharp edges, may want to wait for more libraries to make this
easier to use

CS3211 L11 - Asynchronous Programming 64


Summary
• Never block in async code!
• Asynchronous tasks are cooperative (not preemptive)
• You can only use await in async functions
• Rust won’t let you write async functions in traits
• You can use a crate called async-trait though
• References
• Lectures 18 and 19 from Stanford’s CS110L:
https://reberhardt.com/cs110l/spring-2021/
• Katharina Fey, 2018: https://www.youtube.com/watch?v=j0SIcN-Y-LA
• https://www.youtube.com/watch?v=g2PmsO_C4eY
CS3211 L11 - Asynchronous Programming 65

You might also like