【Go并发基础】

什么是并发

​ 代码按照顺序执行,即执行完一句才会执行下一句,这样的代码逻辑简单,符合人类的阅读习惯。但是这样是不够的,因为计算机非常强大,比如一款音乐软件,在听音乐的时候想下载歌曲,同一时刻做两件事,在编程中,这就是并发,并发可以让编写的程序在同一时刻做几件事。

进程和线程

​ 讲并发就绕不开线程,不过在介绍线程前,先介绍什么是进程。

进程

​ 在操作系统中,进程是一个非常重要的概念。当启动一个软件时,操作系统会为这个软件创建一个进程,这个进程就是软件的工作空间,它包含了软件运行所需的所有资源,比如内存空间、文件句柄,还有线程。

image-20211213214435023

线程

​ 线程是进程对的执行空间,一个进程可以有多个线程,线程被操作系统调度执行,比如下载一个文件,发送一个消息等。这种多个线程被调度系统同时调度执行的情况,就是多线程并发

​ 一个程序启动,就会有对应的进程被创建,同时进程也会启动一个线程,这个线程就叫主线程。如果主线程结束,那么整个流程就退出了。有了主线程,就可以从主线程里启动其他线程,也就有了多线程并发。

协程(Goroutine)

​ Go语言中没有线程的概念,只有协程,也称为goroutine。相比线程来说,协程更加轻量,一个程序可以随意启动成千上万个goroutine。

​ goroutine被Go Runtime调度,这一点和线程不一样。也即是说,Go语言的并发是由Go自己所调度的,自己决定同事执行多少个goroutine,什么时候执行哪几个。这对于开发者来说是完全透明,只需要在编码的时候告诉Go语言要启动几个goroutine,至于如何调度执行,不用关心。

​ 要启动一个goroutine非常简单,Go语言提供了 go 关键字,相比其他编程语言简化了很多

func main() {
    go fmt.Println("奔跑的蜗牛")
    fm.Println("独臂阿童木")
    time.Sleep(time.Second)
}


// 输出
独臂阿童木
奔跑的蜗牛

// 程序是并发的,go关键字启动的goroutine并不阻塞main  goroutine的执行,所以才会打印出上述结果。示例中main goroutine等待一秒,避免main goroutine执行完就退出了,看不到启动的新goroutine打印结果

​ 这样就启动了一个goroutine,用来调度fmt.Println()函数。这段代码里有两个goroutine,一个是main函数启动的main goroutine,一个是自己通过go关键字启动的goroutine。

go 关键字 紧跟一个方法或者函数,就可以启动一个goroutine,让方法在这启动的goroutine中运行。

Channel

​ 如果启动了多个goroutine,他们之间如何通信呢?这就是Go语言提供的 Channel(通道) 要解决的问题。

声明一个 channel

​ 在Go语言中,声明一个channel非常简单,使用内置make函数即可

ch := make(chan string)

// chan 是关键字,表示是chan类型,后面string表示channel里面的数据是string类型。通过channel申明可以看到,chan是一个集合类型

​ 定义好chan后就可以使用了,一个chan操作只有两种:发送和接收。

  • 接收: 获取chan中的值,操作符为 <-chan
  • 发送:向chan发送值,把值放在chan中,操作符为 chan <-

小技巧:注意发送和接收的操作符都是**<-**,只不过位置不同。接收的操作符在chan的左侧,发送的操作符在chan的右侧。

func main() {
    ch := make(chan string)
    
    go func() {
        fmt.Println("奔跑的蜗牛")
        ch <- "goroutine完成"
    }()
    
    fmt.Println("独臂阿童木")
    v := <-ch
    fmt.Println("接收到的chan中的值:", v)
}

// 程序并没有直接退出,可以看到打印结果,达到了time.Sleep函数的效果
// 因为通过maike创建的chan中没有值,而main goroutine想从chan中获得值,获取不到就一直等待,等到另一个goroutine向chan中发送值为止

// 出书
独臂阿童木
奔跑的蜗牛
接收到的chan中的值: goroutine完成
上述示例中,在新启动的goroutine中向chan类型的变量ch发送值;在main goroutine中,从变量ch接收值,如果ch中没有值,则阻塞等待ch中有值可以接收为止。

​ channel有点像在两个goroutine之间架起管道,一个goroutine可以往这个管道发送数据,另一个可以从这个管道接收数据,有点类似队列。

无缓冲channel

