go语言并发(代码片段)

aresxin aresxin     2022-12-13     145

关键词:

并发与并行

并发:同一时间段执行多个任务
并行:同一时刻执行多个任务
Go语言的并发通过goroutine实现。goroutine类似于线程,属于用户态的线程,我们可以根据需要创建成千上万个goroutine并发工作。goroutine是由Go语言的运行时调度完成,而线程是由操作系统调度完成。
Go语言还提供channel在多个goroutine间进行通信。goroutine和channel是 Go 语言秉承的 CSP(Communicating Sequential Process)并发模式的重要实现基础。

goroutine

goroutine 的概念类似于线程,但 goroutine 由 Go 程序运行时的调度和管理。Go 程序会智能地将 goroutine 中的任务合理地分配给每个 CPU。Go语言之所以被称为现代化的编程语言,就是因为它在语言层面已经内置了调度和上下文切换的机制。

使用goroutine

Go 程序中使用go关键字为一个函数创建一个goroutine。一个函数可以被创建多个 goroutine,一个goroutine必定对应一个函数。

启动单个goroutine

在调用的函数(普通函数和匿名函数)前面加上一个go关键字。

func hello() 
    fmt.Println("Hello ares!")

func main() 
    hello()
    fmt.Println("Hello BJ!")

#串行执行,先输出Hello ares!后输出Hello BJ!

在调用hello函数前面加上关键字go,也就是启动一个goroutine去执行hello这个函数。

func hello() 
    fmt.Println("Hello ares!")

func main() 
    go hello() // 启动另外一个goroutine去执行hello函数
    fmt.Println("Hello BJ!")

#只输出了Hello BJ!因为在程序启动时,Go程序就会为main()函数创建一个默认的goroutine。当main()函数返回的时候该goroutine就结束了,所有在main()函数中启动的goroutine会一同结束。

让main函数等待hello函数,可使用sleep

func hello() 
    fmt.Println("Hello ares!")

func main() 
    go hello()
    fmt.Println("Hello BJ!")
    time.Sleep(time.Second) 

#先输出Hello BJ!后输出Hello ares!因为在创建新的goroutine的时候需要花费一些时间,而此时mian函数所在的goroutine是继续执行的。

sync.WaitGroup

Go语言中可以使用sync.WaitGroup来实现并发任务的同步。
sync.WaitGroup是一个结构体,传递的时候要传递指针。
| 方法名 | 功能 |
| --- | --- |
| (wg * WaitGroup) Add(delta int) | 计数器+delta |
|(wg *WaitGroup) Done() |计数器-1|
|(wg *WaitGroup) Wait() |阻塞直到计数器变为0|
sync.WaitGroup内部维护着一个计数器,计数器的值可以增加和减少。例如当我们启动了N个并发任务时,就将计数器值增加N。每个任务完成时通过调用Done()方法将计数器减1。通过调用Wait()来等待并发任务执行完,当计数器值为0时,表示所有并发任务已经完成。

var wg sync.WaitGroup
func hello() 
    defer wg.Done()
    fmt.Println("Hello ares!")

func main() 
    wg.Add(1)
    go hello()
    fmt.Println("Hello BJ!")
    wg.Wait()

启动多个goroutine

var wg sync.WaitGroup
func hello(i int) 
    defer wg.Done()
    fmt.Println("Hello ares!",i)

func main() 
    for i:=0;i<10;i++
        wg.Add(1)
        go hello(i)
    
    wg.Wait()

每次打印的顺序不一样。这是因为10个goroutine是并发执行的,而goroutine的调度是随机的。

goroutine与线程

可增长的栈

OS线程(操作系统线程)一般都有固定的栈内存(通常为2MB),一个goroutine的栈在其生命周期开始时只有很小的栈(典型情况下2KB),goroutine的栈不是固定的,可以按需增大和缩小,goroutine的栈大小限制可以达到1GB,虽然极少会用到。所以在Go语言中一次创建十万左右的goroutine也是可以的。

goroutine调度

