0% found this document useful (0 votes)
55 views12 pages

Go Routines and Channels

The document discusses Go's concurrency model, which is based on Communicating Sequential Processes (CSP) and utilizes goroutines and channels for managing concurrent operations. It explains how goroutines are lightweight threads that allow for non-blocking execution, and introduces channels as a means for goroutines to communicate and synchronize. Additionally, it covers concepts such as buffered channels, channel direction, and the select statement for handling multiple channel operations.
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
55 views12 pages

Go Routines and Channels

The document discusses Go's concurrency model, which is based on Communicating Sequential Processes (CSP) and utilizes goroutines and channels for managing concurrent operations. It explains how goroutines are lightweight threads that allow for non-blocking execution, and introduces channels as a means for goroutines to communicate and synchronize. Additionally, it covers concepts such as buffered channels, channel direction, and the select statement for handling multiple channel operations.
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd

Go Routines and Channels

Concurrency is the computer science term for breaking up a single process into inde‐ pendent
components and specifying how these components safely share data. Most languages provide concurrency
via a library using operating system–level threads that share data by attempting to acquire locks. Go is
different. Its main concurrency model, arguably Go’s most famous feature, is based on Communicating
Sequential Processes (CSP).

Broadly speaking, all programs follow the same three-step process: they take data, transform it, and then
output the result. Whether you should use concurrency in your program depends on how data flows
through the steps in your program. Some‐ times two steps can be concurrent because the data from one is
not required for the other to proceed, and at other times two steps must happen in series because one
depends on the other’s output. Use concurrency when you want to combine data from multiple operations
that can operate independently.

Another important thing to note is that concurrency isn’t worth using if the process that’s running
concurrently doesn’t take a lot of time. Concurrency isn’t free; many common in-memory algorithms are
so fast that the overhead of passing values via concurrency overwhelms any potential time savings you’d
gain by running concur‐ rent code in parallel.

A goroutine as a lightweight thread, managed by the Go runtime system.

This program consists of two goroutines. The first goroutine is implicit and is the main function
itself. The second goroutine is created when we call go f(0). Normally when we invoke a

function our program will execute all the statements in a function and then return to the next line
following the invocation. With a goroutine we return immediately to the next line and don't wait
for the function to complete. This is why the call to the Scanln function has been included;

without it the program would exit before being given the opportunity to print all the numbers.

Goroutines are lightweight and we can easily create thousands of them.

1. program to run 10 goroutines

package main
import (
"fmt"
"math/rand"
"time"
)

func f(n int) {


for i := 0; i < 10; i++ {
fmt.Println(n, ":", i)
amt := time.Duration(rand.Intn(250))
time.Sleep(time.Millisecond * amt)
}
}

func main() {
for i := 0; i < 10; i++ {
go f(i)
}

var input string


fmt.Scanln(&input)
}

Output:

6:0
8:0
1:0
7:0
2:0
9:0
0:0
3:0
3:1
4:0
5:0
3:2
7:1
4:1
8:1
5:1
3:3
7:2
1:1
2:1
0:1
9:1
4:2
8:2
6:1
5:2
4:3
3:4
7:3
9:2
8:3
0:2
3:5
1:2
1:3
6:2
1:4
2:2
3:6
4:4
8:4
7:4
1:5
0:3
5:3
3:7
6:3
0:4
9:3
5:4
4:5
2:3
8:5
6:4
7:5
3:8
1:6
8:6
9:4
6:5
5:5
1:7
0:5
4:6
5:6
3:9
7:6
2:4
8:7
1:8
1:9
6:6
7:7
6:7
9:5
4:7
0:6
8:8
5:7
2:5
9:6
4:8
6:8
7:8
4:9
8:9
0:7
9:7
2:6
5:8
9:8
2:7
9:9
0:8
2:8
6:9
7:9
0:9
5:9
2:9

time.Duration is used to convert an int to type time.Duration


This is because time.Millisecond * amt involves multiplying a time.Duration (which
time.Millisecondis) by an int, and Go doesn’t allow this without explicit conversion.

Channels
Channels provide a way for two goroutines to communicate with one another and synchronize their
execution. Here is an example program using channels:
2. GO channels
package main

import (
"fmt"
"time"
)

func pinger(c chan string) {


for i := 0; ; i++ {
c <- "ping"
}
}

func printer(c chan string) {


for {
msg := <-c
fmt.Println(msg)
time.Sleep(time.Second * 1)
}
}

func main() {
var c chan string = make(chan string)
//fmt.Println(len(c), cap(c))

go pinger(c)
go printer(c)

var input string


fmt.Scanln(&input)

}
This program will print “ping” forever (hit enter to stop it). A channel type is represented with the
keyword chan followed by the type of the things that are passed on the channel (in this case we are
passing strings). The <- (left arrow) operator is used to send and receive messages on the channel. c
<- "ping" means send "ping". msg := <- c means receive a message and store it in msg. The

fmt line could also have been written like this: fmt.Println(<-c) in which case we could remove

