go concurrent programming

1. demo with time.Sleep()

py java 多线程编程,多进程编程,问题:cost memory
web 2.0 用户级线程,绿程,轻量级线程,协程 java-netty
coroutine:内存占用小(2k),切换快
go only has -goroutine,convenient

主死随从

func asynPrint(){
    time.Sleep(time.Second)
    fmt.Println("bobby")
}
// main coroutine
func main(){
    go asynPrint()
    // 主死随从
    fmt.Println("main")
    time.Sleep(2*time.Second)
}

purpose: println for 10s

func main(){
    // 匿名函数启动goroutine
    go func(){
        for{
            time.Sleep(time.Second)
            fmt.Println("...")
        }
    }() 
    time.Sleep(10*time.Second)
}

2. multi-variable

simulate 100 coroutines:

for i:=0; i<100;i++{
    go func(){
        fmt.Println(i)
    }()
}

the problem is each coroutine share the same variable i
that may lead to 100 100 100 100

// solution 1
    for i := 0; i < 100; i++ {
        tmp := i
        go func() {
            fmt.Println(tmp)
        }()
    }
    time.Sleep(10 * time.Second)

// solution 2, elegantly
    for i := 0; i < 100; i++ {
        go func(i int) {
            fmt.Println(i)
        }(i) // pass by value, copy 
    }
    time.Sleep(10 * time.Second)

3. GMP scheduling

goroutine -> 调度器映射到 -> 固定的线程池

为了调度公平需要一把lock,影响性能

 最小化lock,g1 g2 有排队机制,一旦sleep for-loop,g1 idle,g2 active

g2需要进行操作系统级别调用如写入:g1 就找别的线程了

 在此过程中操作系统的线程不会频繁的切换,而是processor分配给线程

4. WaitGorup: 优雅的阻塞

func WgFunc(n int) {
    var wg sync.WaitGroup
    for i := 0; i < n; i++ {
        wg.Add(1) // wg +1
        go func(i int) {
            defer wg.Done() // wg -1
            fmt.Println(i)
        }(i)
    }
    wg.Wait() // wait until wg == 0
}

func main() {
    WgFunc(100)
}

5. Mutex & Atomic

 1. use lock to make "total += 1" atomic -> lock can also lock a piece of logic

import (
    "fmt"
    "sync"
)

var total int
var wg sync.WaitGroup
var lock sync.Mutex

func add() {
    defer wg.Done()
    for i := 0; i < 100000; i++ {
        lock.Lock()
        total += 1
        lock.Unlock()
    }
}
func sub() {
    defer wg.Done()
    for i := 0; i < 100000; i++ {
        lock.Lock()
        total -= 1
        lock.Unlock()
    }
}

func main() {
    wg.Add(2)
    add()
    sub()
    wg.Wait()
    fmt.Println(total)
}

2. use atomic.addInt32() -> higher performance 

var total int32
var wg sync.WaitGroup

//var lock sync.Mutex

func add() {
    defer wg.Done()
    for i := 0; i < 100000; i++ {
        atomic.AddInt32(&total, 1)
    }
}
func sub() {
    defer wg.Done()
    for i := 0; i < 100000; i++ {
        atomic.AddInt32(&total, -1)
    }
}

6. RWMutex

读读并发,读写串行,写写串行

写锁要向读锁请求 -> 写入用读锁(无法读了)-> 写锁释放读锁 -> 可以读了

var wg sync.WaitGroup
var rw sync.RWMutex

func main() {
    wg.Add(6) // 一共6个并发
    go func() { // 模拟写操作5s一次
        time.Sleep(5 * time.Second)
        defer wg.Done()
        rw.Lock()
        defer rw.Unlock()
        fmt.Println("writeeeeee")
        time.Sleep(5 * time.Second)
    }()
    for i := 0; i < 5; i++ { // 模拟5个并发的读,每个并发0.5s读一次
        go func() {
            defer wg.Done()
            for {
                rw.RLock()
                fmt.Println("readdddd")
                time.Sleep(500 * time.Millisecond)
                rw.RUnlock() // 无法用defer因为用于return前但这个是一个for-loop
            }
        }()
    }
    wg.Wait()
}

7. channel

1) demo:有缓存通道,发送到channel(会阻塞) 先于 从channel接受

    var msg chan string
    msg = make(chan string, 1)

    msg <- "123"
    data := <-msg
    fmt.Println(data)