​ 上述示例中make创建的就是一个无缓冲channel,它的容量是0,不能存储任何数据。所以无缓冲channel只起传输数据的作用,数据并不会在channel中停留。这也意味着,无缓冲channel的发送和接收操作是痛失进行的,它也被称为同步channel

有缓冲channel

​ 有缓冲channel类似一个阻塞对垒,内部的元素先进先出。通过make函数的第二个参数可以指定channel容量的大小,进而创建一个有缓冲channel

cacheCh := make(chan string, 3)

// 创建一个容量为5的channel,内部元素类型是string,也就是说这个channel内部最多可以存放5个类型为string的元素

​ 一个有缓冲channel具备一下特点:

1. 有缓冲channel的内部有一个缓冲队列
2. 发送操作是向对垒的尾部插入元素,如果队列已满,则阻塞等待,直到另一个goroutine执行,接收操作释放队列空间
3. 接收操作是从队列的头部获取元素并把它从队列中删除,如果队列为空,则阻塞等待,知道另一个goroutine执行,发送数据插入新的元素

​ 因为有缓冲channel类似一个队列,可以获取它的容量和里面元素的个数。

cacheCh := make(chan int, 5)
cachech <- 2
cachech <- 4
fmt.Println("cacheCh 容量为:", cap(cacheCh), "元素个数为:"len(cacheCh))

// 通过内置函数cap可以获取channel的容量,也就是最大能存放多少元素,通过内置函数len可以获取channel中元素的个数

小提示:无缓冲channel其实就是一个容量大小为0的channel。比如 make(chan int, 0)

关闭 channel

​ channel 可以使用内置函数 close 关闭。

close(cacheCh)

​ 如果一个channel被关闭了,就不能向里面发送数据了,如果发送的话,会引起panic异常。但是还可以接收channel里的数据,如果channel里面没有数据的话,接收的数据是元素类型的零值。

单向channel

​ 有时候,因为特殊的业务需求,比如限制一个channel只可以接收但不能发送,或者限制一个channel只能发送但不能接接收,这种channel被称为单向channel

​ 单向channel的声明也很简单,只需要在声明的时候带上<- 操作符即可。

onlySend := make(chan <- int)
onlyReceive := make(<-chan int)

// 注意: 声明单向channel <- 操作符的位置和上面发送接收操作是一样的。

​ 在函数后者方法的参数中,使用单向channel比较多,这样可以防止一些操作影响了channel.

func counter(out chan <- int) {
    // 函数内容使用变量out, 只能进行发送操作
}

select + channel示例

​ 假如我们要下载一个文件,启动了3个goroutine进行下载,并发结果发送到3个channel中。哪个先下载好,就先用哪个channel的结果。

​ 在Go语言中,通难过select语句实现多路复用操作,语句格式如下:

select {
case i1 <- c1 :
    // todo
case i2 <- c2 :
    // todo
default :
    // todo
}

​ 整体结构和switch很像,都有case和default,只不过select和case是一个个可以操作的channel。

小提示:多路复用可以简单的理解为,N个channel中,任意一个channel有数据产生,select都可以监听到, 然后执行对应的分支,接收数据并处理。

// 多路下载示例

func main() {
    // 声明3个存放结果的channel
    firstCh := make(chan string)
    secondCh := make(chan string)
    threeCh := make(chan string)
    
    // 同时开启3个goroutine下载
    go func() {
        firstCh <- downloadFile("firstCh")  
    }()
     go func() {
        secondCh <- downloadFile("secondCh")  
    }()
     go func() {
        threeCh <- downloadFile("threeCh")  
    }()
    
    // 开启多路复用,哪个channel能获取到值,就先用哪个
    select {
        case filePath := <- firstCh :
        fmt.Println(filePath)
        case filePath := <- secoudCh :
        fmt.Println(filePath)
        case filePath := <- threeCh :
        fmt.Println(filePath)
    }
    
    func downloadFile(chanName string) string {
        // 模拟下载文件,可以随机time.Sleep时间
        time.Sleep(time.Second)
        return chanName + "filePath"
    }
}

​ 如果这些case中有一个可以执行,select语句会选择该case语句执行,如果同时又多个case可以被执行,则随机选择一个,这样每个case都有平等的被执行的机会。如果一个select没有任何case,那么就会一直等下去。

小结:channel是Go语言并发的基础,在Go语言中,提倡通过通信来共享内存,而不是通过共享内存来通信,其实就是提倡通过channel发送接收消息的方式进行数据通信,而不是通过修改同一个变量。所以在数据流动、传递的场景中要优先使用channel,它是并发安全的,性能也不错。


  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值