How are Go channels implemented?

GoChannel

Go Problem Overview


After (briefly) reviewing the Go language spec, effective Go, and the Go memory model, I'm still a little unclear as to how Go channels work under the hood.

What kind of structure are they? They act kind of like a thread-safe queue /array.

Does their implementation depend on the architecture?

Go Solutions


Solution 1 - Go

The source file for channels is (from your go source code root) in /src/pkg/runtime/chan.go.

hchan is the central data structure for a channel, with send and receive linked lists (holding a pointer to their goroutine and the data element) and a closed flag. There's a Lock embedded structure that is defined in runtime2.go and that serves as a mutex (futex) or semaphore depending on the OS. The locking implementation is in lock_futex.go (Linux/Dragonfly/Some BSD) or lock_sema.go (Windows/OSX/Plan9/Some BSD), based on the build tags.

Channel operations are all implemented in this chan.go file, so you can see the makechan, send and receive operations, as well as the select construct, close, len and cap built-ins.

For a great in-depth explanation on the inner workings of channels, you have to read Go channels on steroids by Dmitry Vyukov himself (Go core dev, goroutines, scheduler and channels among other things).

Solution 2 - Go

Here is a good talk that describes roughly how channels are implemented:
https://youtu.be/KBZlN0izeiY

Talk description:

> GopherCon 2017: Kavya Joshi - Understanding Channels > > Channels provide a simple mechanism for goroutines to communicate, and a powerful construct to build sophisticated concurrency patterns. We will delve into the inner workings of channels and channel operations, including how they're supported by the runtime scheduler and memory management systems.

Solution 3 - Go

You asked two questions:

  1. What kind of structure are they?

Channels in go are indeed "kind of like a thread-safe queue", to be more precise, channels in Go have the following properties:

  • goroutine-safe
  • Provide FIFO semantics
  • Can store and pass values between goroutines
  • Cause goroutines to block and unblock

Every time you create a channel, an hchan struct is allocated on the heap, and a pointer to the hchan memory location is returned represented as a channel, this is how go-routines can share it.

The first two properties described above are implemented similarly to a queue with a lock. The elements that the channel can pass to different go-routines are implemented as a circular queue (ring buffer) with indices in the hchan struct, the indices account for the position of elements in the buffer.

Circular queue:

qcount   uint           // total data in the queue
dataqsiz uint           // size of the circular queue
buf      unsafe.Pointer // points to an array of dataqsiz elements

And the indices:

sendx    uint   // send index
recvx    uint   // receive index

Every time a go-routine needs to access the channel structure and modify it's state it holds the lock, e.g: copy elements to/ from the buffer, update lists or an index. Some operations are optimized to be lock-free, but this is out of the scope for this answer.

The block and un-block property of go channels is achieved using two queues (linked lists) that hold the blocked go-routines

recvq    waitq  // list of recv waiters
sendq    waitq  // list of send waiters

Every time a go-routine wants to add a task to a full channel (buffer is full), or to take a task from an empty channel (buffer is empty), a pseudo go-routine sudog struct is allocated and the go-routine adds the sudog as a node to the send or receive waiters list accordingly. Then the go-routine updates the go runtime scheduler using special calls, which hints when they should be taken out of execution (gopark) or ready to run (goready). Notice this is a very simplified explanations that hides some complexities.

  1. Does their implementation depend on the architecture?

Besides the lock implementation that is OS specific as @mna already explained, I'm not aware of any architecture specific constraints optimizations or differences.

Solution 4 - Go

A simpler way to look at channels is as such, in that you may like to hold a program up while waiting for a condition to complete, typically used to prevent RACE condition, which means a thread might not finish before another, and then something your later thread or code depends on sometimes does not complete. An example could be, you have a thread to retrieve some data from a database or other server and place the data into a variable, slice or map, and for some reason it gets delayed. then you have a process that uses that variable, but since it hasn't been initialised, or its not got its data yet. the program fails. So a simple way to look at it in code is as follows: package main

import "fmt"

var doneA = make(chan bool)
var doneB = make(chan bool)
var doneC = make(chan bool)

func init() { // this runs when you program starts.
	go func() {
		doneA <- true  //Give donA true
	}()
}

func initB() { //blocking
	go func() {
		a := <- doneA  //will wait here until doneA is true
		// Do somthing here
		fmt.Print(a)
		doneB <- true //State you finished
	}()
}

func initC() {
	go func() {
		<-doneB // still blocking, but dont care about the value
		// some code here
		doneC <- true // Indicate finished this function
	}()
}

func main() {
	initB()
	initC()
}

So hope this helps. not the selected answer above, but i believe should help to remove the mystery. I wonder if I should make a question and self answer?

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionMattView Question on Stackoverflow
Solution 1 - GomnaView Answer on Stackoverflow
Solution 2 - GoaykeView Answer on Stackoverflow
Solution 3 - GoNetKatView Answer on Stackoverflow
Solution 4 - GoCyberienceView Answer on Stackoverflow