• Go 并发编程


    并发编程

    1.1 并发与并⾏
    在这里插入图片描述
    并⾏与并发是两个不同的概念,普通解释:

    • 并发:交替做不同事情的能⼒
    • 并⾏:同时做不同事情的能⼒

    如果站在程序员的⻆度去解释是这样的:

    • 并发:不同的代码块交替执⾏
    • 并⾏:不同的代码块同时执⾏

    并发和并⾏都是为了提⾼机器硬件利⽤率,提升应⽤运⾏效率。并⾏和并发就是为达⽬的的两个⼿段。
    并⾏可以提升CPU核⼼数,并发则是通过系统设计,⽐如分时复⽤系统,多进程,多线程的开发⽅法,
    不过在对并发的⽀持上,Go语⾔是天⽣最好的,因为它设计的理念就是并发,⽽且是在语⾔层⾯实现的
    并发。

    1.2 Goroutine
    如果对线程和进程有所了解的话,我们可以这样给进程和线程下⼀个专业点的定义。

    • 线程是最⼩的执⾏单位
    • 进程是最⼩的资源申请单位

    在Go语⾔当中,有⼀个存在是⽐线程还要⼩的执⾏单位,那就是Goroutine,翻译上习惯叫例程或协
    程,不过我们遵循原汁原味还是直接⽤Goroutine来称呼它。Go语⾔开发者为了实现并⽀持
    Goroutine,特意花了⼤⼒⽓来开发⼀个语⾔层⾯的调度算法。
    具体调度算法详情介绍可以查看英⽂原⽂:调度算法链接

    简单理解的话,⾸先要明确Go语⾔调度算法中提到的三个字⺟,M,P,G,分别代表线程,上下⽂以
    及Goroutine。
    在这里插入图片描述
    在操作系统层⾯线程仍然是最⼩的执⾏单位,在进程调度的时候,CPU需要在不同进程间切换,此时需
    要保留运⾏的上下⽂信息,也就是前⾯提到的P,⾄于G则是代表了Go语⾔当中的Goroutine。

    在这里插入图片描述
    每个线程M有⼀个⾃⼰的上下⽂P,同⼀时刻,每个线程内部可以执⾏⼀个Groutine代码,如上图所
    示,每个线程有⼀个⾃⼰的调度队列,这个队列⾥存放的就是若⼲个Goroutine。

    在这里插入图片描述
    当线程发⽣系统调⽤时,该线程会被阻塞,此时为了提⾼运⾏效率,Goroutine调度算法会将该阻塞的
    线程Goroutine队列转移到其他线程上。当线程执⾏完系统调⽤后,⼜会从其他线程的队列中“借”⼀些
    Goroutine过来。

    1.3 Goroutine启动
    在⼀台主机上,线程启动的数量是有上限的,这个上限并⾮可以启动的上限,⽽是不影响系统性能的上
    限。Goroutine在并发上则没有这⽅⾯的顾虑,可以随⼼所欲的启动,不⽤担⼼数量的问题,当然这归
    功于Go语⾔的调度算法。
    那么Goroutine如何启动呢?其实在我们之前写的代码中,都存在⼀个Goroutine,就是我们的主函
    数,我们习惯叫他main-goroutine。如果我们想再启动⼀个Goroutine,也⾮常容易,直接关键字go再
    加上函数调⽤就⾏了。

    go call_func()
    
    • 1
    package main
    
    import "fmt"
    
    func main() {
    	fmt.Println("begin call goroutine")
    	//启动goroutine
    	go func() {
    		fmt.Println("I am a goroutine!")
    	}()
    	fmt.Println("end call goroutine")
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    执⾏这个代码,我们⼤概率是看不到“I am a goroutine!”这句话的,因为go func()这⾥创建的Goroutine
    或者尚未创建成功时,main-goroutine已经结束执⾏了,main-goroutine结束执⾏,也就代表着进程
    退出,那么不会有任何代码被执⾏了!那么⼤家考虑⼀下,如何解决这个问题呢?

    1.5 Go语⾔运⾏时
    所谓运⾏时,就是运⾏的时刻,Go语⾔的运⾏时就是描述与进程运⾏相关的信息,我们可以使⽤
    runtime包来显示⼀些运⾏时的信息。

    1.5.1 GOMAXPROCS

    func GOMAXPROCS(n int) int
    //当n<=1时,查看当前进程可以并⾏的goroutine最⼤数量(CPU核⼼数)
    //当n>1时,代表设置可并⾏的最⼤goroutine数量
    
    • 1
    • 2
    • 3

    这个函数可以帮我们查看或设置当前进程的最⼤CPU核⼼数。

    2. 同步
    同步在不同的语境代表不同的含义,在数据库中是指数据的同步,在分布式系统中是指系统内的数据⼀
    致,⽽在语⾔层⾯的同步是指运⾏时步调⼀致,避免竞争,有先有后。

    2.1 如何做到同步
    为了提⾼CPU的使⽤效率,我们需要启动多个Goroutine,⽽多个Goroutine⽐线程的颗粒度还⼩,他
    们之间必然存在争抢同⼀资源的现象,就像我们在线程中要控制同步⼀样,多个Goroutine在访问同⼀
    共享资源时,我们仍然要控制同步。

    如何做到最直接的同步,可以借鉴我们⽣活中的例⼦,在⽕⻋上我们去卫⽣间的时候,都会把⻔锁上,
    这样别⼈就没法进来了。在这个例⼦中,我们和其他⼈就是Goroutine,⽽卫⽣间就是那个共享资源,
    我们不允许发⽣⼤家⼀起进⼊使⽤的情况,⽽解决这个问题的关键就是锁!
    那么Go语⾔给我们提供了哪些同步机制呢?主要有如下⽅式:

    • WaitGroup 计数等待法
    • Once 执⾏⼀次
    • Mutex 互斥锁
    • RWMutex 读写锁
    • Cond 条件变量
    • channel 通道,Go语⾔的最⼤特性

    在上述⽅法中,除了channel,其余的同步⽅式都在Go语⾔的sync包当中。

    2.2 WaitGroup
    Go语⾔的同步⽅式很多,WaitGroup的实现⽅式很巧妙,主要只有3个API。

    //增加计数
    func (wg *WaitGroup) Add(delta int)
    //减少计数
    func (wg *WaitGroup) Done()
    //阻塞等待计数变为0
    func (wg *WaitGroup) Wait()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    它的核⼼思想是当启动⼀个Goroutine时,使⽤Add添加⼀个计数器,⽽Wait的功能是阻塞等待计数器
    归0,Done的作⽤则是Goroutine运⾏结束后执⾏此句话清掉计数器的⼀个计数。
    利⽤WaitGroup的特性,我们可以优雅的实现⼀个例⼦:启动10个Goroutine,让他们顺序退出,
    main-goroutine等待所有Goroutine退出后才可退出。

    package main
    
    import (
    	"fmt"
    	"sync"
    	"time"
    )
    
    var w sync.WaitGroup
    
    func main() {
    	for i := 0; i < 10; i++ {
    		w.Add(1) //添加⼀个要监控的Goroutine数量
    		go func(num int) {
    			time.Sleep(time.Second * time.Duration(num))
    			fmt.Printf("I am %d Goroutine\n", num)
    			w.Done() //释放⼀个
    		}(i)
    	}
    	w.Wait() //阻塞等待
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    I am 0 Goroutine
    I am 1 Goroutine
    I am 2 Goroutine
    I am 3 Goroutine
    I am 4 Goroutine
    I am 5 Goroutine
    I am 6 Goroutine
    I am 7 Goroutine
    I am 8 Goroutine
    I am 9 Goroutine
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    2.3 Mutex互斥锁
    提到互斥锁,我们通常会提到⼀个临界区的概念,Goroutine在准备访问共享数据时,我们就认为它进
    ⼊了临界区。
    使⽤互斥锁的核⼼思想就是进⼊临界区之前先要申请锁,申请到锁的Goroutine继续执⾏,⽽没有申请
    到的Goroutine则阻塞等待别⼈释放这个mutex,这样就可以有效的控制Goroutine之间竞争的问题。
    下述代码就是⼀个存在数据修改竞争的例⼦,循环1000次对⼀个数据⾃增,⽬标的输出结果是1000,
    但执⾏下⾯的代码很难获得1000。

    package main
    
    import (
    	"fmt"
    	"sync"
    )
    
    var x = 0
    
    func increment(wg *sync.WaitGroup) {
    	x = x + 1
    	wg.Done()
    }
    func main() {
    	var w sync.WaitGroup
    	for i := 0; i < 1000; i++ {
    		w.Add(1)
    		go increment(&w)
    	}
    	w.Wait()
    	fmt.Println("final value of x", x)
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    final value of x 974
    
    • 1

    上述代码如果在进⼊临界区前使⽤mutex,就可以很好的解决该问题。代码主要修改increment函数即
    可:

    func increment(wg *sync.WaitGroup) {
     mutex.Lock() //上锁
     x = x + 1 //临界区
     mutex.Unlock() //释放锁
     wg.Done()
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    修改后再执⾏代码,就可以很好的看到效果。

    final value of x 1000
    
    • 1

    2.4 RWMutex

    RWMutex我们可以称其为读写锁,它算是mutex的改进版,因为mutex的特点是排他性,只要有⼀个
    上锁成功了,其余⼈都不可以使⽤。但在实际开发过程中,经常会出现多个Goroutine去访问相同的共
    享资源,只不过这些Goroutine中有些是读数据,有些是写数据。开发者肯定明⽩,读数据不会对数据
    造成影响,这样理论上来说,⼀个读的Goroutine上锁了,其余的读Goroutine理应也可以访问,这样
    就出现了读写锁。对于读写锁来说,关键是掌握它的原则:

    • 读共享
    • 写独占
    • 写优先级⾼
    package main
    
    import (
    	"fmt"
    	"sync"
    	"time"
    )
    
    var rwlock sync.RWMutex
    var wg sync.WaitGroup
    var x = 0
    
    func go_reader(num int) {
    	for {
    		rwlock.RLock()
    		fmt.Printf("I am %d reader goroutine x = %d\n", num, x)
    		time.Sleep(time.Millisecond * 2)
    		rwlock.RUnlock()
    	}
    	wg.Done()
    }
    func go_writer(num int) {
    	for {
    		rwlock.Lock()
    		x += 1
    		fmt.Printf("I am %d writer goroutine x = %d\n", num, x)
    		time.Sleep(time.Millisecond * 2)
    		rwlock.Unlock()
    	}
    	wg.Done()
    }
    func main() {
    	wg.Add(10)
    	for i := 0; i < 7; i++ {
    		go go_reader(i)
    	}
    	for i := 0; i < 3; i++ {
    		go go_writer(i)
    	}
    	wg.Wait()
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42

    2.5 Once
    在很多时候,我们会有⼀个需求,那就是虽然多个Goroutine都要运⾏⼀段代码,但我们却希望这段代
    码只能被⼀个Goroutine运⾏,也就是说只被允许运⾏⼀次。在Go语⾔当中,就给我们提供了这样的机
    制 – Once。

    package main
    
    import (
    	"fmt"
    	"sync"
    )
    
    func main() {
    	var count int
    	var once sync.Once
    	var wg sync.WaitGroup
    	for i := 0; i < 100; i++ {
    		wg.Add(1)
    		go func() {
    			once.Do(func() {
    				count += 1 //确保只执⾏⼀次
    			})
    			wg.Done()
    		}()
    	}
    	wg.Wait()
    	fmt.Printf("Count is %d\n", count)
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    Count is 1
    
    
    • 1
    • 2
  • 相关阅读:
    数据分析是大数据最热门的投资赛道
    Yarn调度器
    全屏Activity弹出键盘不顶起布局
    不知道PPT转PDF简单方法有哪些?三个方法让你知道PPT转PDF怎么转
    更强悍 更智能!飞凌嵌入式FET3588-C核心板震撼发布!
    Win10系统无法安装geforce game ready driver?
    vs code 工具HTML、css、javaScript、Vue、等代码插件安装
    Unrecognized SSL message, plaintext connection?
    计算机毕业设计Java小动物领养网站(源码+系统+mysql数据库+Lw文档)
    【配置】如何在打包Spring Boot项目时按需使用日常、测试、预发、正式环境的配置文件
  • 原文地址:https://blog.csdn.net/weixin_47906106/article/details/133705523