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

Technical nits: Threads and Drop #63

Closed
QuineDot opened this issue Dec 26, 2022 · 6 comments · Fixed by #2282
Closed

Technical nits: Threads and Drop #63

QuineDot opened this issue Dec 26, 2022 · 6 comments · Fixed by #2282
Assignees

Comments

@QuineDot
Copy link

This slide says that threads are all daemon threads, but this is untrue:

  • For one, you're only talking about thread::spawn created threads, as per the very next slide
  • For two, these threads are only detached if you drop the JoinHandle
  • You also mention panicking and payloads, but you can only catch unwind payloads on the current thread; for panics of another spawned thread, you call join on the join handle to see if it panicked and to get the payload
    • I.e. you can only do this if the thread isn't detached

So you should talk about JoinHandle and detaching via Drop.

On the scoped page you should talk about the different behavior:

  • ScopedJoinHandle joins and blocks upon Drop instead of detaching
  • Any dropped ScopeJoinHandle that witnesses a thread panic will cause thread::scope to panic
  • But you can manually join and check for thread panics instead, similar to JoinHandle

Aside from the ability to borrow, this difference in handle drop behavior (and thus what may panic where) is the main difference between the two tools.

@steffahn
Copy link

  • ScopedJoinHandle joins and blocks upon Drop instead of detaching

I don't think this is true. AFAIK, it only blocks upon leaving the scope the thread was spawned in.

@mgeisler
Copy link
Collaborator

Hi @QuineDot,

Thanks for reading the slides so carefully :-)

This slide says that threads are all daemon threads, but this is untrue:

  • For one, you're only talking about thread::spawn created threads, as per the very next slide
  • For two, these threads are only detached if you drop the JoinHandle

Okay, I had not thought of it from that perspective. But let me first understand if we understand the same by daemon thread?

I first encountered the term in Python where a daemon thread is a thread which will not keep the program alive. That is all — non-daemon threads will keep the program alive, even if you exit from the main thread. When teaching the class, I was told that Java people also know the term with the same meaning.

With that definition of daemon thread, I believe that dropping the JoinHandle or not isn't significant: regardless of what you do, the program will exit when the main thread exits.

Now, you're right that the main thread cannot exit before the spawned thread if it calls handle.join() — but I don't think that's related to the spawned thread being a daemon thread. To me, it's more a consequence of calling handle.join() which (as we all know) blocks until the thread exists.

Aside from the ability to borrow, this difference in handle drop behavior (and thus what may panic where) is the main difference between the two tools.

Right, I see that there are differences in when you get panics. However, is the ability to borrow local variables not the important difference?

Remember that this is taught to people in a classroom on Day 4 — they'll have seen ~15 hours of Rust code at this point, so I need to focus on the most important points 😄 (while also not writing untrue things, of course).

I'm working on support for speaker notes #53 and with those, there will be a place to fill in details which the slides cannot contain. When I give the course, we do speak a lot about panics and I normally live-code some panics to show people how they can catch them (if unwinding is enabled).

@QuineDot
Copy link
Author

Okay, I had not thought of it from that perspective. But let me first understand if we understand the same by daemon thread?

