Course outline

Ruby / Integration: data, resources, runtime evidence, and trust / Lesson 16

Choose concurrency with explicit state and lifetime ownership

Distinguish threads, mutex-protected invariants, cooperative fibers, and version-scoped Ractor boundaries without universal parallelism claims.

About 5 min readContent reviewed
On this page

Lesson goals

  • Protect the complete shared-state transition rather than assuming an interpreter lock makes the application atomic.
  • Assign an owner to completion, errors, cancellation, and every concurrency primitive.

Different concurrency primitives solve different problems

Threads can share mutable objects, fibers provide cooperative suspension, and Ractors impose their own sharing and messaging restrictions. These are not interchangeable wrappers around a block. Runtime engine and native-library behavior affect whether work can execute in parallel and when I/O permits other execution. Do not promise CPU speedup merely because a script creates threads. This course uses the Ruby 3.3 documentation baseline and makes no claim that CRuby, JRuby, or TruffleRuby have identical execution behavior. Define the workload, state boundary, and lifecycle need before choosing an implementation strategy.

A compound update needs a coherent protection protocol

Incrementing a counter conceptually reads, computes, and writes. An implementation-level lock does not establish a business transaction across arbitrary Ruby operations or external calls. A Mutex can coordinate a critical section only when every competing access follows the same protocol. Protect all fields needed for the invariant together; separate locks or individually safe operations can still expose an invalid combined state. Keep the critical section focused and avoid unnecessary external waiting while holding it. A process-local mutex does not coordinate another application process or protect an authoritative database row by itself.

Join the workers before reading the final result

The program starts two tiny threads with two protected increments each. Joining both workers places the final read after their completion, so the model expects four. This is a deliberately small illustrative calculation with no workload generation or external service. Thread failures also need observation and a declared application policy; abandoning workers when the main lifetime ends is not reliable background processing. No Ruby interpreter was available, so neither thread scheduling nor the Mutex API was exercised here. The expected value follows the documented protocol, not a measured race or performance experiment.

ruby A bounded mutex-protected thread model
mutex = Thread::Mutex.new
count = 0
workers = 2.times.map do
  Thread.new do
    2.times { mutex.synchronize { count += 1 } }
  end
end
workers.each(&:join)
puts count
Expected output
4

Expected output is 4 under the declared protocol. All writes share the mutex and the read follows joins. The program was not run, benchmarked, or used as a load test.

A Fiber resumes only under a cooperative protocol

Creating a Fiber does not start it immediately. resume runs it until yield or completion, and a later resume continues from the suspension point. A fiber is not automatically parallel or nonblocking I/O. Fiber scheduler integration can change how supported blocking operations cooperate, but depends on the configured scheduler and API. The example uses explicit resume and yield with fixed strings so control order is clear. It owns no resource that must survive suspension. Real fiber-based I/O still needs cancellation, failure, and cleanup ownership rather than assuming cooperative syntax settles every lifetime question.

ruby Explicit cooperative suspension and completion
fiber = Fiber.new do
  Fiber.yield 'first'
  'last'
end
puts fiber.resume
puts fiber.resume
puts fiber.alive?
Expected output
first
last
false

Expected output is first, last, false. The second resume completes the fiber. No scheduler, asynchronous I/O, or Ruby runtime test was executed.

Treat isolation and transfer as version-specific contracts

Ractor in the selected Ruby 3.3 context has experimental aspects and restricts what can be shared or transferred across its boundary. It is not a general permission to pass arbitrary resource-owning objects into parallel work. Shareability, copying, moving, and recursive freezing have different consequences for existing aliases. Keep messages small, identify requests and outcomes, and define worker shutdown and late-result handling. Test the actual engine and library compatibility before adopting the model. This course runs no Ractor, thread, fiber scheduler, concurrency stress, or throughput experiment and makes no production concurrency guarantee.

Common errors and fixes

A shared count loses updates despite assumptions about the interpreter.

Why it happens
The application transition lacked a complete synchronization protocol.
How to fix it
Protect the invariant and all access paths with the appropriate owner or synchronization.
Prevent it next time
Reason about compound operations rather than relying on engine folklore.

A Fiber is expected to run before resume.

Why it happens
Cooperative execution was confused with automatic thread startup.
How to fix it
Use the declared resume/yield or scheduler protocol.
Prevent it next time
Document who drives and terminates the fiber.

A local Mutex is treated as cross-process inventory protection.

Why it happens
Memory coordination was confused with authoritative shared storage.
How to fix it
Enforce the real invariant at its authoritative persistence boundary.
Prevent it next time
State the exact scope of each concurrency mechanism.

Check your understanding

1. Does one successful run of an unsynchronized counter prove correctness?

Show worked solution

No. A race depends on scheduling and may not appear in that run. Establish a synchronization argument covering every access and verify representative isolated cases. Testing can expose defects but cannot turn an unprotected compound operation into an atomic one. Record the engine and environment rather than generalizing from a lucky result.

2. Why must a background worker still have an owner after the caller stops waiting?

Show worked solution

Its result, exception, resources, and possible external effects still need a defined destination and cleanup policy. Decide whether to retain, cancel cooperatively, or reconcile the work. A timeout or abandoned reference does not prove the computation stopped or an external operation never completed. Bound pending state and preserve request identity where relevant.

Key takeaways

  • Concurrency changes ownership and failure obligations as well as scheduling.
  • Engine-specific behavior must not become a universal parallelism claim.

Official references