Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Start part 1: intro and concurrency chapter #230

Merged
merged 1 commit into from
Nov 14, 2024
Merged

Conversation

nrc
Copy link
Member

@nrc nrc commented Oct 10, 2024

cc #227, #228, #229

Draft of chapters 2 and 3.

@nrc nrc force-pushed the guide-intro branch 2 times, most recently from 3327c77 to 8e29140 Compare November 4, 2024 01:10
This was referenced Nov 5, 2024
@nrc nrc marked this pull request as ready for review November 5, 2024 04:16
@nrc nrc changed the title WIP Start part 1 Start part 1: intro and concurrency chapter Nov 5, 2024
[^other]: There are some programming languages (or even libraries) which have concurrency which is managed within the program (without the OS), but with a pre-emptive scheduler rather than relying on cooperation between threads. Go is a well-known example. These systems don't require `async` and `await` notation, but have other downsides including making interop with other languages or the OS much more difficult, and having a heavyweight runtime. Very early versions of Rust had such a system, but no traces of it remained by 1.0.


## Concurrency and Parallelism
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The line I've been using here is from Aaron Tuuron's PhD thesis: concurrency is a way of organizing work, parallelism is a resource. This line is reflected in std::thread::available_parallelism, which intentionally deviates from C++'s hardware_concurrency. Maybe that's a helpful framing to use here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a paragraph on this in the 'Enough silly examples, let's try to define things properly', I do like this framing a lot, definitely useful to add.

[^busywait]: There's another option which is that the thread can *busy wait* by just spinning in a loop until the IO is finished. This is not very efficient since other threads won't get to run and is uncommon in most modern systems. You may come across it in the implementations of locks or in very simple embedded systems.


## Async programming
Copy link
Member

@yoshuawuyts yoshuawuyts Nov 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This section focuses a fair bit on what happens under the hood for both threads and futures - but I believe it might be more important for users to emphasize what async allows you to do that threads can't:

  • futures support arbitrary cancellation in a uniform way
  • futures support arbitrary concurrent execution
  • futures support user-defined control flow constructs like join and timeout which combine concurrency and cancellation to do interesting new things that non-async code can't do

I wrote more about this in my "why async?" post. Personally I find this more compelling than arguments around performance, since this describes actual new capabilities which can be used by users. Things like being able to apply timeouts to arbitrary computations in a standard way is a big deal - I think we should highlight that!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, I think that's right, but I don't think this is the right place. This chapter is meant to be a high-level summary of the different kinds of concurrency, and in a practical way, what is similar and what is different. In particular I want to be clear and precise about the assumptions from threaded concurrency which carry over to async. The performance difference mentioned here is present more as a consequence of that, rather than because I'm trying to 'sell' async. I've made a conscious decision to avoid any syntax here and focus on tasks rather than futures or about how async is represented in the source code. So I don't think talking about join, etc. would fit well here.

I think that cancellation and timeouts will only murky the waters for this chapter - there are equivalents for threads, they're just not as ergonomic as for async, and they don't tend to feature in libraries, etc. However, there isn't a fundamental reason for why not, and I think getting into the distinctions and details wouldn't be appropriate here.

I do have a little section in the intro which more about motivation for choosing async, and I'll expand that to include more of these things.

[^other]: There are some programming languages (or even libraries) which have concurrency which is managed within the program (without the OS), but with a pre-emptive scheduler rather than relying on cooperation between threads. Go is a well-known example. These systems don't require `async` and `await` notation, but have other downsides including making interop with other languages or the OS much more difficult, and having a heavyweight runtime. Very early versions of Rust had such a system, but no traces of it remained by 1.0.


## Concurrency and Parallelism
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another thought here: the way I think about futures vs threads is that threads inherently couple concurrency and parallelism into a single construct. This is exemplified by things like hyper-threading.

Futures on the other hand intentionally decouple concurrency from parallelism. Concurrency after all is just a way of scheduling work, and with futures we don't have to make use of threads for this. This I feel like might be the most important property of futures, and I think is worth highlighting.

A framing I've started using is that tasks are not the async/await form of threads; it's more accurate to think of them as parallelizable futures. This framing does not match Tokio and async-std's current task design; but both also have trouble propagating cancellation. See parallel_future and tasks are the wrong abstraction for more.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking quite a bit about this perspective, and my conclusion is that 'threads inherently couple concurrency and parallelism into a single construct' (which is the way I have been thinking as well) is not quite right. It's not inherent to threads, rather it is about the way they are conventionally used and configured. It's easy enough in most OS's to pin a thread to a CPU and/or specify which threads operate on the same cores (i.e., to separate out the parallelism), it's just that this is usually not surfaced in an ergonomic way in PLs. With futures/async, some degree of configuration is explicit in the code (e.g., join vs spawn) and some can be configured in the runtime.

A framing I've started using is that tasks are not the async/await form of threads; ...

While I agree this is a really useful framing, given that it's not how any major runtime works today and that the reader is likely to have some background with threads, I think the 'tasks are sort of threads' is going to be a more useful way to introduce readers here. I think I'll rely more on the 'tasks are parallelisable futures' framing when I introduce join, select, etc. later in the guide

@nrc nrc merged commit d9a44f3 into rust-lang:master Nov 14, 2024
1 check failed
@nrc nrc deleted the guide-intro branch November 14, 2024 23:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants