Concurrency and Channels in Go

harinathar

Harinatha R

Posted on November 29, 2021

Concurrency and Channels in Go

This article assume that you have read about Golang, and know the basics at least.

Overview:

Before explaining Go channels we should look at what is concurrency is and what are the problems.

We could run our tasks sequentially in a single thread. But it does not fit for some problems. So we can use more than one thread to run our task asynchronously. Concurrency is not same as parallelism where tasks are executed simultaneously. Runnings tasks as parallel is a mix of hardware and software requirements. But concurrency is about code design.

In computer science, concurrency is is the ability of different parts or units of a program, algorithm, or problem to be executed out-of-order or in partial order without affecting the final outcome.

alt text

Several problems may arise in concurrency. Shared resource is one of them. Shared resources can be accessed by several threads concurrently. So the sequence of access to that resource is important. When a thread tries to read a shared variable, another thread may change the value of shared variable. That may cause inconsistency.

Concurrency Solutions in Go

We may solve concurrency problems using Mutexes, Semaphores, Locks etc. Basically, when one thread tries to access the shared resource, it locks the critical section, in this way other threads can not access the shared resource until the section is unlocked.

Go solves these problems in another way. It uses goroutines instead of threads and uses channels instead of accessing to shared state.

Goroutines:

Threads in a traditional Java application map directly to OS threads by JVM. Go uses goroutines rather than threads. Goroutines are dividend into small number of OS threads. They exist only in the virtual space of go runtime. Go has a segmented stack that grows when needed. That means it is controlled by Go runtime, not OS.

  • Goroutines have other advantages like startup time. Goroutines are started faster than threads.

  • Goroutines are created with only 2 KB stack size. A Java thread takes about 1 MB stack size.

func goRoutineA(a <-chan int) {
    val := <-a
    fmt.Println("goRoutineA received the data", val)
}
func main() {
    ch := make(chan int)
    go goRoutineA(ch)
    time.Sleep(time.Second * 1)
}
Enter fullscreen mode Exit fullscreen mode

You see the go keyword when call it. It makes function asynchronous. We could do the same thing in Java by creating a thread and invoking callback method inside that thread.

How goroutines communicate?

Goroutines are good and easy to use. But how they communicate?
We have learned a term called shared resource in the previous chapter. Do the gotoutine accessed to a shared resource? The answer is No.

The philosophy behind the Go's concurrency is:

Do not communicate by sharing the memory; share memory by communicating.

It is not preferred to share resource in Go, Instead use channels to communicate goroutines.

Go channels:

Go channels are like pipes. One goroutine sends the data and another goroutine receives that data from the other side. There are 2 types of channels called buffer channels and unbuffered channels.

ch := make(chan string) //unbuffered channel 
ch := make(chan string, 5) //buffered channel
Enter fullscreen mode Exit fullscreen mode

There is a given capacity to hold in buffered channel. But unbuffered channel does not have capacity to hold more than one data. that means, only one piece of data fits through the unbuffered channel at a time.

hchan struct
When we write make(chan int, 5)channel is created from the hchan struct, which has the following fields.

type hchan struct {
    qcount   uint           // total data in the queue
    dataqsiz uint           // size of the circular queue
    buf      unsafe.Pointer // points to an array of dataqsiz elements
    elemsize uint16
    closed   uint32
    elemtype *_type // element type
    sendx    uint   // send index
    recvx    uint   // receive index
    recvq    waitq  // list of recv waiters
    sendq    waitq  // list of send waiters

    // lock protects all fields in hchan, as well as several
    // fields in sudogs blocked on this channel.
    //
    // Do not change another G's status while holding this lock
    // (in particular, do not ready a G), as this can deadlock
    // with stack shrinking.
    lock mutex
}

type waitq struct {
   first *sudog
   last  *sudog
}
Enter fullscreen mode Exit fullscreen mode

Lets put descriptions to a few fields that we encountered in the channel structure.

dataqsize Is the size of the buffer mentioned above, that is make(chan T, N), the N.
elemsize Is the size of a channel corresponding to a single element.
buf is the circular queue where our data is actually stored. (used only for buffered channel)
closed Indicates whether the current channel is in the closed state. After a channel is created, this field is set to 0, that is, the channel is open; by calling close to set it to 1, the channel is closed.
sendx and recvx is state field of a ring buffer, which indicates the current index of buffer — backing array from where it can send data and receive data.
recvq and sendq waiting queues, which are used to store the blocked goroutines while trying to read data on the channel or while trying to send data from the channel.
lock To lock the channel for each read and write operation as sending and receiving must be mutually exclusive operations.

sudog represent the goroutine. sudog struct:

type sudog struct {
   g     *g             //goroutine
   elem  unsafe.Pointer // data element 
   ...
}
Enter fullscreen mode Exit fullscreen mode

By default, writing and reading from an unbuffered channel is blocking operation. When one goroutine sends data, it is blocked until other goroutines receive data from the channel. It is the same for the receiving part. When one goroutine tries to receive data from the channel, it is blocked until a data sent to the channel. It is kind of the same for the buffered channel. Sender goroutine is blocked when capacity is full until other goroutines fetch the data from the channel.

Go channels are highly recommended to use in Go applications when dealing with concurrency. But if your problem can not be solved with channels, you may still use other solutions by sync package. This package provides low-level components like mutexes.

Conclusion:

I hope thing article is helpful to learn basics of goroutines and channels. If you have any thoughts or feedbacks, please write it in the discussion section below.

💖 💪 🙅 🚩
harinathar
Harinatha R

Posted on November 29, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related