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.
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 ratings0% 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.
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