OS线程是由OS内核来调度的,goroutine则是由Go运行时(runtime)自己的调度器调度的,这个调度器使用一个称为m:n调度的技术(复用/调度m个goroutine到n个OS线程)。goroutine的调度不需要切换内核语境,所以调用一个goroutine比调度一个线程成本低很多。

GOMAXPROCS

Go运行时的调度器使用GOMAXPROCS参数来确定需要使用多少个OS线程来同时执行Go代码。默认值是机器上的CPU核心数。
Go语言中可以通过runtime.GOMAXPROCS()函数设置当前程序并发时占用的CPU逻辑核心数。
Go1.5版本之前,默认使用的是单核心执行。Go1.5版本之后,默认使用全部的CPU逻辑核心数。
将任务分配到不同的CPU逻辑核心上实现并行示例:

func a() 
    for i:=0;i<10;i++
        fmt.Println("A:",i)
    


func b()  
    for i:=0;i<10;i++
        fmt.Println("B:",i)
    


func main()  
    runtime.GOMAXPROCS(1)
    #runtime.GOMAXPROCS(2) 使用两个cpu,此时两个任务并行执行
    go a()
    go b()
    time.Sleep(time.Second)

Go语言中的操作系统线程和goroutine的关系:

  • 一个操作系统线程对应用户态多个goroutine。
  • go程序可以同时使用多个操作系统线程。
  • goroutine和OS线程是多对多的关系,即m:n。

channel

go语言的并发模型是CSP,提倡通过通信共享内存而不是通过共享内存而实现通信。
channel是可以让一个goroutine发送特定值到另一个goroutine的通信机制。
Go 语言中的通道(channel)是一种特殊的类型。通道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序。每一个通道都是一个具体类型的导管,也就是声明channel的时候需要为其指定元素类型。

声明channel

格式:

var 变量 chan 元素类型
元素类型可以是任意类型

创建channel

通道是引用类型,通道类型的空值是nil。

var ch chan int
fmt.Println(ch) // <nil>

声明的通道后需要使用make函数初始化之后才能使用。 创建channel的格式如下:

make(chan 元素类型, [缓冲大小])

channel操作

通道有发送(send)、接收(receive)和关闭(close)三种操作。
发送和接收都使用<-符号。

发送

ch <- 19 #将19发动到ch中

接受

x := <- ch // 从ch中接收值并赋值给变量x
<-ch       // 从ch中接收值,忽略结果

关闭

close(ch)

示例:

func main()  
    var ch1 chan int
    var ch2 chan string
    fmt.Println(ch1)    //nil
    fmt.Println(ch2)    //nil
    ch3 := make(chan int,5)
    ch3 <- 10
    ret := <- ch3
    fmt.Println(ch3)    //0xc000096000
    fmt.Println(ret)    //10
    close(ch3)
    fmt.Println(ch3)    //0xc000096000
    fmt.Println(ret)    //10

关于关闭通道需要注意的事情是,只有在通知接收方goroutine所有的数据都发送完毕的时候才需要关闭通道。通道是可以被垃圾回收机制回收的,它和关闭文件是不一样的,在结束操作之后关闭文件是必须要做的,但关闭通道不是必须的。

关闭后的通道有以下特点:

  • 对一个关闭的通道再发送值就会导致panic。
  • 对一个关闭的通道进行接收会一直获取值直到通道为空。
  • 对一个关闭的并且没有值的通道执行接收操作会得到对应类型的零值。
  • 关闭一个已经关闭的通道会导致panic。

无缓冲的通道

无缓冲的通道又称为阻塞的通道。无缓冲的通道必须有接收才能发送。
错误示范,无接收值的无缓冲通道:

func main() 
    ch := make(chan int )
    ch <- 19
    fmt.Println("succeed")

无接收值,可以编译,但无法执行,错误:fatal error: all goroutines are asleep - deadlock!代码会阻塞在ch <- 19这一行代码形成死锁。
可以使用goroutine去接收值来解决:

func recv(c chan int) 
    ret := <- c
    fmt.Println("succeed:",ret) //succeed: 19

func main() 
    ch := make(chan int )
    go recv(ch) // 启用goroutine从通道接收值
    ch <- 19
    fmt.Println("succeed")  //succeed

无缓冲通道上的发送操作会阻塞,直到另一个goroutine在该通道上执行接收操作,这时值才能发送成功,两个goroutine将继续执行。相反,如果接收操作先执行,接收方的goroutine将阻塞,直到另一个goroutine在该通道上发送一个值。
使用无缓冲通道进行通信将导致发送和接收的goroutine同步化。因此,无缓冲通道也被称为同步通道。

有缓冲通道

在使用make函数初始化通道的时候为其指定通道的容量,例如:

func main() 
    ch := make(chan int,10)
    ch <- 10
    fmt.Println("succeed:",ch)
    fmt.Println(len(ch),cap(ch))//1 10

只要通道的容量大于零,那么该通道就是有缓冲的通道,通道的容量表示通道中能存放元素的数量。
可以使用内置的len函数获取通道内元素的数量,使用cap函数获取通道的容量。

优雅的从通道循环取值

当通道被关闭时,往该通道发送值会引发panic,从该通道里接收的值一直都是类型零值。通常使用的是for range的方式判断一个通道是否被关闭:

func main() 
    ch1 := make(chan int)
    ch2 := make(chan int)
    // 开启goroutine将0~100的数发送到ch1中
    go func() 
        for i :=0;i < 100;i++
            ch1 <- i
        
        close(ch1)
    ()
    // 开启goroutine从ch1中接收值,并将该值的平方发送到ch2中
    go func() 
        for 
            i,ok := <- ch1
            if !ok 
                break
            
            ch2 <- i * i
        
        close(ch2)
    ()
    // 在主goroutine中从ch2中接收值打印
    for i:= range ch2
        fmt.Println(i)
    

单向通道

有的时候我们会将通道作为参数在多个任务函数间传递,很多时候我们在不同的任务函数中使用通道都会对其进行限制,比如只能发送或只能接收。Go语言中提供了单向通道来处理这种情况。

func counter(out chan<- int) 
    for i :=0;i<10;i++
        out <- i
    
    close(out)

func squarer(out chan <- int,in <- chan int)  
    for i := range in
        out <- i* i
    
    close(out)

func printer(in <- chan int)  
    for i := range in
        fmt.Println(i)
    

func main()  
    ch1 := make(chan int)
    ch2 := make(chan int)
    go counter(ch1)
    go squarer(ch2,ch1)
    printer(ch2)

chan<- int是一个只能发送的通道,可以发送但是不能接收;<-chan int是一个只能接收的通道,可以接收但是不能发送。在函数传参及任何赋值操作中将双向通道转换为单向通道是可以的,但反过来是不可以的。

select多路复用

在某些场景下我们需要同时从多个通道接收数据。通道在接收数据时,如果没有数据可以接收将会发生阻塞。
Go内置了select关键字,可以同时响应多个通道的操作。select的使用类似于switch语句,它有一些列case分支和一个默认的分支。每个case会对应一个通道的通信(接收或发送)过程。select会一直等待,直到某个case的通信操作完成时,就会执行case分支对应的语句。具体格式如下:

select
    case <-ch1:
        ...
    case data := <-ch2:
        ...
    case ch3<-data:
        ...
    default:
        默认操作

示例:

func main() 
//声明一个存放int类型,容量为10的通道
    ch := make(chan int,10)
    for i:=0;i<10;i++
        select 
        case x := <- ch:  //尝试从ch中接收值
            fmt.Println(x)
        case ch <- i: // 尝试向ch中发送数据
        
    

使用select语句能提高代码的可读性。如果多个case同时满足,select会随机选择一个。对于没有case的select会一直等待。

并发安全和锁

在Go代码中可能会存在多个goroutine同时操作一个资源(临界区),这种情况会发生竞态问题(数据竞态)。

var x int64
var wg sync.WaitGroup

