Concurrency in Golang
Yesterday, I answered a question in Quora about the concurrency model in Go. Now, I feel like I want to say more!! Concurrency in Golang is one of the most powerful features in the language. Numerous folks covered the topic, ranging in their takes from very simple to overly complicated. Today, it’s my turn to give my two cents.
Concurrency in Golang is a way of thinking more than just syntax. In order to harness the power of Go , you need to first understand how Go approaches concurrent execution of code. Go relies on a concurrency model called CSP ( Communicating Sequential Processes) , which -in computer science- is basically a model that describes interactions between concurrent systems. But since this is not a scientific paper, I will skip the formalities and jump into the practical description of what it does.
A lot of Go talks, presentations and documentations use the following phrase when explaining concurrency in Golang:
Do not communicate by sharing memory; instead, share memory by communicating.
Sounds good, good. But what does that really mean. It took me a while to fully envelope the concept with my head. But once I did, programming in Go became way more fluid for me. Albert Einstein once said “If you can’t explain it simply, you don’t understand it well enough.” , so here is the simplest explanation I could come up with
Do not communicate by sharing memory
In mainstream programming language, when you think of concurrent execution of code, you mostly think of a bunch of threads running in parallel performing some kind of a complex operation. Then, most often than not, you would need to share data structures\variables\memory\whathaveyou between the different threads. You do this by either locking the piece of memory so that not two threads can access\write to it at the same time, or you just let it roam free and hope for the best. This is typically how different threads “communicate” in a lot of popular programming languages, and this typically causes all sorts of issues with race conditions, memory management , random-weird-unexplained-exceptions-waking-you-up-all-night ….et cetera.
Instead, share memory by communicating
So how does Go do it? Instead of locking variables to share memory, Go allows you to communicate (or send) the value stored in your variable from one thread to another (in reality, it’s not exactly a thread but let’s just think of it as a thread for now). The default behavior is that both the thread sending the data and the one receiving the data will wait till the value reaches it’s destination. The “waiting” of the threads forces proper synchronization between threads when data is being exchanged. Thinking that way about concurrency before rolling your sleeves and starting your code design; allows for more stable software.
To put it more clearly: the stability will arise from the fact that -by default- neither the sending thread nor the receiving thread will do anything till the value transfer is complete. Meaning, that there is much less opportunity for race conditions or similar issues arising from either of the threads acting on the data before the other thread is done with it.
Go gives you native features that you can use to achieve this behavior without needing to call extra libraries or frameworks, the behavior is simply built into the language. Go also allow you to have a “buffered channel” should you need it. Meaning that in some cases, you wouldn’t want both threads to lock or synchronize till a value is transferred, instead, you would want the synchronization\locking to only take place if you fill up a predefined number of values waiting to be processed in the channel between the two threads.
A word of caution though, this model can be overused. You have to sense when to use it, or when to revert to the good old share-my-memory model. For example, a reference count is best protected inside a lock, same for file access. Go will support you there as well via the sync package.
Coding concurrency in Golang
So let’s talk some code. How can we implement the share-by-communication model? read on
In Go, a “goroutine” serves the concept of a thread in our description above. In reality, it is not really a thread, it is basically a function that can run concurrently with other goroutines in the same address space. They are multiplexed amongst the O.S. threads , so that if one blocks, others can continue. All the synchronization and memory management is carried out natively by Go. The reason why they are not real threads is because they are not necessarily parallel all the time. However, you get concurrent behavior due to the multiplexing and synchronization. To start a new goroutine, you just use the keyword “go”:
go processdataFunction()
A “go channel” is another key concept to achieve concurrency in Go. This is the channel that is used to communicate memory between goroutines. To create a channel, “make” is used:
myChannel := make(chan int64)
Creating a buffered channel to allow for more values to be queued before the goroutines wait, looks like this
myBufferedChannel := make(chan int64,4)
In the above two examples, I assumed the channel variables weren’t created prior to this. This is why I used “:=” to create the variable with the inferred type as opposed to “=” which would only assign the value and will cause a compile error if the variable wasn’t declared previously.
Now to use a channel , you use the “<-” notation. The goroutine sending the value will assign it to the channel like this:
mychannel <- 54
The goroutine receiving the value, will extract it from the channel and assign it to a new variable like this:
myVar := <- mychannel
Now let’s see an example to show case concurrency in Golang
package main import ( "fmt" "time" ) func main() { ch := make(chan int) //create a channel to prevent the main program from exiting before the done signal is received done := make(chan bool) go sendingGoRoutine(ch) go receivingGoRoutine(ch,done) //This will prevent the program from exiting till a value is sent over the "done" channel, value doesn't matter <- done } func sendingGoRoutine(ch chan int){ //start a timer to wait 5 seconds t := time.NewTimer(time.Second*5) <- t.C fmt.Println("Sending a value on a channel") //this goroutine will wait till another goroutine received the value ch <- 45 } func receivingGoRoutine(ch chan int, done chan bool){ //this gourtine will wait till the channel received a value v := <- ch fmt.Println("Received value ", v) done <- true }
The output will look like this:
Sending a value on a channel Received value 45
Interested to learn more about Go? Please take a moment to check my Mastering Go Programming course. It provides a unique combination of covering deep internal aspects of the language, while also diving into very practical topics about using the language in production environments.
thread pooling and user qaouts are different things. Thread pools are user-unaware. However, maybe techniques introduced with pooling can help somewhat. Why do you absolutely want to limit user activity to a predefined number of concurrent statements? I think there is something like priority under the covers of this thread pool- I read earlier that first transaction statements have low prio. While I personally would find this automagic feature questionable (why should transaction users suffer, if their requests are non-overlapping), in general priorities are interesting. So your problem could also be solved by setting request priority low if the same user has many normal prio requests already.It is not necessary to fully block a query, on Unix every blocking means there is a filler thread that stucks in pthread_cond_wait and does not do anything useful.