when capacity is 0: 无缓存通道,从channel接受(会阻塞) 先于 发送到channel

func main() {
    var msg chan string
    msg = make(chan string, 0)

    msg <- "123" // block
    data := <-msg // cannot reach -> deadlock,解决:当前进程已阻塞,需要另起一个goroutine继续执行操作
    fmt.Println(data)

}

happen-before 参考:https://segmentfault.com/a/1190000039729417#item-3-5

https://golang.design/go-questions/channel/happens-before/

func main() {
    var msg chan string
    msg = make(chan string, 0)

    go func(msg chan string) {
        data := <-msg // 先执行,当前goroutine阻塞
        fmt.Println(data)
    }(msg)
    
    msg <- "123" // 后执行,receive后恢复goroutine
    time.Sleep(3 * time.Second)
}

no buffer channel:适用于通知
buffer channel:消费者与生产者
1. 消息传递,过滤
2. broadcast
3. subscription
4. task allocation
6. result summary
7. control concurrency
8. sync & async

 for range:

func main() {
    var msg chan string
    msg = make(chan string, 2)

    go func(msg chan string) {
        for data := range msg {
            fmt.Println(data)
        }
    }(msg)

    msg <- "1"
    msg <- "2"
    close(msg)
    //msg <- "3" // after closing the channel, cannot send data
    data := <-msg
    fmt.Println(data) // but can still receive data
    time.Sleep(3 * time.Second)
}
package main

import (
    "fmt"
)

func main() {
    ch := make(chan int, 3)

    // Sending data to the channel
    ch <- 1
    ch <- 2
    ch <- 3

    // Closing the channel
    close(ch)

    // Receiving data from the closed channel
    fmt.Println(<-ch) // Output: 1
    fmt.Println(<-ch) // Output: 2
    fmt.Println(<-ch) // Output: 3

    // Receiving from an empty closed channel
    fmt.Println(<-ch) // Output: 0 (zero value of int)

// Uncommenting the following line will cause a panic
    // ch <- 4
}

bidirectional channel can convert to unidirectional channel

msg := make(chan int)

var send chan<- int = msg

var read <- chan int = msg 

package main

import (
    "fmt"
    "sync"
)

func producer(send chan<- int) {
    defer wg.Done()
    for i := 0; i < 10; i++ {
        send <- i
    }
    close(send) // A sigal to the consumer 
}