func add()  
    for i:=0;i<10;i++
        x += 1
    
    wg.Done()

func main()  
    wg.Add(2)
    go add()
    go add()
    wg.Wait()
    fmt.Println(x)

上面的代码中开启了两个goroutine去累加变量x的值,这两个goroutine在访问和修改x变量的时候就会存在数据竞争,导致最后的结果与期待的不符。

互斥锁

互斥锁是一种常用的控制共享资源访问的方法,它能够保证同时只有一个goroutine可以访问共享资源。Go语言中使用sync包的Mutex类型来实现互斥锁。

var x int64
var wg sync.WaitGroup
var lock sync.Mutex

func add() 
    for i:=0;i<10;i++
        lock.Lock() //加锁
        x += 1
        lock.Unlock()   //解锁
    
    wg.Done()

func main() 
    wg.Add(2)
    go add()
    go add()
    wg.Wait()
    fmt.Println(x)

使用互斥锁能够保证同一时间有且只有一个goroutine进入临界区,其他的goroutine则在等待锁;当互斥锁释放后,等待的goroutine才可以获取锁进入临界区,多个goroutine同时等待一个锁时,唤醒的策略是随机的。

读写互斥锁

读写锁在Go语言中使用sync包中的RWMutex类型。
读写锁分为两种:读锁和写锁。当一个goroutine获取读锁之后,其他的goroutine如果是获取读锁会继续获得锁,如果是获取写锁就会等待;当一个goroutine获取写锁之后,其他的goroutine无论是获取读锁还是写锁都会等待。

var(
    x   int64
    wg  sync.WaitGroup
    lock    sync.Mutex
    rwlock sync.RWMutex
)

func write() 
    rwlock.Lock()   //加写锁
    x += 1
    time.Sleep(time.Millisecond * 10)   //操作耗时10ms
    rwlock.Unlock() //解锁
    wg.Done()


func read() 
    rwlock.RLock()  //加读锁
    time.Sleep(time.Millisecond)    //读操作耗时1ms
    rwlock.RUnlock()    //解锁
    wg.Done()


func main() 
    start := time.Now()
    for i:=0;i<10;i++
        wg.Add(1)
        go write()
    
    for i:=0;i<1000;i++
        wg.Add(1)
        go read()
    
    wg.Wait()
    end := time.Now()
    fmt.Println(end.Sub(start))

读写锁非常适合读多写少的场景!

sync.Once

sync.Map

09.go语言并发(代码片段)

Go语言并发并发指在同一时间内可以执行多个任务。并发编程含义比较广泛,包含多线程编程、多进程编程及分布式程序等。本章讲解的并发含义属于多线程编程。Go语言通过编译器运行时(runtime),从语言上支持了并发的特性... 查看详情

图解go并发(代码片段)

你很可能从某种途径听说过Go语言。它越来越受欢迎,并且有充分的理由可以证明。Go快速、简单,有强大的社区支持。学习这门语言最令人兴奋的一点是它的并发模型。Go的并发原语使创建多线程并发程序变得简单而有趣。我将... 查看详情

go语言并发,并行,信道(代码片段)

go语言  并发  并行  信道packagemainimport("fmt""time")补充://并发:看上去在同一时间同时执行,实际是切换执行利用时间片轮转法,同一个CPU进行切换执行//并行:是在真正的同一时间两个程序同时进行吗,这个是在多核cpu... 查看详情

go语言基础之并发(代码片段)

并发是编程里面一个非常重要的概念,Go语言在语言层面天生支持并发,这也是Go语言流行的一个很重要的原因。Go语言中的并发编程并发与并行并发:同一时间段内执行多个任务并行:同一时刻执行多个任务Go语言的并发通过goro... 查看详情

go语言基础之并发(代码片段)

Go语言中的并发编程——并发是编程里面一个非常重要的概念,Go语言在语言层面天生支持并发,这也是Go语言流行的一个很重要的原因。并发与并行并发:同一时间段内执行多个任务(你在用微信和两个女朋友聊天)。并... 查看详情

