GO语言并发

并发与并行

并发:同一时间段执行多个任务
并行:同一时刻执行多个任务
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

原文地址:https://www.cnblogs.com/aresxin/p/GO-yu-yan-bing-fa.html

时间: 2024-08-30 18:26:50

GO语言并发的相关文章

Go语言并发编程示例 分享(含有源代码)

GO语言并发示例分享: ppt http://files.cnblogs.com/files/yuhan-TB/GO%E8%AF%AD%E8%A8%80.pptx 代码, 实际就是<<Go语言程序设计>> (Mark Summerfield[英]著,许式伟 吕桂华 徐立 何李石 译) 第七章的全部示例: http://files.cnblogs.com/files/yuhan-TB/blog_go.tar.gz

Go语言并发与并行学习笔记(三)

目录(?) [-] Go语言并发的设计模式和应用场景 生成器 服务化 多路复合 select监听信道 结束标志 菊花链 随机数生成器 定时器 TODO Go语言并发的设计模式和应用场景 以下设计模式和应用场景来自Google IO上的关于Goroutine的PPT:https://talks.golang.org/2012/concurrency.slide 本文的示例代码在: https://github.com/hit9/Go-patterns-with-channel 生成器 在Pytho

Go语言开发(九)、Go语言并发编程

Go语言开发(九).Go语言并发编程 一.goroutine简介 1.并发与并行简介 并行(parallel):指在同一时刻,有多条指令在多个处理器上同时执行.并发(concurrency):指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行.并行在多处理器系统中存在,而并发可以在单处理器和多处理器系统中都存在,并发能够在单处理器系统中存在是因为并发是并行的假象,并行要

融云开发漫谈:你是否了解Go语言并发编程的第一要义?

2007年诞生的Go语言,凭借其近C的执行性能和近解析型语言的开发效率,以及近乎完美的编译速度,席卷全球.Go语言相关书籍也如雨后春笋般涌现,前不久,一本名为<Go语言并发之道>的书籍被翻译引进国内,并迅速引起广泛关注,本书由融云的一位研发工程师赵晨光联合翻译,旨在帮助Go语言学习者了解并发设计的模式和应用场景,更加深入理解Go语言特性,从而提升自身技术研发水平. “Go语言并发编程快速入门的第一要义” <Go语言并发之道>主要讲解了Golang语言的最佳实践和模式,引导读者如何选

go 语言 并发,并行,信道

go语言 并发 并行 信道 package main import ( "fmt" "time" ) 补充: //并发:看上去在同一时间同时执行,实际是切换执行利用时间片轮转法,同一个CPU进行切换执行 // 并行:是在真正的同一时间两个程序同时进行吗,这个是在多核cpu情况下,同一时刻,两个事情同时进行 //进程:进程是程序执行的最小单位,例如qq进程,迅雷进程, // 线程: 例如迅雷进程中,有上传,下载,上传就是一个线程,下载就是另一个线程 // 协程:当线程

Go语言之Go语言并发

Go 语言并发 Golang从语言层面就对并发提供了支持,而goruntine是Go语言并发设计的核心. Go语言的并发机制运用起来非常舒适,在启动并发的方式上直接添加了语言级的关键字就可以实现,和其他编程语言相比更加轻量. 进程&线程 A.进程是程序在操作系统中的一次执行过程,系统进行资源分配和调度的一个独立单位. B.线程是进程的一个执行实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位. C.一个进程可以创建和撤销多个线程:同一个进程中的多个线程之间可以并发执行. 并

Go 语言并发笔记

前言: 本文是学习<<go语言程序设计>> -- 清华大学出版社(王鹏 编著) 的2014年1月第一版 做的一些笔记 , 如有侵权, 请告知笔者, 将在24小时内删除, 转载请注明出处! 1. Goroutine - 定义: 在语言级别上支持的轻量级线程. - Go标准库提供的所有操作系统调用操作(包括同步I/O操作), 都会让出处理机给它. 所以它的切换和管理不依赖于系统的进程和线程 - 是go语言库的功能 , 而不是操作系统的功能. Goroutine不是用线程实现的. 本质是

由Go语言并发模型想到游戏服务器并发

这段时间看了一些Go语言相关的东西,发现Go语言的最大特性并发模型类似于C++里面的线程池,正好我们项目服务器也是用的线程池,记录下. Go语言的并发单位是语言内置的协程,使用关键字go+函数创建一个新的协程,新创建的协程会自动加入到协程调度上下文的等待调度队列,一个协程调度上下文对应一个线程,一个协程调度上下文对应多个协程.新加入的协程会动态负载到各个调度上下文,如果所有调度上下文的平均负载较高时,总调度器会自动创建新的线程和对应的调度上下文用于工作.整体上看,是N个线程:N个调度上下文:M个

Go语言并发编程总结

转自:http://blog.csdn.net/yue7603835/article/details/44309409 Golang :不要通过共享内存来通信,而应该通过通信来共享内存.这句风靡在Go社区的话,说的就是 goroutine中的 channel ....... 他在go并发编程中充当着 类型安全的管道作用. 1.通过golang中的 goroutine 与sync.Mutex进行 并发同步 import( "fmt" "sync" "runt

Go语言并发与并行学习笔记(一)

转:http://blog.csdn.net/kjfcpua/article/details/18265441 如果不是我对真正并行的线程的追求,就不会认识到Go有多么的迷人. Go语言从语言层面上就支持了并发,这与其他语言大不一样,不像以前我们要用Thread库 来新建线程,还要用线程安全的队列库来共享数据. 以下是我入门的学习笔记. 首先,并行!=并发, 两者是不同的,可以参考:http://concur.rspace.googlecode.com/hg/talk/concur.html G