├── README.md ├── internal ├── 02-fan-in │ └── fan-in-starter.go ├── 01-fan-out │ └── fan-out.go ├── 03-buffered-channels │ └── buffered-channels.go └── 04-map-reduce │ └── map-reduce.go └── LICENSE /README.md: -------------------------------------------------------------------------------- 1 | # Concurrent Programming in Go 2 | 3 | Completed live-coding exercises for "Concurrent Programming in Go", taught via O'Reilly Media. 4 | -------------------------------------------------------------------------------- /internal/02-fan-in/fan-in-starter.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "sync" 4 | 5 | // https://go.dev/play/p/0YBVXu1N2CR 6 | 7 | func main() { 8 | ch := make(chan int) 9 | var wg sync.WaitGroup 10 | 11 | // TODO: start 10 'producer' goroutines 12 | // Each producer should generate and send 10 integers to the consumer using fanInChan. 13 | 14 | // TODO: start 1 consumer goroutine 15 | // The consumer should receive all of the integers from fanInChan and print them out. 16 | 17 | // Challenge: Use wait-groups to ensure that every goroutine returns before the main() func stops. 18 | 19 | wg.Wait() 20 | } 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Alex Mills 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /internal/01-fan-out/fan-out.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "sync" 7 | "time" 8 | ) 9 | 10 | func main() { 11 | ctx := context.Background() 12 | ctx, cancel := context.WithTimeout(ctx, 1000*time.Millisecond) 13 | defer cancel() // for resource cleanup 14 | 15 | var wg sync.WaitGroup 16 | numWorkers := 10 17 | 18 | workQueue := make(chan int) 19 | 20 | for i := 0; i < numWorkers; i++ { 21 | wg.Add(1) 22 | go func(workerID int) { // worker goroutines 23 | defer wg.Done() // -- 'happens before' line 33 24 | for { // blocks until a msg is received 25 | select { 26 | case msg, ok := <-workQueue: 27 | if !ok { 28 | fmt.Println("channel closed! from worker", workerID) 29 | return 30 | } 31 | DoRPC(ctx, workerID, msg) 32 | case <-ctx.Done(): 33 | fmt.Println("worker", workerID, "ended") // (happens after close at line 32) 34 | return 35 | } 36 | } 37 | }(i) 38 | } 39 | 40 | loop: 41 | for i := 0; i < 100; i++ { 42 | // racing two channel operations against one another 43 | select { 44 | case workQueue <- i: // blocks until a receiver is available 45 | // run this code 46 | case <-ctx.Done(): 47 | fmt.Printf("sender was cancelled while sending message %d\n", i) 48 | break loop 49 | } 50 | } 51 | close(workQueue) // closes the channel (happens before line 24) 52 | wg.Wait() // blocks until the counter is zero (i.e. until all goroutines have finished) 53 | fmt.Println("program ended") 54 | } 55 | 56 | // DoRPC fakes a remote procedure call. 57 | func DoRPC(ctx context.Context, workerID int, msg int) { 58 | fmt.Printf("sending message %d from worker %d\n", msg, workerID) 59 | // TODO: use the ctx in the real RPC. 60 | time.Sleep(100 * time.Millisecond) // fake RPC. 61 | // blocking call -- stops the currently running goroutine. 62 | fmt.Println("worker", workerID, ": message", msg, "was sent") 63 | } 64 | -------------------------------------------------------------------------------- /internal/03-buffered-channels/buffered-channels.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "sync" 7 | ) 8 | 9 | func main() { 10 | ch := NewBufferedChannel(4) 11 | 12 | var wg sync.WaitGroup 13 | wg.Add(2) 14 | 15 | go func(id int) { // senders 16 | for i := 0; i < 10; i++ { 17 | ch.Send(i) 18 | fmt.Println("sender", id, ": value sent:", i) 19 | } 20 | wg.Done() 21 | }(0) 22 | 23 | go func(id int) { 24 | for i := 0; i < 10; i++ { 25 | val := ch.Receive() 26 | fmt.Println("receiver", id, "value received:", val) 27 | 28 | } 29 | wg.Done() 30 | }(0) 31 | wg.Wait() 32 | } 33 | 34 | type BufferedChannel struct { 35 | buffer []int 36 | 37 | head int // next empty index in the buffer (if it exists) 38 | tail int // next unread index in the buffer (if one exists) 39 | 40 | isFull bool // true iff the channel is full 41 | 42 | mut *sync.Mutex 43 | fullWaiters *sync.Cond 44 | emptyWaiters *sync.Cond 45 | } 46 | 47 | func NewBufferedChannel(size int) *BufferedChannel { 48 | result := &BufferedChannel{ 49 | buffer: make([]int, size), 50 | mut: &sync.Mutex{}, 51 | } 52 | result.fullWaiters = sync.NewCond(result.mut) 53 | result.emptyWaiters = sync.NewCond(result.mut) 54 | return result 55 | } 56 | 57 | var ErrEmpty = errors.New("channel was empty") 58 | var ErrFull = errors.New("channel was full") 59 | 60 | func (bc *BufferedChannel) Send(val int) { 61 | bc.mut.Lock() 62 | defer bc.mut.Unlock() 63 | 64 | for bc.head == bc.tail && bc.isFull { 65 | bc.fullWaiters.Wait() // wait for the buffer to be not full 66 | } 67 | 68 | bc.buffer[bc.head] = val 69 | bc.head = (bc.head + 1) % len(bc.buffer) 70 | 71 | if bc.head == bc.tail { 72 | bc.isFull = true 73 | } 74 | bc.emptyWaiters.Signal() 75 | 76 | return 77 | } 78 | 79 | func (bc *BufferedChannel) Receive() int { 80 | bc.mut.Lock() 81 | defer bc.mut.Unlock() 82 | 83 | for bc.head == bc.tail && !bc.isFull { 84 | bc.emptyWaiters.Wait() 85 | } 86 | 87 | val := bc.buffer[bc.tail] 88 | bc.tail = (bc.tail + 1) % len(bc.buffer) 89 | 90 | bc.isFull = false 91 | bc.fullWaiters.Signal() 92 | 93 | return val 94 | } 95 | -------------------------------------------------------------------------------- /internal/04-map-reduce/map-reduce.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "sync" 7 | ) 8 | 9 | var lines = []string{ 10 | "Lorem Ipsum is simply dummy text of the printing and typesetting industry.", 11 | "Lorem Ipsum has been the industry's standard dummy text ever since the", 12 | "when an unknown printer took a galley of type and scrambled it to make a type specimen book.", 13 | "It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.", 14 | } 15 | 16 | func main() { 17 | numMappers := 3 18 | numReducers := 6 // max at 26 -- (because of our hacky choice of hash function) 19 | 20 | lineChan := make(chan string) 21 | wordChannels := make([]chan string, numReducers) 22 | for i := 0; i < numReducers; i++ { 23 | wordChannels[i] = make(chan string) 24 | } 25 | countChannel := make(chan map[string]int) 26 | 27 | // mappers 28 | var mapperWg sync.WaitGroup 29 | for i := 0; i < numMappers; i++ { 30 | mapperWg.Add(1) 31 | go func(id int) { 32 | defer func() { 33 | mapperWg.Done() 34 | if id == 0 { // mapper with id = 0 will close all reducer channels 35 | mapperWg.Wait() // wait for all mappers to conclude sending 36 | for i := 0; i < numReducers; i++ { 37 | close(wordChannels[i]) // close reducer channels. 38 | } 39 | } 40 | }() 41 | 42 | for line := range lineChan { 43 | // take the first letter in the word and use it to send 44 | // to the correct reducer 45 | line = strings.ToLower(line) 46 | words := strings.Split(line, " ") 47 | for _, word := range words { 48 | idx := (int(word[0] - 'a')) % numReducers // dirty trick 49 | wordChannels[idx] <- word 50 | } 51 | } 52 | fmt.Printf("mapper %d finished\n", id) 53 | }(i) 54 | } 55 | 56 | // reducers 57 | var reducerWg sync.WaitGroup 58 | for i := 0; i < numReducers; i++ { 59 | reducerWg.Add(1) 60 | go func(id int) { 61 | defer func() { 62 | reducerWg.Done() 63 | if id == 0 { 64 | reducerWg.Wait() 65 | close(countChannel) 66 | fmt.Println("count channel closing") 67 | } 68 | }() 69 | // counting all the words seen 70 | localMap := make(map[string]int) 71 | for word := range wordChannels[id] { 72 | localMap[word]++ 73 | } 74 | countChannel <- localMap 75 | fmt.Printf("reducer %d finished\n", id) 76 | }(i) 77 | } 78 | 79 | // consumer 80 | var consumerWg sync.WaitGroup 81 | consumerWg.Add(1) 82 | go func() { 83 | defer consumerWg.Done() 84 | for counts := range countChannel { 85 | fmt.Println("consumer received: ", counts) 86 | } 87 | fmt.Println("consumer done") 88 | }() 89 | 90 | // feed the mappers each line of the file 91 | for _, line := range lines { 92 | lineChan <- line 93 | } 94 | close(lineChan) 95 | fmt.Println("all lines sent!") 96 | 97 | reducerWg.Wait() 98 | mapperWg.Wait() 99 | consumerWg.Wait() 100 | } 101 | --------------------------------------------------------------------------------