📄 Page
1
M A N N I N G James Cutajar
📄 Page
2
zz z z zz z zz zzz 1 client opens connection to main goroutine 2 connection sent over channel to worker pool 4 HTTP request 5 HTTP response main goroutine 3 idle worker wakes up and handles client's request worker pool browser channel acting as the work queue Using a Worker Pool in an HTTP Server
📄 Page
3
Learn Concurrent Programming with Go
📄 Page
4
(This page has no text content)
📄 Page
5
Learn Concurrent Programming with Go JAMES CUTAJAR M A N N I N G SHELTER ISLAND
📄 Page
6
For online information and ordering of this and other Manning books, please visit www.manning.com. The publisher offers discounts on this book when ordered in quantity. For more information, please contact Special Sales Department Manning Publications Co. 20 Baldwin Road PO Box 761 Shelter Island, NY 11964 Email: orders@manning.com ©2024 by Manning Publications Co. All rights reserved. No part of this publication may be reproduced, stored in a retrieval system, or transmitted, in any form or by means electronic, mechanical, photocopying, or otherwise, without prior written permission of the publisher. Many of the designations used by manufacturers and sellers to distinguish their products are claimed as trademarks. Where those designations appear in the book, and Manning Publications was aware of a trademark claim, the designations have been printed in initial caps or all caps. Recognizing the importance of preserving what has been written, it is Manning’s policy to have the books we publish printed on acid-free paper, and we exert our best efforts to that end. Recognizing also our responsibility to conserve the resources of our planet, Manning books are printed on paper that is at least 15 percent recycled and processed without the use of elemental chlorine. The author and publisher have made every effort to ensure that the information in this book was correct at press time. The author and publisher do not assume and hereby disclaim any liability to any party for any loss, damage, or disruption caused by errors or omissions, whether such errors or omissions result from negligence, accident, or any other cause, or from any usage of the information herein. Manning Publications Co. Development editor: Becky Whitney 20 Baldwin Road Technical editor: Steven Jenkins PO Box 761 Review editor: Mihaela Batinić Shelter Island, NY 11964 Production editor: Kathy Rossland Copy editor: Andy Carroll Proofreader: Melody Dolab Technical proofreader: Nicolas Modrzyk Typesetter: Gordan Salinovic Cover designer: Marija Tudor ISBN 9781633438385 Printed in the United States of America
📄 Page
7
v brief contents PART 1 FOUNDATIONS..................................................................1 1 ■ Stepping into concurrent programming 3 2 ■ Dealing with threads 16 3 ■ Thread communication using memory sharing 42 4 ■ Synchronization with mutexes 66 5 ■ Condition variables and semaphores 89 6 ■ Synchronizing with waitgroups and barriers 114 PART 2 MESSAGE PASSING .........................................................139 7 ■ Communication using message passing 141 8 ■ Selecting channels 163 9 ■ Programming with channels 187 PART 3 MORE CONCURRENCY....................................................215 10 ■ Concurrency patterns 217 11 ■ Avoiding deadlocks 245 12 ■ Atomics, spin locks, and futexes 273
📄 Page
8
vi contents preface xi acknowledgments xiii about this book xv about the author xix about the cover illustration xx PART 1 FOUNDATIONS ....................................................... 1 1 Stepping into concurrent programming 3 1.1 About concurrency 5 1.2 Interacting with a concurrent world 5 1.3 Increasing throughput 6 1.4 Improving responsiveness 8 1.5 Programming concurrency in Go 9 Goroutines at a glance 9 ■ Modeling concurrency with CSP and primitives 9 ■ Building our own concurrency tools 10 1.6 Scaling performance 11 Amdahl’s law 11 ■ Gustafson’s law 13
📄 Page
9
CONTENTS vii 2 Dealing with threads 16 2.1 Multiprocessing in operating systems 17 2.2 Abstracting concurrency with processes and threads 20 Concurrency with processes 20 ■ Creating processes 22 ■ Using multiprocessing for common tasks 23 ■ Concurrency with threads 24 A multithreaded application in practice 28 ■ Using multiple processes and threads together 29 2.3 What’s so special about goroutines? 29 Creating goroutines 29 ■ Implementing goroutines in the user space 33 ■ Scheduling goroutines 37 2.4 Concurrency versus parallelism 38 2.5 Exercises 39 3 Thread communication using memory sharing 42 3.1 Sharing memory 43 3.2 Memory sharing in practice 46 Sharing a variable between goroutines 46 ■ Escape analysis 47 Updating shared variables from multiple goroutines 49 3.3 Race conditions 55 Stingy and Spendy: Creating a race condition 56 ■ Yielding execution does not help with race conditions 60 ■ Proper synchronization and communication eliminate race conditions 61 The Go race detector 62 3.4 Exercises 63 4 Synchronization with mutexes 66 4.1 Protecting critical sections with mutexes 67 How do we use mutexes? 67 ■ Mutexes and sequential processing 70 ■ Non-blocking mutex locks 74 4.2 Improving performance with readers–writer mutexes 77 Go’s readers–writer mutex 77 ■ Building our own read-preferred readers–writer mutex 83 4.3 Exercises 87
📄 Page
10
CONTENTSviii 5 Condition variables and semaphores 89 5.1 Condition variables 90 Combining mutexes with condition variables 90 ■ Missing the signal 96 ■ Synchronizing multiple goroutines with waits and broadcasts 99 ■ Revisiting readers–writer locks using condition variables 102 5.2 Counting semaphores 108 What’s a semaphore? 108 ■ Building a semaphore 109 ■ Never miss a signal with semaphores 110 5.3 Exercises 112 6 Synchronizing with waitgroups and barriers 114 6.1 Waitgroups in Go 115 Waiting for tasks to complete with waitgroups 115 ■ Creating a waitgroup type using semaphores 118 ■ Changing the size of our waitgroup while waiting 120 ■ Building a more flexible waitgroup 122 6.2 Barriers 126 What is a barrier? 126 ■ Implementing a barrier in Go 127 Concurrent matrix multiplication using barriers 130 6.3 Exercises 136 PART 2 MESSAGE PASSING ...............................................139 7 Communication using message passing 141 7.1 Passing messages 142 Passing messages with channels 142 ■ Buffering messages with channels 146 ■ Assigning a direction to channels 149 Closing channels 150 ■ Receiving function results with channels 153 7.2 Implementing channels 154 Creating a channel with semaphores 155 ■ Implementing the Send() function in our channel 157 ■ Implementing the Receive() function in our channel 158 7.3 Exercises 161
📄 Page
11
CONTENTS ix 8 Selecting channels 163 8.1 Combining multiple channels 163 Reading from multiple channels 164 ■ Using select for non-blocking channel operations 166 ■ Performing concurrent computations on the default case 167 ■ Timing out on channels 170 ■ Writing to channels with select 172 ■ Disabling select cases with nil channels 174 8.2 Choosing between message passing and memory sharing 178 Balancing code simplicity 179 ■ Designing tightly versus loosely coupled systems 179 ■ Optimizing memory consumption 180 Communicating efficiently 182 8.3 Exercises 183 9 Programming with channels 187 9.1 Communicating sequential processes 188 Avoiding interference with immutability 189 ■ Concurrent programming with CSP 189 9.2 Reusing common patterns with channels 190 Quitting channels 191 ■ Pipelining with channels and goroutines 193 ■ Fanning in and out 198 ■ Flushing results on close 202 ■ Broadcasting to multiple goroutines 204 Closing channels after a condition 208 ■ Adopting channels as first-class objects 210 9.3 Exercises 213 PART 3 MORE CONCURRENCY ..........................................215 10 Concurrency patterns 217 10.1 Decomposing programs 217 Task decomposition 219 ■ Data decomposition 220 ■ Thinking about granularity 221 10.2 Concurrency implementation patterns 222 Loop-level parallelism 222 ■ The fork/join pattern 227 ■ Using worker pools 230 ■ Pipelining 235 ■ Pipelining properties 240 10.3 Exercises 242
📄 Page
12
CONTENTSx 11 Avoiding deadlocks 245 11.1 Identifying deadlocks 245 Picturing deadlocks with resource allocation graphs 247 Deadlocking in a ledger 251 11.2 Dealing with deadlocks 255 Detecting deadlocks 255 ■ Avoiding deadlocks 258 Preventing deadlocks 264 11.3 Deadlocking with channels 267 11.4 Exercises 271 12 Atomics, spin locks, and futexes 273 12.1 Lock-free synchronization with atomic variables 274 Sharing variables with atomic numbers 274 ■ Performance penalty when using atomics 276 ■ Counting using atomic numbers 278 12.2 Implementing a mutex with spin locks 280 Comparing and swapping 282 ■ Building a mutex 284 12.3 Improving on spin locking 286 Locking with futexes 286 ■ Reducing system calls 289 ■ Go’s mutex implementation 291 12.4 Exercises 294 index 297
📄 Page
13
xi preface “Can you describe a situation in which two threads of execution would cause a dead- lock?” asked my interviewer. After I gave the correct answer, he probed further: “And what would you do in that situation to make sure the code avoids the deadlock?” Luck- ily, I also knew the solution. The interviewer proceeded to show me some code and inquire whether I could spot anything wrong with it. The code had a bad race condi- tion, which I highlighted, and I suggested ways to resolve the problem. This exchange came up during my third and final interview for a core backend developer position at an international tech company in London. In this role, I was exposed to some of the most challenging problems in programming—problems requiring that I sharpen my skills in developing concurrent, low-latency, and high- throughput services. That was more than 15 years ago. Throughout my career in technology, over 20+ years, many circumstances have changed: developers can now work from anywhere, computer languages have evolved to model more-complex businesses, and geeks have become cool since they now run the giant tech companies. However, a few aspects have remained constant: program- mers will always struggle to name variables, a great many problems can be solved by turning systems off and then on again, and concurrent programming skills are still in short supply. The tech industry lacks programmers skilled in concurrency because concurrent programming is perceived as extremely challenging. Many developers even dread using concurrent programming to solve problems. The perception in the tech indus- try is that this is an advanced topic, reserved only for hard-core computer nerds.
📄 Page
14
PREFACExii There are many reasons for this. Developers are not familiar with the concepts and tools available for managing concurrency, and sometimes they fail to recognize how concurrency can be modeled programmatically. This book is my attempt to address this problem and explain concurrent programming in a pain-free manner.
📄 Page
15
xiii acknowledgments I am grateful for the significant contributions of those who helped during the writing of this book. First, I want to thank Joe Cordina, who introduced me to concurrent program- ming early in my studies and sparked my interest in this subject. I also want to thank Miguel David for planting the idea to publish material using Go. I am indebted to Russ Cox for providing me with valuable pointers on Go’s history. Special thanks go to development editor Becky Whitney, technical proofreader Nicolas Modrzyk, and technical editor Steven Jenkins, who is a senior engineer at a global financial services company and has designed, built, and supported systems from startups to global enterprises. He finds Go useful for efficiently building and delivering scalable solutions. Their professionalism, expertise, and dedication ensured that this book meets the highest standards of quality. To all the reviewers: Aditya Sharma, Alessandro Campeis, Andreas Schroepfer, Arun Saha, Ashish Kumar Pani, Borko Djurkovic, Cameron Singe, Christopher Bailey, Clifford Thurber, David Ong Da Wei, Diego Stamigni, Emmanouil Chardalas, Ger- mano Rizzo, Giuseppe Denora, Gowtham Sadasivam, Gregor Zurowski, Jasmeet Singh, Joel Holmes, Jonathan Reeves, Keith Kim, Kent Spillner, Kévin Etienne, Man- uel Rubio, Manzur Mukhitdinov, Martin Czygan, Mattia Di Gangi, Miki Tebeka, Mou- hamed Klank, Nathan B. Crocker, Nathan Davies, Nick Rakochy, Nicolas Modrzyk, Nigel V. Thomas, Phani Kumar Yadavilli, Rahul Modpur, Sam Zaydel, Samson Desta, Sanket Naik, Satadru Roy, Serge Simon, Serge Smertin, Sergio Britos Arévalo, Sla- vomir Furman, Steve Prior, and Vinicios Henrique Wentz, your suggestions helped make this a better book.
📄 Page
16
ACKNOWLEDGMENTSxiv Finally, I would like to thank Vanessa, Luis, and Oliver for their unwavering sup- port throughout this project. Their encouragement and enthusiasm kept me moti- vated and inspired me to give my best.
📄 Page
17
xv about this book Learn Concurrent Programming with Go was written to help developers increase their programming skills with more advanced programming in concurrency. Go was chosen as the language in which to present examples because it provides a wide range of tools for fully exploring this concurrent programming world. In Go, these tools are quite intuitive and easy to grasp, letting us focus on the principles and best practices of concurrency. After reading this book, you will be able to ■ Use concurrent programming to write software that is responsive, high- performance, and scalable ■ Grasp the advantages, limits, and properties of parallel computing ■ Distinguish between memory sharing and message passing ■ Utilize goroutines, mutexes, readers-writer locks, waitgroups, channels, and condition variables—and, additionally, understand how to build some of these tools ■ Identify typical errors to watch for when dealing with concurrent executions ■ Improve your programming skills in Go with more advanced, multithreading topics Who should read this book This book is for readers who already have some programming experience and would like to learn about concurrency. The book assumes no prior knowledge of concurrent programming. Though the ideal reader would already have some experience with Go
📄 Page
18
ABOUT THIS BOOKxvi or another C-syntax-like language, this book is also well suited for developers coming from any language—if some effort is spent learning Go’s syntax. Concurrent programming adds another dimension to your programming: pro- grams stop being a set of instructions executing one after the other. This makes it a chal- lenging topic, and it requires you to think about programs in a different way. Thus, being already proficient in Go is not as important as possessing curiosity and drive. This book does not focus on explaining Go’s syntax and features but instead uses Go to demonstrate concurrency principles and techniques. Most of these techniques can be applied to other languages. For Go tutorials and documentation, see https:// go.dev/learn. How this book is organized: A road map This book has three parts with 12 chapters. Part 1 introduces the fundamentals of con- current programming and communication using memory sharing: ■ Chapter 1 introduces concurrent programming and talks about some of the laws governing parallel execution. ■ Chapter 2 discusses the various ways we can model concurrency and the abstrac- tions provided by operating systems and the Go runtime. The chapter also com- pares concurrency and parallelism. ■ Chapter 3 talks about inter-thread communication using memory sharing, and it introduces race conditions. ■ Chapter 4 explores different types of mutexes as solutions to some race condi- tions. It also shows how to implement a basic readers-writer lock. ■ Chapter 5 shows how to use condition variables and semaphores to synchronize concurrent executions. This chapter also describes how to build a semaphore from scratch and improve the readers-writer lock developed in the previous chapter. ■ Chapter 6 demonstrates how to build and use more complex synchronization mechanisms, such as waitgroups and barriers. Part 2 discusses how multiple executions can communicate using message passing instead of memory sharing: ■ Chapter 7 describes message passing using Go’s channels. This chapter shows the various ways channels can be used, and it illustrates how channels can be built on top of memory sharing and synchronization primitives. ■ Chapter 8 explains how we can combine multiple channels by using Go’s select statement. In addition, this chapter gives some guidelines on choosing memory sharing versus message passing when developing concurrent programs. ■ Chapter 9 provides examples and best practices for reusing common message- passing patterns. This chapter also demonstrates the flexibility of having a lan- guage (such as Go) where channels are first-class objects.
📄 Page
19
ABOUT THIS BOOK xvii Part 3 explores common concurrency patterns and some more advanced topics: ■ Chapter 10 lists techniques for breaking down problems so that programs can run more efficiently by employing concurrent programming. ■ Chapter 11 illustrates how deadlock situations can develop when we have con- currency and describes various techniques for avoiding them. ■ Chapter 12 deals with the internals of mutexes. It explains how mutexes are implemented in both the kernel and user space. How to read the book Developers with no experience in concurrency should view the book as a journey, starting with the first chapter and following through to the end. Each chapter teaches new skills and techniques that build on the knowledge acquired in previous ones. Developers who already have some experience with concurrency can read chapters 1 and 2 as a refresher on how operating systems model concurrency and then decide whether to skip to some of the more advanced topics. For example, a reader who is already familiar with race conditions and mutexes may choose to continue learning about condition variables in chapter 5. About the code The source code in the book’s listings is in a fixed-width font to distinguish it from the rest of the document, with keywords in Go set in bold. Code annotations accom- pany many of the listings, highlighting important concepts. To download all the source code in the book, including the exercise solutions, go to https://github.com/cutajarj/ConcurrentProgrammingWithGo. The complete code for the examples in the book is also available for download from the Manning website at https://www.manning.com/books/learn-concurrent-programming-with-go. You can get executable snippets of code from the liveBook (online) version of this book at https://livebook.manning.com/book/learn-concurrent-programming-with-go. The source code in this book requires the Go compiler, which can be downloaded from https://go.dev/doc/install. Note that some of the source code in this book will not work correctly on Go’s online playground—the playground is blocked from cer- tain operations, such as opening web connections. liveBook discussion forum Purchase of Learn Concurrent Programming with Go includes free access to liveBook, Manning’s online reading platform. Using liveBook’s exclusive discussion features, you can attach comments to the book globally or to specific sections or paragraphs. It’s a snap to make notes for yourself, ask and answer technical questions, and receive help from the author and other users. To access the forum, go to https://livebook .manning.com/book/learn-concurrent-programming-with-go/discussion. You can also learn more about Manning’s forums and the rules of conduct at https://livebook .manning.com/discussion.
📄 Page
20
ABOUT THIS BOOKxviii Manning’s commitment to our readers is to provide a venue where a meaningful dialogue between individual readers and between readers and the author can take place. It is not a commitment to any specific amount of participation on the part of the author, whose contribution to the forum remains voluntary (and unpaid). We sug- gest you try asking the author some challenging questions lest his interest stray! The forum and the archives of previous discussions will be accessible from the publisher’s website as long as the book is in print.