liveBook · Manning

Circular wait

As explained in Learn Concurrent Programming with Go

A circular wait is a dependency cycle in which each concurrent execution waits for a condition or resource held by another execution in the cycle.

A circular wait is a dependency cycle of two or more concurrent executions: each is blocked waiting for a resource held by the next execution in the cycle. It is one of four conditions required for deadlock, alongside mutual exclusion, waiting while holding resources, and no preemption. A cycle can involve any number of executions, not only two. section 11.1.1

Figure 11.2 Resource allocation graph of the red() and blue() goroutines

Use a resource allocation graph to trace who holds each resource and who is waiting. A closed chain reveals the circular dependency behind a deadlock. section 11.1.1

With channels, a circular wait can arise even without explicit locks. A directory-handling goroutine may be blocked sending file paths on one channel while a file-handling goroutine is blocked sending directory paths on another. Neither goroutine can resume to receive the message that would unblock the other. section 11.3

Directory traversal deadlock

Two unbuffered channels carry directory paths and file paths. The directory handler fills the files channel and waits for a receiver; meanwhile, the file handler finds a subdirectory, tries to send it on the dirs channel, and waits for a receiver. Because both handlers are stopped on sends, traversal deadlocks. Adding a finite buffer or more file-handler goroutines only postpones the failure when a sufficiently large directory is reached. section 11.3 section 11.3

Figure 11.14 Deadlock with two channels

The two-channel diagram makes the blocking cycle visible: each goroutine needs the other to receive before it can continue. section 11.3

Break the cycle rather than merely delaying it:

  • Send channel values from a newly spawned goroutine, freeing the current handler to continue receiving. section 11.3
  • Use select to coordinate receiving and sending so one handler can make progress on either channel operation. section 11.3
  • Redesign message flow to avoid circular dependencies; channel capacity acts like a mutually exclusive resource, so bounded buffering is not a general cure. section 11.5
  • When resource needs are known in advance, request resources in a predefined order to prevent deadlocks. section 11.5

A resource allocation graph can support cycle detection. Go’s runtime also detects deadlock, but only when every goroutine is blocked; it then terminates the program with an error. Treat that error as a signal to inspect waiting relationships, not as proof that every partial stall will be reported. section 11.5

Practice: score updates and channel handlers

Concurrent score updates that lock several player mutexes may deadlock. Practice preventing the problem by changing the locking approach, then adapt the file handler to use select for both receiving and sending across the two channels. section 11.4 section 11.4

How Learn Concurrent Programming with Go covers it

How deep it goes
Defines the condition, visualizes it with allocation graphs, and works through a two-channel failure with several remedies.
Examples
Uses rail crossings, a directory traversal with two channels, and concurrent player-score mutex updates.
What you can do after
Diagnose/debug with Go channel and resource-wait relationships, then break cycles with select, separate goroutines, ordering, or redesigned flows.
How it connects
Connects deadlock to mutual exclusion, no preemption, resource allocation graphs, channels, goroutines, and select.

Related concepts

This explainer was generated by Manning's knowledge graph from the text of Learn Concurrent Programming with Go. The section links point to the parts of the book it draws on.