Ah, okay -- that isn't the meaning I inferred, so I guess I didn't know that phrase. Also I take your point about being Day 4 (although you're the one who brought up panics 😉). Talking about things in terms of Drop probably is the wrong approach.

However, I do still think there's probable confusion here, based on the slides alone anyway. When writing my first stab at a reply, I realized where most my "this is confusing and going to be frustrating" gripes come from: Slide A sounds like a bunch of universal statements about (quoting the first line) "Rust threads". But Slide A and Slide B are really talking about different flavors of threads (especially concerning practical take-aways over technical truths). Let me try again to point out what I think is confusing in three passes of increasing detail.

As a preliminary, I'm going to assume we could agree that while a waited-for thread might technically be a daemon thread, it doesn't really "matter" due to being waitied-for, because the parent thread will wait for the child thread. (I know there's more nuance here... but hey it's Day 4.)

Pass 1: Typical usage, ignore handles, ignore panics

Slide A currently says:

  • "Rust threads work similarly to threads in other languages"
  • Threads are all daemon threads, the main thread does not wait for them

Slide B currently says:

  • Normal threads cannot borrow from their environment; however, you can use a scoped thread for this
  • (Nothing about waiting)

The question I, the student, asks in class:

  • Why would I ever use a non-scoped thread? You've only mentioned things they do better.

Things I, the student, get confused and then annoyed at later:

  • You said the main threads don't wait for other threads, but that's definitely what I'm observing with scope threads!

Suggested change:

  • Clarify that the main thread does not wait for spawned threads, but scope does wait for its child threads

Pass 2: You still want to mention panics (but not payloads necessarily)

Slide A currently says:

  • Thread panics are independent of each other
    • One aspect: A panic in sibling thread X won't kill sibling thread Y
    • But another aspect: A panic in child thread C won't kill parent thread P
      • (I assume this was part of your point since you mention payloads, which sibling threads can't catch)

Slide B currently says:

  • (Nothing about panics)

Things I, the student, get confused and then annoyed at later:

  • I panicked in a scoped thread and my main thread panicked too

Suggested change

  • Clarify that (with typical, non-handle-using code) spawned threads ignore child panics while scoped threads bubble them up

(Also double-panics can cause an abort, even in child threads, but... nah, nevermind, it's Day 4.)

Pass 3: You still want to talk about payloads too

Slide A currently says:

  • (Nothing about handles)
  • Panics can carry a payload, which can be unpacked with downcast_ref
    • This is right below the bullet that says the main thread doesn't wait for child threads

Slide B currently says:

  • (Nothing about panics or their payloads)

Things I, the student, ask in class if I'm awake enough:

  • downcast_ref what?
    • (follow-up) So hold up, if I want the payload I do have to wait, right?
  • Why can't I do this with scoped threads? I have to wait but can't even avoid a panic?

Suggested changes:

  • Drop the conversation about payloads, or
  • Highlight in Slide A that you have to join the handle (and wait) in order to get the payload (which is a trade-off), and
  • Highlight in Slide B that you can prevent panic propagation by manually joining (and optionally doing something with the payload)
    • Otherwise the student may not realize there's a way to avoid panic propagation with scoped threads

If you're worried about things being to complicated, I think this subtopic is optional, or mention-in-passing-not-in-detail. Though I personally think mentioning that you can avoid panic propagation in scoped threads is worth it, if you mentioned the panic tradeoff to begin with.

In summary

  • If you mention how it works with spawn on Slide A, mention how it works with scope on Slide B
  • Talk about the tradeoffs, especially
    • If thing on bullet three (you can unpack a payload) negates something on bullet one (the main thread doesn't wait)
    • If the thing on Slide B differs from the thing on Slide A (waiting, panic propagation)

You also asked, isn't the difference in borrowing ability the important difference? It's the motivating difference I agree, but I think that the difference in waiting and panicking are also important. If not, why even have the bullet points? (Additionally, they answer the question of why anyone would ever prefer spawned threads.)

@mgeisler
Copy link
Collaborator

mgeisler commented Jan 5, 2023

Thanks for all the comments and the thoughts put into them.

  • If you mention how it works with spawn on Slide A, mention how it works with scope on Slide B
  • Talk about the tradeoffs, especially
    • If thing on bullet three (you can unpack a payload) negates something on bullet one (the main thread doesn't wait)
    • If the thing on Slide B differs from the thing on Slide A (waiting, panic propagation)

Right, I think we can be more consistent here!

There is now also support for adding explanations to speaker notes via a simple <details> ... </details> block on the page. Some of the points should go into those so the instructor knows what to explain (and so students who do the class as self-study don't lose out on the information).

Some of the things you discuss are things I live-code during the class: I normally show the use of downcast_ref and I show how to wait on the thread with a handle. Some of this could go into the speaker notes (let me make a PR for that).

mgeisler added a commit that referenced this issue Jan 5, 2023
From the discussion in #63.
@mgeisler
Copy link
Collaborator

mgeisler commented Jan 5, 2023

I created #117 to help a bit with this (I'll leave this issue open since there is more we can do here).

mgeisler added a commit that referenced this issue Jan 9, 2023
From the discussion in #63.
NoahDragon pushed a commit to wnghl/comprehensive-rust that referenced this issue Jul 19, 2023
From the discussion in google#63.
@fw-immunant
Copy link
Collaborator

I've significantly expanded the notes on this slide, which I think helps this situation some.

I think the prose near "daemon threads" could be improved, though--I've written Unix programs for a long time and have never heard the notion of daemon (a program that double-fork to disassociate from its parent process and continues running in the background) used to refer to a thread.

A google search suggests that this is a term exclusively used in the Java and Python communities:

Java defines two types of thread: user thread (normal thread) and daemon thread. By default, when you create a new thread it is user thread. The Java Virtual Machine (JVM) won’t terminate if there are still user threads running. But it will exit if there are only daemon threads running.
[source]

Given that Rust threading is based on OS implementations, which are roughly standardized around pthreads, I think we should use pthreads terminology here ("detached thread") or simply explicitly state that the existence of other threads does not prolong the execution of the program past the end of main, but by calling join on their JoinHandles we can block main or another function on the completion of a thread.

Suggested prose:

Spawning new threads does not delay program termination at the end of main.

More detailed discussion is present in the slide's notes, which I think are pretty clear.

@fw-immunant fw-immunant self-assigned this Mar 7, 2024
@fw-immunant fw-immunant linked a pull request Aug 12, 2024 that will close this issue
fw-immunant added a commit that referenced this issue Aug 13, 2024
This is the last remaining fix left from #63.
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 a pull request may close this issue.

4 participants