A goroutine is a lightweight thread in Go.
Goroutine
We usually use goroutine in those cases.
- When one task can be split into different segments to perform better.
- When making multiple requests to different API endpoints.
- Any work that can utilize multi-core CPUs should be well optimized using goroutines
- Running background operations in a program might be a use case for a goroutine.
We simply add a keyword go in front of the function for creating a goroutine.
There is one thing to keep in mind.
goroutine cannot have a return value.
func printA() {
for i := 0; i < 10; i++ {
fmt.Println("A", i)
}
}
func printB() {
for i := 0; i < 10; i++ {
fmt.Println("B", i)
}
}
func main() {
go printA()
go printB()
}
But, it will not work.
This is because when the main function is terminated, the goroutine is terminated immediately, even if it is running.
Therefore, it should be allowed to wait for the execution of the goroutine.
func printA() {
for i := 0; i < 50; i++ {
fmt.Println("A", i)
}
}
func printB() {
for i := 0; i < 50; i++ {
fmt.Println("B", i)
}
}
func main() {
go printA()
go printB()
time.Sleep(10 * time.Millisecond)
}
// A 0
// A 1
// A 2
// ...
// B 47
// B 48
// B 49
It works now, but it is ungraceful.
Now, it's time to learn about the sync.WaitGroup.
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
func printA() {
for i := 0; i < 50; i++ {
fmt.Println("A", i)
}
wg.Done()
}
func printB() {
for i := 0; i < 50; i++ {
fmt.Println("B", i)
}
wg.Done()
}
func main() {
wg.Add(2) // set number of jobs
go printA()
go printB()
wg.Wait() // wait until all jobs are done
}
sync.WaitGroup helps goroutine.
It has three methods.
- Add() method sets the number of jobs.
- Done() method is called when a job is done.
- Wait() method waits all set jobs are done.
The Pros of Goroutine
When we use goroutine, the CPU core and OS thread aren't changed. In other words, there is no context switching.
In goroutine, only goroutine itself changes.
Therefore, there are benefits in performance.
Precautions for Goroutine
A concurrency problem occurs when multiple goroutines simultaneously access the same memory resource.
We can use mutex(MUTual EXclusion) in this case.
var mutex sync.Mutex
func testFunc(num *int) {
mutex.Lock()
defer mutex.Unlock()
*num += 1000
time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond)
*num -= 1000
}
We just simply call Lock() method before accessing the shared resources and call Unlock() method after using the shared resources.
Mutex can resolve the concurrency problem.
However, we lose the benefit of concurrency programming because only the goroutine that obtained mutex is executed.
And, it may occur a deadlock problem.
So, you need to pay attention to design architecture when you use concurrency programming.
Channel
Channel is a medium that the goroutine use in order to communicate effectively.
Channel is also a message queue between goroutines.
Let's learn how to make channel first.
// Method 1
var channel chan int
channel = make(chan int)
// Method 2
channel := make(chan int)
Channel that is not initialized or is zero-value is nil.
To send and receive data using a channel we should use the channel operator which is <-.
// Send data to channel
ch <- 7
// Receive data from channel
data := <-ch
Noe, let's use a channel.
func usingChannel(ch chan int, value int) {
ch <- value
}
func main() {
ch := make(chan int)
go usingChannel(ch, 7)
fmt.Println(<-ch)
// 7
}
You can use any data type with channel even structure.
Channel can be unidirectional.
func sendOnlyChannel(ch chan<- int, value int) {
ch <- value
}
func main() {
ch := make(chan<- int)
go sendOnlyChannel(ch, 7)
}
You can close an open channel.
func closeChannel(ch chan string, str string) {
ch <- str
close(ch)
}
func main() {
ch := make(chan string)
go closeChannel(ch, "Hello Channel")
value, ok := <-ch
if !ok {
fmt.Println("Channel closed")
}
fmt.Println(value)
// Hello Channel
}
A range loop can be used to iterate over all the values sent through the channel.
func loopChannel(ch chan int) {
ch <- 1
ch <- 2
ch <- 3
ch <- 4
ch <- 5
close(ch)
}
func main() {
ch := make(chan int)
go loopChannel(ch)
for v := range ch {
fmt.Println(v)
}
// 1
// 2
// 3
// 4
// 5
}
The channel should be closed after use it.
All the channels we've looked at so far were unbuffered channels.
We can make a buffered channel as well.
make(chan int, 5)
Simply specify the size of the buffer.
Others, such as how to use them, are all the same.
Select
Select can wait from multiple channels.
func testFunc(wg *sync.WaitGroup, num chan int, quit chan bool) {
for {
select {
case n := <-num:
fmt.Printf("Num: %d\n", n)
time.Sleep(time.Second)
case <-quit:
wg.Done()
return
}
}
}
func main() {
var wg sync.WaitGroup
num := make(chan int)
quit := make(chan bool)
wg.Add(1)
go testFunc(&wg, num, quit)
for i := 0; i < 4; i++ {
num <- i
}
quit <- true
wg.Wait()
}
In the above example, select statement can receive data from num channel and quit channel.
Context
Context is a kind of job specification.
We can use the context to direct the task for a specific time or cancel it externally.
Context With Cancel
var wg sync.WaitGroup
func testFunc(ctx context.Context) {
tick := time.Tick(time.Second)
for {
select {
case <-ctx.Done():
wg.Done()
return
case <-tick:
fmt.Println("1s Tick")
}
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
wg.Add(1)
go testFunc(ctx)
time.Sleep(5 * time.Second)
cancel()
wg.Wait()
}
If we use WithCancel() method, it returns a cancel() function .
The cancel() function can cancel a job that we want to stop.
Context with Timeout
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
If we change the WithCancel() method to WithTimeout() method, the job will be stopped after the set time.
Context with WithValue
var wg sync.WaitGroup
func testFunc(ctx context.Context) {
if v := ctx.Value("number"); v != nil {
n := v.(int)
fmt.Println(n)
}
wg.Done()
}
func main() {
ctx := context.WithValue(context.Background(), "number", 7)
wg.Add(1)
go testFunc(ctx)
wg.Wait()
}
If we use WithValue() method, we can order workers to do the job with a pair of a key and a value.
Nested Context
Context can be nested.
ctx, cancel := context.WithCancel(context.Background())
ctx = context.WithValue(ctx, "number", 7)
'Go' 카테고리의 다른 글
[Go] Testing (0) | 2021.10.26 |
---|---|
[Go] Error Handling (0) | 2021.10.22 |
[Go] Map (0) | 2021.10.21 |
[Go] Linked List (0) | 2021.10.20 |
[Go] Functions (0) | 2021.10.19 |
댓글