go语言实战并发模式(代码片段)

章节目录学习内容有:runner、pool、Go读写锁、以及总结。总结我习惯将其放在前面。总结稍后添加runnercommon.gopackagecommonimport("time""os""errors""os/signal")varErrTimeOut=errors.New("执行者执行超时")varErrInterrupt=errors.New("执行者被中断")//一个... 查看详情

go语言并发与通道的运用(代码片段)

在go语言中我们可以使用goroutine开启并发。goroutine是轻量级线程,goroutine的调度是由Golang运行时进行管理的。goroutine语法格式:go函数名(参数列表)实例1:packagemainimport("fmt""time")funcsay(sstring)fori:=0;i< 查看详情

19.go语言基础之并发(代码片段)

...执行多个任务(windows中360在杀毒,同时你也在写代码)Go语言的并发通过goroutine实现。goroutine类似于线程,属于用户态的线程,我们可以根据需要创建成千上万个goroutine并发工作。goroutine是由Go语言的运行时(runtime)调度完成,而... 查看详情

go原生并发原语和最佳实践(代码片段)

Go编程语言是用并发作为一等公民创建的。它是一种语言,通过抽象出语言中并发原语1背后的并行性细节,您可以轻松编写高度并行的程序。大多数语言都将并行化作为标准库的一部分,或者期望开发者生态系统提供并行化库。... 查看详情

go语言之并发(代码片段)

一:并发基础1并发和并行并发和并行是两个不同的概念:1并行意味着程序在任意时刻都是同时运行的:2并发意味着程序在单位时间内是同时运行的详解:  并行就是在任一粒度的时间内都具备同时执行的能力:最简单的并行... 查看详情

go语言并发编程(代码片段)

并发编程基本概念学习并发编程之前我们需要脑补几个基础知识和思考一个问题什么是串行?什么是并行?什么是并发?什么是程序?什么是进程?什么是线程?什么是协程?什么是串行?串行就是按顺序执行,就好比银行只有1个窗口,有3个... 查看详情

使用go语言实现高效的并发编程(代码片段)

...竞态条件使用信道来协调多个goroutine之间交互总结概述Go语言支持并发编程。你可以通过创建多个并发单元(称为goroutines)来实现多线程编程。每个goroutine都是一个独立的执行单元,可以并行执行代码。例如,如... 查看详情

使用go语言实现高效的并发编程(代码片段)

...竞态条件使用信道来协调多个goroutine之间交互总结概述Go语言支持并发编程。你可以通过创建多个并发单元(称为goroutines)来实现多线程编程。每个goroutine都是一个独立的执行单元,可以并行执行代码。例如,如... 查看详情

go语言并发编程(代码片段)

一、goroutine与传统的系统级线程和进程相比,协程的大优势在于其“轻量级”,可以轻松创建上百万个而不会导致系统资源衰竭,而线程和进程通常多也不能超过1万个。这也是协程也叫轻量级线程的原因。golang原生支持并发编... 查看详情

go语言并发编程-原子操作(代码片段)

...操作是原子的,即保证本例中n自增到最后的值为1000.示例Go语言代码:packagemainimp 查看详情

go语言并发编程-原子操作(代码片段)

...操作是原子的,即保证本例中n自增到最后的值为1000.示例Go语言代码:packagemainimp 查看详情

7.3go语言中通过waitgroup控制并发(代码片段)

...发,但是在开发习惯上与显示的表达不太相同,所以在Go语言中可以利用sync包中的WaitGroup实现并发控制,更加直观。基本使用示例我们将之前的示例加以改造,引入sync.WaitGroup来实现并发控制。首先我们在主函数中定义WaitGroupvarw... 查看详情

go并发编程模型(代码片段)

一、前言Go语言中实现了两种并发模型,一种是依赖于共享内存实现的线程-锁并发模型,另一种则是CSP(CommunicationingSequentialProcesses,通信顺序进程)并发模型。大多数编程语言(比如C++、Java、Python... 查看详情