In Ruby, what do Thread.new, Thread#join and Thread#value return, and what happens to threads the main program never joins?
answer
- Thread.new needs a block
- join returns the thread
- join(limit) returns nil on timeout
- value returns the block's result
- main thread exit kills the rest
basics
~20 sThread.new starts a block in a new thread and returns the Thread. join waits and returns that thread, or nil when its limit expires; value returns the block's result or re-raises its exception. Unjoined threads die when main exits.
solid answer
~40 s`Thread.new(*args) { |*args| ... }` starts the block concurrently and immediately returns a `Thread` object; without a block it raises `ThreadError`. `thr.join` blocks the caller until the thread finishes and returns `thr`; `thr.join(5)` waits at most five seconds and returns `nil` if the thread is still running, which leaves it running. `thr.value` calls `join` and then returns the block's last expression. If the thread died from an exception, both `join` and `value` re-raise that exception in the caller. When the main thread finishes, Ruby kills every other thread, so a thread you never join may never complete its work. The usual pattern for 50 page fetches is `threads = urls.map { |u| Thread.new(u) { |x| fetch(x) } }` followed by `threads.map(&:value)`.
code
ruby · 16 linesslow = Thread.new { sleep 2; :done }
slow.join(0.1) # => nil (still running)
slow.alive? # => true
slow.join # => slow (the Thread itself)
slow.value # => :done
boom = Thread.new do
Thread.current.report_on_exception = false
raise KeyError, "EUR missing"
end
begin
boom.value
rescue KeyError => e
e.message # => "EUR missing"
endgo deeper
Recall that Thread.new returns immediately, join waits and returns the thread, and value returns the block's result.
Explain join(limit) returning nil while the thread keeps running, value re-raising the thread's exception, and why unjoined threads die at exit.
Show the fan-out and fan-in pattern with ordered results, decide how one failed fetch should affect the batch, and bound thread counts for large inputs.
Weigh ad-hoc threads against a shared worker pool or an executor library for a codebase, considering error handling, shutdown and observability.
## Starting a thread `Thread.new` takes a block and any arguments, starts a new thread that runs the block, and returns the `Thread` object at once. The caller does not wait. - Arguments passed to `Thread.new` are yielded to the block: `Thread.new(url) { |u| ... }`. - Calling `Thread.new` without a block raises `ThreadError` ("must be called with a block"). - `Thread.start` and `Thread.fork` are the same as `Thread.new` except that they do not call a subclass's `initialize`. ## Waiting: join `Thread#join` suspends the calling thread until the target finishes. 1. `thr.join` with no argument waits for as long as it takes and returns `thr` itself. 2. `thr.join(limit)` waits at most `limit` seconds. If the thread has finished, it returns `thr`; if not, it returns **`nil`** and the thread **keeps running**. A `nil` from `join` is therefore not a result, only "not yet". 3. Joining the current thread, or the main thread from another thread, raises `ThreadError`. `join` returns the thread, not what the block computed, so it is the tool for "wait until done" and for chaining (`threads.each(&:join)`). ## Getting results: value `Thread#value` joins the thread and then returns the value of the block's last expression: ```ruby rate = Thread.new { 1.0842 } rate.value # => 1.0842 ``` Calling `value` twice is fine; the second call returns the same object without waiting. ## Exceptions surface on join and value An exception that escapes a thread's block ends that thread, not the program. The exception is stored with the thread and **re-raised in whichever thread calls `join` or `value`**: ```ruby t = Thread.new { Integer("n/a") } t.value # raises ArgumentError in the caller ``` That is why collecting results with `value` is safer than reading a shared variable: a failure cannot pass silently as a missing result. | Call | Returns when the block succeeds | When the block raised | When a limit expires | |---|---|---|---| | `Thread.new { ... }` | the new `Thread`, immediately | (not applicable) | (not applicable) | | `thr.join` | `thr` | re-raises the exception | (no limit) | | `thr.join(2)` | `thr` | re-raises the exception | `nil`, thread still running | | `thr.value` | the block's value | re-raises the exception | (no limit; waits) | ## When the main thread exits The main thread is special. When it reaches the end of the script, or calls `exit`, Ruby **kills every other thread** before the process ends. A thread started with `Thread.new { sleep 1; save_rates }` and never joined will usually be killed before `save_rates` runs. Always join, or call `value` on, threads whose work must finish. ## Collecting 50 page fetches The idiomatic fan-out and fan-in: ```ruby threads = urls.map { |url| Thread.new(url) { |u| Net::HTTP.get(u) } } pages = threads.map(&:value) ``` - Passing `url` as an argument makes each thread's input explicit. Blocks passed to `each` or `map` already get a fresh block parameter per iteration, but a `for` loop does not create a new variable scope, so a thread started inside `for url in urls` that reads `url` later can see a later value. - `threads.map(&:value)` returns the results in the same order as `urls`, regardless of which thread finished first. - If one fetch raised, `map(&:value)` re-raises at that element; handle errors inside the block (returning `nil` or an error object) when partial results are acceptable. For unbounded lists, starting one thread per item is wasteful; a fixed set of workers pulling from a queue is the usual fix. ## Common mistakes - **Reading `join`'s return value as the result.** `threads.map(&:join)` gives you an array of `Thread` objects, not page bodies; use `value`. - **Treating `nil` from `join(limit)` as failure handling.** The thread is still running and still holds whatever it opened; the caller has merely stopped waiting. - **Writing results into a shared Array from each thread** and then reading it without joining. The array may be incomplete, and the order depends on scheduling. Returning the result from the block and collecting it with `value` avoids both problems. - **Starting threads at the bottom of a script** and letting the script end. The main thread's exit kills them mid-work, often before any output appears. - **Joining inside the loop that starts the threads** (`urls.each { |u| Thread.new { fetch(u) }.join }`). Each thread finishes before the next starts, so the program is serial again with extra overhead.
- Why can threads started inside a Ruby for loop all see the same loop variable?A `for` loop does not create a new variable scope, so every block created in its body closes over the one `url` variable, which the loop keeps reassigning. A thread that reads it after the next iteration sees the newer value. `each` gives each iteration a fresh block parameter; passing the value as `Thread.new(url) { |u| ... }` makes it explicit either way.
- If thr.join(5) returns nil, is the thread stopped?No. `nil` only means the limit expired; the thread keeps running. To stop it you must signal it through shared state it checks, or use `Thread#kill`/`Thread#raise`, which interrupt it at an arbitrary point and are risky for code holding resources.
saying these in an interview costs you the question
- Thread#join returns the value the thread's block computed.
- join(limit) raises an error when the thread does not finish in time.
- A nil from join(limit) means the thread was stopped.
- An exception in a thread is lost; join and value never see it.
- Ruby waits for all running threads to finish before the program exits.