一文理解 Go 语言的协程:goroutine​

对Go语言的第一印象,一定是它从语言层面天生支持并发,非常方便,让开发者能快速写出高性能且易于理解的程序。

而在 Python (其他主流编程语言也类似)中,并发编程的门槛并不低,你要学习多进程,多线程,还要掌握各种支持并发的库 asyncio,aiohttp 等,同时你还要清楚它们之间的区别及优缺点,懂得在不同的场景选择不同的并发模式。

而 Golang  不需要你直面这些复杂的问题。在 Golang 里,你没得选,也不需要选,它原生提供的 goroutine (也即协程,一个 goroutine(协程) 本身就是一个函数已经足够优秀,能够自动帮你处理好所有的事情,而你要做的只是执行它,就这么简单。

当你直接调用 func( ) 函数时,它就是一个普通函数。如果在调用前加一个关键字 go ,那就开启了一个 goroutine(协程)。

// 执行一个函数
func()

// 开启一个协程执行这个函数
go func()

协程的初步使用

一个 Go 程序的入口通常是 main 函数,程序启动后,main 函数最先运行,我们称之为main goroutine

注意:在 main 中或者其下调用的代码中才可以使用 go + func() 的方法来启动协程。

main 的地位相当于主线程,当 main 函数执行完成后,这个线程也就终结了,其下的运行着的所有协程也不管代码是不是还在跑,也得乖乖退出。

举个栗子:

import "fmt"

func mytest() {
    fmt.Println("hello, go")
}

func main() {
    // 启动一个协程
    go mytest()
    fmt.Println("hello, world")
}

因此上面的这段代码运行完,只会输出  hello, world ,而不会输出hello, go(因为协程的创建需要时间,当 hello, world打印后,协程还没来得及并执行)。其实我们可以使用过time.Sleep() 来阻塞主程序使协程运行完全,但后续我们会学习更优雅的方式(Mutex)使其他协程能够有机会运行完全。

多个协程的效果

为了让你看到并发的效果,这里举个最简单的栗子:

import (
    "fmt"
    "time"
)

func mygo(name string) {
    for i := 0; i < 10; i++ {
        fmt.Printf("In goroutine %s\n", name)
        // 为了避免第一个协程执行过快,观察不到并发的效果,加个休眠
        time.Sleep(10 * time.Millisecond) 
    }
}

func main() {
    go mygo("协程1号") // 第一个协程
    go mygo("协程2号") // 第二个协程
    time.Sleep(time.Second)
}

输出如下:
In goroutine 协程2号
In goroutine 协程1号
In goroutine 协程1号
In goroutine 协程2号
In goroutine 协程2号
In goroutine 协程1号
In goroutine 协程1号
In goroutine 协程2号
In goroutine 协程1号
In goroutine 协程2号
In goroutine 协程1号
In goroutine 协程2号
In goroutine 协程1号
In goroutine 协程2号
In goroutine 协程1号
In goroutine 协程2号
In goroutine 协程1号
In goroutine 协程2号
In goroutine 协程1号
In goroutine 协程2号

可以观察到两个协程就如两个线程一样,并发执行,通过这些简单的栗子,已经体会到Go的这种强大的并发特性:实现异步,只要一个关键字 go 

以上只是介绍了协程的简单使用,真正的并发程序还是要结合信道(channel)来实现。

goroutine 作为 Go语言程序的并发执行的基本单元,而多个 goroutine 的通信需要依赖的是—— channel (通道或信道)。

信道的定义与使用

信道,就是一个管道,连接多个 goroutine 程序 ,它是一种队列式的数据结构,遵循先入先出的规则。

每个信道都只能传递一种数据类型的数据,所以在声明的时候得指定数据类型(string int 等等),声明后的信道,其零值是nil,无法直接使用,必须配合 make 函进行初始:

方式一:
var 信道实例 chan 信道类型
信道实例 = make(chan 信道类型)

或者
方式二:
信道实例 := make(chan 信道类型)

举个栗子,假如我要创建一个可以传输int类型的信道,可以这样子写。

// 定义信道
pipline := make(chan int)

信道的数据操作有两种:发送数据读取数据

// 往信道中发送数据
pipline<- 200

// 从信道中取出数据,并赋值给mydata
mydata := <-pipline

信道用完后可对其进行关闭,避免出现等待,并且对一个已关闭的信道再关闭,是会报错的。

close(pipline)

所以如何判断一个信道是否被关闭?

x, ok := <-pipline

当从信道中读取数据时,可以有多个返回值,其中第二个可以表示 信道是否被关闭:

  • ok 为 false,已经被关闭。
  • ok 为true。还没被关闭。

信道的容量与长度

一般创建信道都是使用 make 函数,make 函数接收两个参数

  • 第一个参数:必填,指定信道类型

  • 第二个参数:选填,不填默认为0,指定信道的容量(可缓存多少数据)

对于信道的容量,很重要,这里要多说几点(后面也有介绍和举例):

  • 当容量为0时,说明信道中不能存放数据,在发送数据时,必须要求立马有人接收,否则会报错。此时的信道称之为无缓冲信道

  • 当容量为1时,说明信道只能缓存一个数据,若信道中已有一个数据,此时再往里发送数据,会造成程序阻塞。 利用这点可以利用信道来做锁。

  • 当容量大于1时,信道中可以存放多个数据,可以用于多个协程之间的通信管道,共享资源。

至此我们知道,信道就是一个容器,具有长度和容量两个度量。若将它比做一个纸箱子:

  • 它可以装10本书,代表其容量为10,使用 cap 函数获取

  • 当前只装了1本书,代表其当前长度为1,使用 len 长度获取

package main

import "fmt"

func main() {
    pipline := make(chan int, 10)
    fmt.Printf("信道可缓冲 %d 个数据\n", cap(pipline))
    pipline<- 1
    fmt.Printf("信道中当前有 %d 个数据", len(pipline))
}

输出如下:
信道可缓冲 10 个数据
信道中当前有 1 个数据

缓冲信道与无缓冲信道

按照是否可缓冲数据可分为:缓冲信道(容量不为0) 与 无缓冲信道(容量为0)

缓冲信道

允许信道里存储一个或多个数据,这意味着,设置了缓冲区后,发送端和接收端可以处于异步的状态。

pipline := make(chan int, 10)

无缓冲信道

在信道里无法存储数据,这意味着,接收端必须先于发送端准备好,以确保你发送完数据后,有人立马接收数据,否则发送端就会造成阻塞,原因很简单,信道中无法存储数据。也就是说发送端和接收端是同步运行的。

pipline := make(chan int)

// 或者
pipline := make(chan int, 0)

双向信道与单向信道

通常情况下,我们定义的信道都是可发送也可接受数据的双向信道

但有时候,我们希望对信道的数据流向做一些控制,比如只能接收数据或者这个信道只能发送数据的单向信道

双向信道

默认情况下你定义的信道都是双向的,比如下面代码

import (
    "fmt"
    "time"
)

func main() {
    pipline := make(chan int)

    go func() {
        fmt.Println("准备发送数据: 100")
        pipline <- 100
    }()

    go func() {
        num := <-pipline
        fmt.Printf("接收到的数据是: %d", num)
    }()
    // 主函数sleep,使得上面两个goroutine有机会执行
    time.Sleep(1)
}

单向信道

单向信道,可以细分为 只读信道 和 只写信道

定义只读信道:

var pipline = make(chan int)
type Receiver = <-chan int // 关键代码:定义别名类型
var receiver Receiver = pipline

定义只写信道:

var pipline = make(chan int)
type Sender = chan<- int  // 关键代码:定义别名类型
var sender Sender = pipline

可见,区别在于 <- 符号在关键字 chan 的左边还是右边。

  • <-chan 表示这个信道,只能从里发出数据,对于程序来说就是只读

  • chan<- 表示这个信道,只能从外面收数据,对于程序来说就是只写

你可能会有疑问:为什么还要先声明一个双向信道,再定义单向通道呢?

因为信道本身就是为了传输数据而存在的,如果只有接收数据或者只有发送数据,那信道就变成了只入不出或者只出不入了吗,没什么用。所以只读信道和只写信道,唇亡齿寒,缺一不可。当然了,若你往一个只读信道中写入数据,或者从一个只写信道中读取数据,是必然都会出错的

完整的示例代码如下,供你参考:

import (
    "fmt"
    "time"
)
 //定义只写信道类型
type Sender = chan<- int  

//定义只读信道类型
type Receiver = <-chan int 

func main() {
    var pipline = make(chan int)

    go func() {
        var sender Sender = pipline
        fmt.Println("准备发送数据: 100")
        sender <- 100
    }()

    go func() {
        var receiver Receiver = pipline
        num := <-receiver
        fmt.Printf("接收到的数据是: %d", num)
    }()
    // 主函数sleep,使得上面两个goroutine有机会执行
    time.Sleep(1)
}

遍历信道

遍历信道,可以使用 for 搭配 range关键字,在range时,要确保信道最终是处于关闭状态,否则循环会阻塞。

import "fmt"

func fibonacci(mychan chan int) {
    n := cap(mychan)
    x, y := 1, 1
    for i := 0; i < n; i++ {
        mychan <- x
        x, y = y, x+y
    }
    // 记得 close 信道
    // 不然主函数中遍历完并不会结束,而是会阻塞。
    close(mychan)
}

func main() {
    pipline := make(chan int, 10)

    go fibonacci(pipline)
        // 遍历信道
    for k := range pipline {
        fmt.Println(k)
    }
}

用信道来做锁

当信道里的数据量已经达到设定的容量时,此时再往里发送数据会阻塞整个程序。利用这个特性,可以用当他来当程序的锁。

举个栗子(注释有解释):

package main

import {
    "fmt"
    "time"
}

// 由于 x=x+1 不是原子操作
// 所以应避免多个协程对x进行操作
// 使用容量为1的信道可以达到锁的效果
func increment(ch chan bool, x *int) {  
    ch <- true
    *x = *x + 1
    <- ch
}

func main() {
    // 注意要设置容量为 1 的缓冲信道
    pipline := make(chan bool, 1)

    var x int
    for i:=0;i<1000;i++{
        go increment(pipline, &x)
    }

    // 确保所有的协程都已完成
    // 以后会介绍一种更合适的方法(Mutex),这里暂时使用sleep
    time.Sleep(3)
    fmt.Println("x 的值:", x)
}

输出如下:
x 的值:1000

但是如果不加锁,输出会小于1000。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

薛定谔的猫96

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值