the previous line.

Using a channel like this synchronizes the two goroutines. When pinger attempts to send a message
on the channel it will wait until printer is ready to receive the message. (this is known as blocking)
Let's add another sender to the program and see what happens. Add this function:

func ponger(c chan string) {


for i := 0; ; i++ {
c <- "pong"
}
}

The program will now take turns printing “ping” and “pong”.

3. Program with two senders

package main

import (
"fmt"
"time"
)

func pinger(c chan string) {


for i := 0; ; i++ {
c <- "ping"
}
}

func printer(c chan string) {


for {
msg := <-c
fmt.Println(msg)
time.Sleep(time.Second * 1)
}
}

func ponger(c chan string) {


for i := 0; ; i++ {
c <- "pong"
}
}

func main() {
var c chan string = make(chan string)

go pinger(c)
go ponger(c)
go printer(c)

var input string


fmt.Scanln(&input)
}

How the Go Scheduler Works:

1. Preemptive Scheduling:
○ Go's scheduler is preemptive, meaning it can interrupt
long-running goroutines to ensure that other goroutines
get a chance to run. This helps prevent a single goroutine
from monopolizing the CPU.
2. Work Stealing:
○ Go uses a work-stealing algorithm to balance the load across
"processors”. If one processor's queue of runnable goroutines becomes
empty, it can "steal" goroutines from the queue of another processor.
3. Goroutine Prioritization:
○ The scheduler may prioritize goroutines that are ready to run
(i.e., those that are not blocked on I/O or synchronization
operations). However, there’s no strict prioritization or fairness
guarantee.
4. System Calls and Blocking Operations:
○ When a goroutine makes a system call or performs a blocking
operation (e.g., waiting on a channel, sleeping, etc.), the scheduler
can put that goroutine to sleep and switch to another one that’s ready
to run.
5. Scheduling Policies:
○ While Go’s scheduling is not purely round-robin, it does attempt
to give each runnable goroutine a fair share of CPU time. However,
this fairness is balanced with efficiency, and the scheduler may favor
certain goroutines based on context (e.g., if a goroutine is quickly
yielding or completing its work).

Channel Direction

We can specify a direction on a channel type thus restricting it to either sending or receiving. For
example pinger's function signature can be changed to this:

func pinger(c chan<- string)

Now c can only be sent to. Attempting to receive from c will result in a compiler error. Similarly we
can change printer to this:

func printer(c <-chan string)

A channel that doesn't have these restrictions is known as bi-directional. A bi-directional channel can
be passed to a function that takes send-only or receive-only channels, but the reverse is not true.

4. Using Select
Go has a special statement called select which works like a switch but for channels:
package main

import (
"fmt"
"time"
)

func main() {
c1 := make(chan string)
c2 := make(chan string)

go func() {
for {
c1 <- "from 1"
time.Sleep(time.Second * 2)
}
}()

go func() {
for {
c2 <- "from 2"
time.Sleep(time.Second * 3)
}
}()

go func() {
for {
select {
case msg1 := <-c1:
fmt.Println(msg1)
case msg2 := <-c2:
fmt.Println(msg2)
}
}
}()

var input string


fmt.Scanln(&input)
}

Output:
from 1
from 2
from 1
from 2
from 1
from 2
from 1
from 1
from 2
from 1
from 2
from 1
from 1
from 2
from 1
from 2
from 1
from 1
from 2

Buffered Channels

It's also possible to pass a second parameter to the make function when creating a channel:

c := make(chan int, 1)

This creates a buffered channel with a capacity of 1. Normally channels are synchronous; both sides
of the channel will wait until the other side is ready. A buffered channel is asynchronous; sending or
receiving a message will not wait unless the channel is already full.

5. Buffered Channels
package main

import (
"fmt"
"time"
)

func main() {
// Create a buffered channel with a capacity of 2
c := make(chan int, 2)
// Start a goroutine that sends data into the channel
go func() {
for i := 1; i <= 5; i++ {
//fmt.Printf("Sending %d\n", i)
c <- i
fmt.Printf("Sent %d\n", i)
time.Sleep(time.Second) // Simulate some delay
}
}()

// Start a goroutine that receives data from the channel


go func() {
for i := 1; i <= 5; i++ {
msg := <-c
fmt.Printf("Received %d\n", msg)
time.Sleep(4 * time.Second) // Simulate longer
processing delay
}
}()

// Give goroutines time to complete


time.Sleep(20 * time.Second)
}

Output:

Sent 1
Received 1
Sent 2
Sent 3
Sent 4
Received 2
Received 3
Sent 5
Received 4
Received 5

6. Range and Close

package main

import (
"fmt"
)
func fibonacci(n int, c chan int) {
x, y := 0, 1
for i := 0; i < n; i++ {
c <- x
x, y = y, x+y
}
close(c)
}

func main() {
c := make(chan int, 10)
go fibonacci(cap(c), c)
for i := range c {
fmt.Println(i)
}
}

You might also like