Seyed Masoud Hosseini · Overview · Study log · Ideas · Transcript · RSS feed
Distributed Systems · Lecture 2 of 20 · 1:20:22
Lecture 2: RPC and Threads
Study guide
What this lecture covers
This lecture explains why the course uses Go and dives into Go's threading model, the main tool used throughout the labs to manage concurrency. It covers why threads help both with I/O concurrency (overlapping waits on the network) and with multi-core parallelism, then walks through the classic dangers of shared state: races, and the locks and coordination primitives (mutexes, wait groups, channels) used to avoid them.
The lecture demonstrates these ideas hands-on by live-coding and running three versions of a web crawler: a serial version, a version using shared state protected by locks, and a version using channels to communicate between worker goroutines instead of sharing memory. After watching, you should be able to explain the difference between I/O concurrency and parallelism, recognize a race condition in Go code, and understand why concurrent goroutines running over a loop variable need special care.
Key ideas
- Goroutine: Go's name for a thread; each has its own program counter, registers and stack, but shares the same address space with other goroutines in the same process.
- I/O concurrency: using threads so that while one is waiting on a network reply, others can make progress, rather than being about using multiple cores.
- Parallelism: using multiple threads to genuinely run on multiple CPU cores at once for compute-heavy work.
- Race condition: a bug where two threads read and write shared data without synchronization, so the final result depends on unpredictable timing.
- Mutex (lock): a Go primitive (
sync.Mutex) used to make a sequence of operations on shared data atomic with respect to other threads; Go itself has no notion of which variables a lock protects, that association exists only in the programmer's head. - WaitGroup: a counter-based coordination tool used to wait for a known number of goroutines to finish.
- Channels: a way for goroutines to communicate by sending data to each other instead of sharing memory directly, avoiding the need for explicit locks.
- Closures over loop variables: an inner function that captures a loop variable by reference sees later updates to that variable unless the value is explicitly copied, a classic source of subtle bugs.
Walkthrough
Why Go, and the basics of threads (0:02)
The lecture opens by explaining why the course uses Go instead of C++: it has convenient threading, locking and RPC support, and it is type-safe, memory-safe and garbage collected, which eliminates whole classes of bugs that plague threaded C++ code. It then introduces goroutines as Go's version of threads, each with its own stack but sharing one address space, and notes that even a Go program's main function runs as a goroutine.
I/O concurrency, parallelism and background tasks (6:14)
The lecture distinguishes two main reasons to use threads. I/O concurrency lets a program launch several RPCs and wait for replies without blocking other work, useful when a client talks to many servers or a server handles many clients. Multi-core parallelism lets compute-heavy work use every core on a machine, though the lecture notes this matters less for the course's labs than for real production servers. A third, lighter reason is convenience: firing off a goroutine to do something periodically, such as a master server checking whether workers are still alive.
Event-driven programming as an alternative (11:19)
In response to a question, the lecture contrasts threads with event-driven (asynchronous) programming, where a single loop waits for and dispatches events while tracking per-client state manually. Threads are usually easier to program because code reads as straightforward sequential logic, but event loops have much lower memory overhead, which matters if a server must track state for very large numbers of simultaneous clients.
Races and locks (21:29)
Using the example of two threads incrementing a shared global variable, the lecture shows how a load-increment-store sequence can lose an update if both threads execute it around the same time, a race condition. The fix is a mutex: wrapping the shared-data access with Lock and Unlock calls makes the sequence effectively atomic. The lecture stresses that Go has no built-in relationship between a lock and any particular data; the programmer alone is responsible for consistently protecting the right data with the right lock, and for avoiding deadlocks that can arise when two threads acquire two locks in opposite orders.
Coordination and the web crawler example (32:38)
The lecture introduces coordination, cases where threads intentionally wait for each other, using channels, condition variables and WaitGroup. It then works through a web crawler exercise: a serial depth-first crawler, followed by a naive attempt to parallelize it by simply adding go in front of the recursive call, which fails because the outer function returns before the goroutines do any work.
The shared-memory crawler with locks (42:55)
A working concurrent crawler shares a fetched map across goroutines, protected by a mutex, and uses a WaitGroup to know when all recursive crawls have finished. The lecture demonstrates live why the URL loop variable must be passed as a function argument rather than captured directly by the inner closure: because the loop variable is reused each iteration, an unprotected closure would see the wrong URL. It then intentionally removes the locks to show that the race often does not manifest visibly, and uses Go's built-in -race detector to pinpoint the unsynchronized read and write by line number.
The channel-based crawler (1:10:10)
The final version avoids shared memory entirely: a single master goroutine keeps its own private fetched map, spawns one worker goroutine per URL, and workers report the URLs they found back to the master over a channel rather than writing to shared state. The master counts outstanding workers to know when the crawl is complete. This illustrates Go's philosophy of communicating over channels instead of synchronizing access to shared memory.
Before you watch
- Lecture 1 introduces the course and MapReduce, which this lecture assumes some familiarity with as motivation for using threads and RPC.
- Basic familiarity with the Go tutorial (goroutines, mutexes, channels) referenced repeatedly in this lecture is helpful, since the lecture builds directly on it.
Check your understanding
- What is the difference between I/O concurrency and multi-core parallelism, and why does the lecture say the labs mostly care about the former?
- In the increment-a-shared-variable example, why can two threads both end up storing the same final value instead of incrementing twice?
- Why does Go have no association between a
sync.Mutexand the data it protects, and what does that imply for the programmer? - Why did the naive
go crawl(...)version of the web crawler only fetch one page instead of running in parallel? - Why does the channel-based crawler avoid the need for locks that the shared-memory version required?
Chapters
- 0:00 Introduction
- 4:30 Threads
- 6:10 IO Concurrency
- 7:57 Multicore Parallelism
- 9:18 Periodicity
- 11:48 Threads in general
- 12:32 Asynchronous programming
- 15:52 Multiple cores
- 16:57 Threads and processes
- 20:43 Thread challenges
- 23:50 Thread instructions are atomic
- 27:19 How does go know which variable
- 30:16 Should the lock be private
- 32:02 Problems with Threads
- 35:43 Web Crawler
- 47:57 Passing by Reference
- 49:13 Running a Go Routine
- 59:28 String Immutability
From the YouTube description
Lecture 2: RPC and Threads
MIT 6.824: Distributed Systems (Spring 2020)
https://pdos.csail.mit.edu/6.824/
