diff --git a/cond/README.md b/cond/README.md new file mode 100644 index 0000000..85c956d --- /dev/null +++ b/cond/README.md @@ -0,0 +1,36 @@ +## cond + +[sync.Cond](https://golang.org/pkg/sync/#Cond) — примитив синхронизации для эффективных межгорутинных нотификаций. + +Обычно использование выглядит так: одни горутины ждут выполнения некоторого условия, +а другие горутины выполняют это условие и информируют ждущих. + +### Что нужно сделать? + +Нужно написать реализацию Cond используя каналы. + +Использование пакета [sync](https://golang.org/pkg/sync) в этой задаче запрещено! + +```go +type Cond struct { + L Locker +} + +func New(l Locker) *Cond {} + +func (c *Cond) Wait() {} +func (c *Cond) Signal() {} +func (c *Cond) Broadcast() {} +``` + +`Locker` это [sync.Locker](https://golang.org/pkg/sync/#Locker), но мы не хотим использовать [sync](https://golang.org/pkg/sync). + +У каждого `Cond` есть блокировка `L`, передающаяся в конструкторе. +`Cond` поддерживает FIFO очередь ожидающих горутин. + +* `Wait()` паникует, если `L` не взята. +* `Wait()` добавляет текущую горутину в очередь, возвращает блокировку `L`, блокирует текущую горутину. + Заснувшая горутина может быть разбужена другой горутиной через вызов `Signal()` или `Broadcast()`. + При просыпании засыпавшая горутина продолжит исполнение, возьмёт `L` и завершит вызов `Wait()`. +* `Signal()` извлекает и разблокирует первую блокировку из очереди, если такая есть, иначе no-op. +* `Broadcast()` извлекает и разблокирует все блокировки из очереди. no-op, если очередь пустая. diff --git a/cond/cond.go b/cond/cond.go new file mode 100644 index 0000000..8178f76 --- /dev/null +++ b/cond/cond.go @@ -0,0 +1,61 @@ +// +build !solution + +package cond + +// A Locker represents an object that can be locked and unlocked. +type Locker interface { + Lock() + Unlock() +} + +// Cond implements a condition variable, a rendezvous point +// for goroutines waiting for or announcing the occurrence +// of an event. +// +// Each Cond has an associated Locker L (often a *sync.Mutex or *sync.RWMutex), +// which must be held when changing the condition and +// when calling the Wait method. +type Cond struct { + L Locker +} + +// New returns a new Cond with Locker l. +func New(l Locker) *Cond { + return &Cond{L: l} +} + +// Wait atomically unlocks c.L and suspends execution +// of the calling goroutine. After later resuming execution, +// Wait locks c.L before returning. Unlike in other systems, +// Wait cannot return unless awoken by Broadcast or Signal. +// +// Because c.L is not locked when Wait first resumes, the caller +// typically cannot assume that the condition is true when +// Wait returns. Instead, the caller should Wait in a loop: +// +// c.L.Lock() +// for !condition() { +// c.Wait() +// } +// ... make use of condition ... +// c.L.Unlock() +// +func (c *Cond) Wait() { + +} + +// Signal wakes one goroutine waiting on c, if there is any. +// +// It is allowed but not required for the caller to hold c.L +// during the call. +func (c *Cond) Signal() { + +} + +// Broadcast wakes all goroutines waiting on c. +// +// It is allowed but not required for the caller to hold c.L +// during the call. +func (c *Cond) Broadcast() { + +} diff --git a/cond/cond_test.go b/cond/cond_test.go new file mode 100644 index 0000000..1337a36 --- /dev/null +++ b/cond/cond_test.go @@ -0,0 +1,184 @@ +package cond + +import ( + "sync" + "testing" + "time" +) + +func TestCondSignal(t *testing.T) { + var m sync.Mutex + c := New(&m) + n := 2 + running := make(chan bool, n) + awake := make(chan bool, n) + for i := 0; i < n; i++ { + go func() { + m.Lock() + running <- true + c.Wait() + awake <- true + m.Unlock() + }() + } + for i := 0; i < n; i++ { + <-running // Wait for everyone to run. + } + for n > 0 { + select { + case <-awake: + t.Fatal("goroutine not asleep") + default: + } + m.Lock() + c.Signal() + m.Unlock() + <-awake // Will deadlock if no goroutine wakes up + select { + case <-awake: + t.Fatal("too many goroutines awake") + default: + } + n-- + } + c.Signal() +} + +func TestCondSignalGenerations(t *testing.T) { + var m sync.Mutex + c := New(&m) + n := 100 + running := make(chan bool, n) + awake := make(chan int, n) + for i := 0; i < n; i++ { + go func(i int) { + m.Lock() + running <- true + c.Wait() + awake <- i + m.Unlock() + }(i) + if i > 0 { + a := <-awake + if a != i-1 { + t.Fatalf("wrong goroutine woke up: want %d, got %d", i-1, a) + } + } + <-running + m.Lock() + c.Signal() + m.Unlock() + } +} + +// nolint +func TestCondBroadcast(t *testing.T) { + var m sync.Mutex + c := New(&m) + n := 200 + running := make(chan int, n) + awake := make(chan int, n) + exit := false + for i := 0; i < n; i++ { + go func(g int) { + m.Lock() + for !exit { + running <- g + c.Wait() + awake <- g + } + m.Unlock() + }(i) + } + for i := 0; i < n; i++ { + for i := 0; i < n; i++ { + <-running // Will deadlock unless n are running. + } + if i == n-1 { + m.Lock() + exit = true + m.Unlock() + } + select { + case <-awake: + t.Fatal("goroutine not asleep") + default: + } + m.Lock() + c.Broadcast() + m.Unlock() + seen := make([]bool, n) + for i := 0; i < n; i++ { + g := <-awake + if seen[g] { + t.Fatal("goroutine woke up twice") + } + seen[g] = true + } + } + select { + case <-running: + t.Fatal("goroutine did not exit") + default: + } + c.Broadcast() +} + +// nolint +func TestCondSignalStealing(t *testing.T) { + for iters := 0; iters < 1000; iters++ { + var m sync.Mutex + cond := New(&m) + + // Start a waiter. + ch := make(chan struct{}) + go func() { + m.Lock() + ch <- struct{}{} + cond.Wait() + m.Unlock() + + ch <- struct{}{} + }() + + <-ch + m.Lock() + m.Unlock() + + // We know that the waiter is in the cond.Wait() call because we + // synchronized with it, then acquired/released the mutex it was + // holding when we synchronized. + // + // Start two goroutines that will race: one will broadcast on + // the cond var, the other will wait on it. + // + // The new waiter may or may not get notified, but the first one + // has to be notified. + done := false + go func() { + cond.Broadcast() + }() + + go func() { + m.Lock() + for !done { + cond.Wait() + } + m.Unlock() + }() + + // Check that the first waiter does get signaled. + select { + case <-ch: + case <-time.After(2 * time.Second): + t.Fatalf("First waiter didn't get broadcast.") + } + + // Release the second waiter in case it didn't get the + // broadcast. + m.Lock() + done = true + m.Unlock() + cond.Broadcast() + } +}