• go 并发


    java并发编程流程:

    1. 维护线程池
    2. 包装一个个任务
    3. 调度线程去执行任务,处理好线程上下文切换

    并发编程

    goroutine

    https://blog.csdn.net/xyc1211/article/details/125065193

    context包

    处理单个请求的多个Goroutine之间与请求域的数据、超时和退出等操作

    https://draveness.me/golang/docs/part3-runtime/ch06-concurrency/golang-context/

    • 方法
    type Context interface {
    	// 上下文截止时间
    	Deadline() (deadline time.Time, ok bool)
    	// Channel,在上下文的工作完成后关闭
    	Done() <-chan struct{}
    	// 上下文结束的原因
    	Err() error
    	// 上下文中的 K-V
    	Value(key interface{}) interface{}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    // 默认上下文, 最简单、最常用的上下文类型
    context.Background()
    
    // 取消信号: 衍生出一个新的子上下文 + 一个用于取消该上下文的函数
    ctx, cancel := context.WithCancel(context.Background())
    
    // 计时器上下文:衍生出一个过期时间为 1s 的上下文
    ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 使用案例
    func main() {
    	//当前函数没有上下文作为入参,我们都会使用 context.Background 作为起始的上下文向下传递。
        ctx, cancel := context.WithCancel(context.Background())
        go handle(ctx)
        
    	cancel() // 通知子goroutine结束
    }
    
    func handle(ctx context.Context) {
    	select {
    		case <-ctx.Done(): // 等待上级通知
    			fmt.Println("handle", ctx.Err())
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    Channel, select多路复用

    通道有发送(send)、接收(receive)和关闭(close)三种操作

    ch <- 10 // 把10发送到ch中
    
    x := <- ch // 从ch中接收值并赋值给变量x
    <-ch       // 从ch中接收值,忽略结果
    
    close(ch)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    对于从无缓冲Channel进行的接收,发生在对该Channel进行的发送完成之前(无缓冲Channel:接收完成,发送才能完成

    select 可以同时响应多个通道的操作。
    同时监听多个channel,直到其中一个channel ready

    高级用法

    goroutine池 worker pool

    控制goroutine数量,防止暴涨
    减少goroutine的创建/销毁的性能损耗

    没有官方包,根据需求自实现,参考: "gitee.com/johng/gf/g/os/grpool"包


    cpu调度: runtime包

    • runtime.Gosched()
      让出CPU时间片,重新等待安排任务

    • runtime.Goexit()
      退出当前协程

    • runtime.GOMAXPROCS()
      设置当前程序并发时占用的CPU逻辑核心数

    定时器: Timer包

    • Time = time.Now()
      输出当前时间

    • *Timer = time.NewTimer( time.Second)
      时间到了,输出1次时间

    timer1 := time.NewTimer(time.Second)
    fmt.Println("时间到:", <-timer1.C)
    
    • 1
    • 2
    • *Timer = time.NewTicker( time.Second)
      时间到了,输出多次时间
    ticker1 := time.NewTicker(1 * time.Second)
    // 一直在循环输出时间
    for {
    	fmt.Println(<-ticker.C)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • [*Timer].Stop()
      停止定时器
    • [*Timer].Reset(time.Second))
      重置定时器
    • time.Sleep(time.Second)

    sync包

    互斥锁 sync.Mutex

    var lock sync.Mutex
    
    func main() {
    	go add()
    	go add()
    }
    
    func add() {
    	lock.Lock() // 加锁, goroutine获取锁才能进入临界区
    	x = x + 1
    	lock.Unlock() // 解锁
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    读写互斥锁 sync.RWMutex

    分为 读锁和写锁, 读锁能多次获取,写锁需要等待
    适合读多写少的场景

    rwlock sync.RWMutex
    
    rwlock.Lock() 		// 加写锁
    rwlock.Unlock() 	// 解写锁
    
    rwlock.RLock()		// 加读锁
    rwlock.RUnlock()	// 解读锁
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    同步信号量 sync.WaitGroup

    (wg *WaitGroup) Add(delta int) 计数器+delta
    (wg *WaitGroup) Done() 计数器-1
    (wg *WaitGroup) Wait() 阻塞直到计数器变为0

    var wg sync.WaitGroup
    
    func hello() {
        defer wg.Done() //计数器-1
        fmt.Println("Hello Goroutine!")
    }
    func main() {
        wg.Add(1)	//计数器+1
        go hello() 
        
        wg.Wait()	//等计数器变为0 才执行
        fmt.Println("main goroutine done!")
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    func并发安全 sync.Once

    var loadIconsOnce sync.Once
    //func()只执行一次,并发安全
    loadIconsOnce.Do(func())
    
    • 1
    • 2
    • 3

    Map并发安全 sync.Map

    var m = sync.Map{}
    
    • 1

    原子操作:sync/atomic包

    性能比加锁操作更好

    GPM调度系统

    和java用操作系统来调度线程不同:线程由 CPU 调度是抢占式的

    go语言自己实现的一套调度系统,用来调度goroutine
    调度是在用户态下完成的,不需要内核态切换,内存分配,协程由用户态调度是协作式的,一个协程让出 CPU 后,才执行下一个协程。

    模型

    • G = goroutine
    • P = 一组goroutine队列 (默认个数=物理线程数) P管理着一组G挂载在M上运行:
    • M = 虚拟内核线程,一个groutine最终是要放到M上执行
      在这里插入图片描述

    M与内核线程一般是一一映射
    P与M一般也是一一对应

    协程与线程 关系是M:N
    在这里插入图片描述

    当一个G长久阻塞在一个M1上时,
    runtime会新建一个M2,阻塞G所在的P 会把其他的G挂载在新建的M2上。
    当旧的G阻塞完成或者认为其已经死掉时 回收旧的M1。

    • P原理
      数据结构:P里面会存储当前goroutine运行的上下文环境(函数指针,堆栈地址及地址边界),
      作用:对自己管理的goroutine队列做一些调度(比如把占用CPU时间较长的goroutine暂停、运行后续的goroutine等等)
      当自己的队列消费完了就去全局队列里取,如果全局队列里也消费完了会去其他P的队列里抢任务

    并发模式

    go 并发相比线程同步,更推荐用channel通信

    1. 线程同步

    同步通信:后台线程执行后 ,主线程再退出

    func main() {
        var mu sync.Mutex
    
        mu.Lock()
        go func(){
            fmt.Println("你好, 世界")
            mu.Unlock()
        }()
    
        mu.Lock()
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    扩展到N个线程完成后,再进行下一步

    func main() {
        var wg sync.WaitGroup
    
        // 开N个后台打印线程
        for i := 0; i < 10; i++ {
            wg.Add(1)
    
            go func() {
                fmt.Println("你好, 世界")
                wg.Done()
            }()
        }
    
        // 等待N个后台线程完成
        wg.Wait()
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    2. 消息传递

    同步通信:后台线程执行后 ,主线程再退出

    func main() {
        done := make(chan int, 1) // 带缓存的管道
    
        go func(){
            fmt.Println("你好, 世界")
            done <- 1
        }()
    
        <-done
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    扩展到N个线程完成后,再进行下一步

    func main() {
        done := make(chan int, 10) // 带 10 个缓存
    
        // 开N个后台打印线程
        for i := 0; i < cap(done); i++ {
            go func(){
                fmt.Println("你好, 世界")
                done <- 1
            }()
        }
    
        // 等待N个后台线程完成
        for i := 0; i < cap(done); i++ {
            <-done
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    生产者消费者

    Go语言实现生产者消费者并发很简单:

    1. 创建1个channel做成果队列
    2. 生产者 并发 发送数据到 channel
    3. 消费者 并发 从channel接收数据

    当成果队列中没有数据时,消费者就进入饥饿的等待中;
    而当成果队列中数据已满时,生产者则面临因产品挤压导致CPU被剥夺的下岗

    发布订阅

  • 相关阅读:
    事务隔离级别是怎么实现的?
    opencv c++图像轮廓计算
    【HarmonyOS开发】设备调试避坑指南
    华中某科技大学校园网疑似dns劫持的解决方法
    带头双向循环链表的实现
    机器学习之逻辑回归
    因果系列文章(9)——反事实(下)
    Ubuntu server 24 (Linux) 普通用户不能sudo 也不能使用root登录 忘记root密码 修复解决方案
    python3.5版本使用openpyxl报‘NoneType‘ object has no attribute ‘read‘问题
    MCSM面板搭建教程和我的世界Paper服务器开服教程
  • 原文地址:https://blog.csdn.net/xyc1211/article/details/125404208