13 ms·
"Maybe we could just have kept Rust as it was circa 2016, and let the crazy non-blocking folks write hand-crafted epoll() loops like they do in C++. I honestly
by jude- 5y ago
"Maybe we could just have kept Rust as it was circa 2016, and let the crazy non-blocking folks write hand-crafted epoll() loops like they do in C++. I honestly don’t know, and think it’s a difficult problem to solve."
I think this is the most underappreciated part of this article. Since when did everything have to be async? There are other ways to represent concurrency that more accurately reflect what the computer is actually doing.
For example, an alternative to async is to represent a workstream as a state machine, where state transitions happen between I/O. Then, your state machine can be a struct, and each state transition can be an impl function on that struct that takes one or more completed I/O requests as input and emits one or more I/O requests as output. This saves you from having to implement everything as a closure, which this article rants about. Your top-level epoll loop merely services I/O requests from state machine instances, and invokes your global application logic to start and stop state machines to carry out business logic tasks.
I realize that many complicated workstreams could have many states due to all the I/O they might do, but the task of converting a high-level workstream into a state machine could be automated by the tooling.
- __s 5y ago> but the task of converting a high-level workstream into a state machine could be automated by the tooling That tooling is literally async/await. async/await transforms functions into state machines
- jude- 5y agoExcept doing so introduces a bunch of needless programming difficulties, including but not limited to the ones discussed in this article. For example, if you make the state machines and epoll loop explicit, you don't have the function coloring problem -- all your I/O requests from state machines to the poll loop (which may or may not cross thread boundaries) are explicit synchronization points with your global business logic, which gives you fine-grained control over how your state machines handle things like request timeouts, resource quota limits, cancellations, execution suspend/resume, and so on. As another example, you're not limited to factoring your state transitions into closures. As a third example, your business logic would have global, explicit control over I/O scheduling, which greatly simplifies end-to-end QoS and request prioritization.
- eximius 5y ago> let the crazy non-blocking folks write hand-crafted epoll() loops like they do in C++. > I think this is the most underappreciated part of this article. I think it's incredibly silly actually. Abandon all async for a difficult and error prone epoll model? > Since when did everything have to be async? It doesn't! No one is forcing anyone to use async. I'm not sure why the author implies that. But if you do want to use async, Rust is attempting to solve the async problem with the same guarantees it has for blocking code. Turns out that is hard.
- jude- 5y agoI'm not saying that we should go back to hand-rolling our own epoll loops. I'm saying that we can do better than async/await by making both the state machines and event loop explicit. For example, here's an API I'd prefer to use over async/await: /// A state machine that adds three numbers and uploads them to a web server struct AddAndUpload { /// I/O handle to the event loop io: IOClient, /// Buffer to store numbers I load nums: [u64; 3], /// URL to upload the data to url: String } impl AddAndUpload { /// Constructor pub fn new(io: IOClient, url: String) -> AddAndUpload { AddAndUpload { io, nums: [0u64; 3], url } } /// Entry point to this state machine pub fn inner_main(&mut self) -> Result<(), IOClient::Error> { /// go and get the data let n1_fut : IOClient::Future<u64> = self.io.sql_async("SELECT n1 FROM table1", &[])?; let n2_fut : IOClient::Future<u64> = self.io.sql_async("SELECT n2 FROM table2", &[])?; let n3_fut : IOClient::Future<u64> = self.io.sql_async("SELECT n3 FROM table3", &[])?; // wait for all I/O operations to finish IOClient::wait_all(&[&n1_fut, &n2_fut, &n3_fut])?; // extract results let n1 = n1_fut.into_inner(); let n2 = n2_fut.into_inner(); let n3 = n3_fut.into_inner(); // upload them let sum = n1 + n2 + n3; let upload_fut : IOClient::Future<IOClient::HTTPStatus> = self.io.http_post_async(&self.url, &["content-type: application/octet-stream"], &sum.to_be_bytes())?; let upload_http_status = upload_fut.wait()?.into_inner(); match upload_http_status.as_u16() { 200 => { Ok(()) } 400..499 => { Err(IOClient::Error::Custom("client error")) } 500..599 => { Err(IOClient::Error::Custom("server error")) } x => { Err(IOClient::Error::Custom("Nonsensical HTTP code")) } } } } impl IOClient::StateMachine for AddAndUpload { type Return = (); fn main(&mut self) -> Result<(), IOClient::Error> { self.inner_main() } } /\* somewhere else \*/ fn main() { let io_server = IOServer::spawn().unwrap(); let io_client = io_server.client().unwrap(); let add_and_upload = AddAndUpload::new(io_client, "http://example.com".to_string()); loop { io_server.run().unwrap(); match add_and_upload.get_machine_status() { Ok(IOClient::StateMachine::Finished(result)) => { eprintln!("add_and_uploaded exited with {:?}", &result); break; } Ok(_) => {}, Err(e) => { panic!("add_and_upload aborted: {:?}", &e); } } } io_server.terminate(); }