The Fundamentals
What and Why?
What is concurrency, and Why is it important?
Sequential, Concurrent & Parallel Processing
In each of the definitions below, we use the image of the glass with the purple boba to explain the concept. The goal in the examples is to empty the glass from the boba balls.
- Sequential: The execution of a process line by line.
A single straw is in the glass to sip out the boba. - Concurrent: The execution of multiple processes within the same period (not at the exact same time).
Multiple straws are placed in the glass, and a person can sip using a single straw at a time but can switch between straws quickly (boba doesn't slide back down); if the person is fast enough, it will seem the balls are moving up the straws in parallel. - Parallel: The execution of multiple processes simultaneously.
Multiple straws and a person can sip out the boba using all straws.
Use Cases
Concurrency is essentially multitasking. It allows us to run multiple processes at the same time.
So why is there such a difference between concurrency and parallelism? That’s because processors are so fast that switching between multiple tasks can seem parallel, but it’s not. It’s similar to how the eye sees a moving image when it is just changing pixels.
With modern computers, multi-core processors give us multiple threads that our processes can execute against, enabling the ability of parallelism.
Examples:
- We can run a browser, document editor, and video player simultaneously.
- We can break down a massive file into multiple parts and process each separately instead of going line by line.
- We can create a web server that accepts and processes multiple requests instead of allowing additional requests only after each request is complete.
Essentially, concurrency is in all our devices which run many processes simultaneously.
Note: Parallelism is a form of concurrency, but concurrency is not parallelism.
Goroutines
- In Go, Goroutines allow us to process concurrently rather than sequentially.
- The
go
keyword creates a new Goroutine. - The
main()
function is a Goroutine, and it is the root process. - Goroutines use a fork-join model.
– A function/closure/method can fork, execute separately, and re-join the parent process. - The essential idea in Goroutines is the concept of blocking.
– A blocking call will stop the routine from continuing to the next call until it is ready.
– Blocking is important in concurrency. For example, when we want to wait for processes that have forked to return before we end the process or move to the next part. More on this in Part 2.
Notice: None of the above functions are guaranteed to complete their execution (although some or all may). All functions in this test goroutine fork to their own non-blocking goroutines. They do not ask the parent process to wait for their completion. Essentially, they fork, but the parent routine may end before they finish their execution and re-join!
Goroutine Memory Utilization
- Goroutines are very light-weight (~3KB, ~3 cheap instructions per function call)
– 100s of thousands of Goroutines can run in the same address space. In other words, you can run millions of goroutines with modern memory capacities! - Goroutines execute in the same address space they are created in and host functions!
– We must pass a copy of the variable's value to the goroutine; otherwise, it will use whatever value it has when it runs (undetermined)!
– We need to synchronize shared memory to ensure goroutines access a variable one at a time and avoid dirty reads (borrowed from database terminology).
– We don't have to worry about accessing freed space because the runtime will not garbage-collect any references to variables until they are out of scope. - When executing against data using multiple goroutines, the order is never guaranteed. The order is not guaranteed because we don't know when a thread is available for the goroutine to execute. We need synchronization primitives like channels and locking (In Part 2)!
Notice: On my machine running 10,000 goroutines returned ~3.5 KB!
Notice: When passing a changing variable value to a goroutine make sure you pass a copy of the variable’s value, otherwise it will use whatever value is available at the time it runs. Since our operations are very light, usually we get the same number printed three times (but not necessarily).
On that note, for pointers, this means we need to copy the value at the pointer, since pointers are passed by address (although they are passed by value).
Part 2 of this article will discuss the building blocks of concurrency in Go: The synchronization primitives and channels!
If you liked this post or have questions, you can find me on Twitter @snassr_.