func customer(read <-chan int) {
    defer wg.Done()
    for data := range read {
        fmt.Println(data)
    }
// If you don't close the send channel in the producer function,
// the consumer's range loop will continue to wait for more values to be sent on the channel.
// Since no more values will be sent and the channel is not closed, the consumer will block indefinitely, }
var wg sync.WaitGroup func main() { msg := make(chan int) wg.Add(2) go producer(msg) // 直接传bidirectional,可以自动转化 go customer(msg) wg.Wait() }

 application: interweavingly print number and letter

package main

import (
    "fmt"
    "time"
)

var letter, number = make(chan bool), make(chan bool)

func PrintNum() {
    i := 0
    for {
        <-letter // wait until letter send me the true to start
        fmt.Printf("%d%d", i, i+1)
        i = i + 2
        number <- true // notify number that should print the letter
    }
}
func PrintLetter() {
    i := 0
    str := "ASDFGHJKLxcvbn"
    for {
        <-number
        if i == len(str) {
            return
        }
        fmt.Print(str[i : i+2])
        i = i + 2
        letter <- true
    }
}

func main() {
    go PrintNum()    // <- letter,blocked
    go PrintLetter() // <- number,blocked
    letter <- true   // <- letter start to work
    time.Sleep(2 * time.Second)
}

使用mutex lock和全局变量实现:for循环不断monitor全局变量,效率低下

var done bool
var lock sync.Mutex

func g1() {
    time.Sleep(time.Second)
    lock.Lock()
    defer lock.Unlock()
    done = true
}
func g2() {
    time.Sleep(2 * time.Second)
    lock.Lock()
    defer lock.Unlock()
    done = true
}
func main() {
    go g1()
    go g2()
    for {
        if done {
            fmt.Println("done")
            break
        }
        time.Sleep(500 * time.Millisecond)
    }
}

 use a single channel to monitor

var done = make(chan struct{}) // channel is Thread safety, no need to mutex lock

func g1() {
    time.Sleep(time.Second)
    done <- struct{}{}
}

func g2() {
    time.Sleep(2 * time.Second)
    done <- struct{}{}
}

func main() {
    go g1()
    go g2()
    <-done
    fmt.Println("done")
}

use multi channels: select 

if two are ready simutaneously, the choice is random : to prevent hungry

var cn1 = make(chan struct{}) // channel is Thread safety, no need to mutex lock
var cn2 = make(chan struct{})

func g1() {
    time.Sleep(time.Second)
    cn1 <- struct{}{}
}

func g2() {
    time.Sleep(2 * time.Second)
    cn2 <- struct{}{}
}

func main() {
    go g1()
    go g2()

    // <- cn1 万一g1时间很长,一直阻塞,而需求是最短的时间
    // <- cn2

    select { // select which one is ready;
    case <-cn1:
        fmt.Println("1 done")
    case <-cn2:
        fmt.Println("2 done")
    }
}

有个问题:如果 cn1 和 cn2 都阻塞,main需要退出: timer

 timer := time.NewTimer(5 * time.Second) set a timer (has a channel) that will be ready in 5s

    timer := time.NewTimer(5 * time.Second)
    go g1()
    go g2()

    for {
        select { // select which one is ready;
        case <-cn1:
            fmt.Println("1 done")
        case <-cn2:
            fmt.Println("2 done")
        case <-timer.C: // when timer is ready: timeout
            fmt.Println("timeout")
            return
        }
    }

8. context

requirement: monitor the cpu and can stop autoly

var wg sync.WaitGroup

// sharing a variable, not recommended, need to add RWMutex
var stop bool

func cpuInfo() {
    defer wg.Done()
    for {
        if stop {
            return
        }
        time.Sleep(time.Second)
        fmt.Println("cpu INFO ...")
    }
}

func main() {
    wg.Add(1)
    go cpuInfo()
    time.Sleep(3 * time.Second)
    stop = true  // to make it quit autoly
    wg.Wait()
}

add RWmutex

// sharing a variable
var stop bool
var rw sync.RWMutex

func cpuInfo() {
    defer wg.Done()
    for {
        rw.RLock()
        if stop {
            return
        }
        rw.RUnlock()
        time.Sleep(time.Second)
        fmt.Println("cpu INFO ...")
    }
}

func main() {
    wg.Add(1)
    go cpuInfo()
    time.Sleep(3 * time.Second)
    rw.Lock()
    stop = true
    rw.Unlock()
    wg.Wait()
}

sharing a variable -> using channel 

using input param to transfer is elegant than using global one

var wg sync.WaitGroup

func cpuInfo(stopCn chan struct{}) {
    defer wg.Done()
    for {
        select {
        case <-stopCn:
            fmt.Println("stop ...")
            return
        default:
            time.Sleep(time.Second)
            fmt.Println("cpu INFO ...")
        }

    }
}

func main() {
    var stopCn = make(chan struct{})
    wg.Add(1)
    go cpuInfo(stopCn)
    time.Sleep(3 * time.Second)
    stopCn <- struct{}{}
    wg.Wait()
}

context makes it even more elegant

func cpuInfo(ctx context.Context) { // context is an interface, can be implemented, within other params
    defer wg.Done()
    for {
        select {
        case <-ctx.Done():
            fmt.Println("stop ...")
            return
        default:
            time.Sleep(time.Second)
            fmt.Println("cpu INFO ...")
        }

    }
}

func main() {
    wg.Add(1)
    ctx, cancel := context.WithCancel(context.Background())
    // WithCancel returns a copy of parent with a new Done channel
    go cpuInfo(ctx)
    time.Sleep(3 * time.Second)
    cancel()
    wg.Wait()
}

 contex的传递性: 调用ctx1的cancel() 会顺着继承关系分别调用儿子的cancel。全部会被调用到

 

func main() {
    wg.Add(1)
    ctx1, cancel1 := context.WithCancel(context.Background())
    ctx2, _ := context.WithCancel(ctx1)
    go cpuInfo(ctx2)
    time.Sleep(3 * time.Second)
    cancel1() // 父亲可以停掉儿子的零花钱
    wg.Wait()
}

 withvalue 方便链路追踪ID,打印日志来跟踪交易: fmt.Printf("traceid = %s\n", ctx.Value("traceId")) 

    wg.Add(1)
    //1 超时,手动关闭
    //ctx1, cancel1 := context.WithCancel(context.Background())
    //ctx2, _ := context.WithCancel(ctx1)
    //2 超时,自动关闭
    ctx, _ := context.WithTimeout(context.Background(), 6*time.Second)
    //3 withdeadline 在时间点关闭
    //4 withvalue
    valueCtx := context.WithValue(ctx, "traceId", "dfghtres134") // 依然是继承一个parentctx
    go cpuInfo(valueCtx)
    wg.Wait()

9. syn.Cond

https://ieevee.com/tech/2019/06/15/cond.html

多个Reader等待共享资源ready的场景

Wait()会自动释放c.L,并挂起调用者的goroutine。之后恢复执行,Wait()会在返回时对c.L加锁。

除非被Signal或者Broadcast唤醒,否则Wait()不会返回。

用者应该在循环中调用Wait

signal() 只能唤醒一个wait()

实现unlimited buffer channel:

package main
import (
"fmt"
"sync"
"time"
)
type InfChannel struct {
in chan int // 输入channel
out chan int // 输出channel
cond *sync.Cond
buffer []int // 缓存
bufLock sync.Mutex // 缓存的锁
closeFlag bool
}
func NewInfChannel(cap int) *InfChannel {
ch := &InfChannel{
in: make(chan int, cap),
out: make(chan int, cap),
cond: sync.NewCond(&sync.Mutex{}),
buffer: []int{},
bufLock: sync.Mutex{},
}
go ch.inWorker()
go ch.outWorker()
return ch
}
func (ch *InfChannel) In(data int) {
ch.in <- data
}
func (ch *InfChannel) Out() <-chan int {
return ch.out
}
func (ch *InfChannel) Close() {
close(ch.in)
ch.closeFlag = true
// 还需要触发一次cond避免其已经陷入wait无法获取close
ch.cond.Signal()
}
func (ch *InfChannel) inWorker() {
for data := range ch.in {
ch.bufLock.Lock()
ch.buffer = append(ch.buffer, data)
ch.bufLock.Unlock()
// 调用cond,说明buf中已经有数据
ch.cond.Signal()
}
}
func (ch *InfChannel) outWorker() {
for {
ch.cond.L.Lock()
// cond等待缓存非空,如果为空等待,如果为空,且发现通道以及关闭,那么关闭输出并且返回
for len(ch.buffer) == 0 {
if ch.closeFlag {
close(ch.out)
return
}
ch.cond.Wait()
}
ch.out <- ch.buffer[0]
ch.bufLock.Lock()
ch.buffer = ch.buffer[1:]
ch.bufLock.Unlock()
ch.cond.L.Unlock()
}
}

10. CAS

https://zhuanlan.zhihu.com/p/56733484

11. 

 

正在被执⾏的 goroutine 发⽣以下情况时让出当前 goroutine 的执⾏权,并调度后⾯的

 

goroutine 执⾏:

 

  •  IO 操作
  • Channel 阻塞
  •  system call
  • 运⾏较⻓时间: 调用   runtime.Gosched()  // yield execution to other goroutines

 

如果⼀个 goroutine 执⾏时间太⻓,scheduler 会在其 G 对象上打上⼀个标志(preempt),当这个 goroutine 内部发⽣函数调⽤的时候,会先主动检查这个标志,如
果为 true 则会让出执⾏权。main 函数⾥启动的 goroutine 其实是⼀个没有 IO 阻塞、没有 Channel 阻塞、没有system call、没有函数调⽤的死循环。
也就是,它⽆法主动让出⾃⼰的执⾏权,即使已经执⾏很⻓时间,scheduler 已经标志了 preempt。⽽ golang 的 GC 动作是需要所有正在运⾏ 
goroutine  都停⽌后进⾏的。因此,程序会卡在  runtime.GC()  等待所有协程退出。

12.

func (set *threadSafeSet) Iter() <-chan string {
    ch := make(chan string)
    go func() {
        set.mu.RLock()
        defer set.mu.RUnlock()
        for key := range set.s {
            ch <- key
        }
        close(ch)
    }()
    return ch
}

 

iterChannel := mySet.Iter()

ch会立即返回,返回后处于阻塞状态,因为no buffer + ch <- key

一旦我们 for-range,等于开始接受<- ch,goroutine就会继续运行

 

posted @ 2023-10-17 14:17  PEAR2020  阅读(16)  评论(0)    收藏  举报