浅谈Go的并发|Channel

151 阅读3分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第5天,点击查看活动详情

今天来学习下Go的并发编程,没涉及到底层知识,只是简单介绍下如何使用😂

并发与并行概念

  • 并发是指多线程程序在一个核心的CPU上运行
  • 并行是指多线程程序在多个核心的CPU上运行

并发主要通过时间片轮转来实现同时运行,并行则是直接利用多核实现多线程的运行

gantt
title 并发
dateFormat  YYYY-MM-DD
section A
A           :a1, 2014-01-01, 10d
A     :  2014-01-25, 10d
section B
B      :2014-01-11  , 10d
B     : 2014-02-04  , 10d
gantt
title 并行
dateFormat  YYYY-MM-DD
section A
A    :a1, 2014-01-01, 5d
section B
B      :2014-01-01  , 5d

进程和线程

  1. 进程:操作系统中的一次执行过程,资源分配和调度的单位
  2. 线程:是执行进程分配的任务,是CPU调度和分派的基本单位,比进程更小的可以独立执行
  3. 进程:线程= 1:n 多个线程可以并发执行

协程和线程

协程:独立的栈空间,共享堆空间,调度由用户自己控制,类似于用户级线程 线程:一个线程上可以运行多个协程,协程是轻量级的线程

Goroutine

Goroutine类似于线程,由运行时runtime调度和管理的

Go会自动的将 goroutine 中的任务合理地分配给每个CPU Go语言之所以适合并发编程,因为它在语言层面已经内置了调度和上下文切换的机制

package main

import (
   "fmt"
   "time"
)

func hello() {
   fmt.Println("Hello Goroutine!")
}

func main() {
   //启动一个goroutine去执行hello函数
   go hello()
   time.Sleep(time.Second)
   fmt.Println("main goroutine done!")
}

image.png

这里需要注意,如果不加time.sleep()函数的话,程序只会输出main goroutine done!,因为Main()函数中启动goroutine需要一定时间,而main函数结束的时候,其他启动的goroutine会一并结束,需要让main函数等一等,所以最简单的方式就是time.sleep()方法

执行多个goroutine

上面简单使用time.Sleep在项目中肯定是不合适的,所以可以使用Go语言中的sync.WaitGroup来实现并发任务的同步

方法名功能
(wg * WaitGroup) Add(delta int)计数器+delta
(wg *WaitGroup) Done()计数器-1
(wg *WaitGroup) Wait()阻塞直到计数器变为0
package main

import (
   "fmt"
   "sync"
)

var wgs sync.WaitGroup

func hello(i int) {
   defer wgs.Done() // goroutine结束就登记-1
   fmt.Println("Hello Goroutine!", i)
}

func main() {

   for i := 0; i < 10; i++ {
      wgs.Add(1) // 启动一个goroutine就登记+1
      go hello(i)
   }
   wgs.Wait() // 等待所有登记的goroutine都结束
}

image.png

多次执行的上面的代码,每次的输出结果都不一致,因为goroutine是并发执行,调度是随机

Channel

channel顾名思义就是管道,它的作用是用来函数之间进行数据通信,因为在goroutine中对共享内存会存在竞争关系,为了保证数据的一致性,需要使用互斥锁进行加锁,这显然会导致性能下降,所以Go语言推荐使用通信共享数据而不是访问共享数据进行通信

goroutine是Go程序并发的执行体,channel就是它们之间的桥梁

channel是一种特殊的类型,类似传送带,遵循先入先出FIFO的规则,保证收发数据的顺序

每一个通道只负责一个具体的类型数据,所以声明channel的时候需要为其指定元素类型


声明管道类型的形式如下:

    var 变量 chan 元素类型  
    var ch1 chan int   // 声明一个传递整型的通道
    var ch2 chan bool  // 声明一个传递布尔型的通道
    var ch3 chan []int // 声明一个传递int切片的通道    

管道声明之后需要使用Make函数初始化才能使用

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

channel操作

  1. 发送操作send: chan<-
  2. 接收操作reeceive: <-chan
  3. 关闭操作: close(chan) 内置函数 close()

关闭操作需要注意的:

  1. 对一个关闭的通道发送值就会导致异常
  2. 对一个关闭的通道(通道内还有值)进行接收会一直获取值直到通道为空
  3. 对一个关闭的并且没有值的通道执行接收操作会得到对应类型的零值
  4. 关闭一个已经关闭的通道会导致异常

缓冲和无缓冲的区别

无缓冲的通道又称为阻塞的通道,需要有接收者才能发送

func recv(c chan int) {
    res := <-c   
    fmt.Println("接收成功", res)
}
func main() {
    ch := make(chan int) //无缓冲管道
    go recv(ch) // 启用goroutine从通道接收值
    ch <- 10
    fmt.Println("发送成功")
}   

image.png

只要通道的容量大于0,那么就创建了一个有缓冲的管道,make函数初始化通道的时候为其指定通道的容量

func main() {
    ch := make(chan int, 1) // 创建一个容量为1的有缓冲区通道
    ch <- 10
    fmt.Println("发送成功")
}   

总结

今天简单的学习了Go并发编程与channel,还有很多细节的部分,下一步需要继续深入,对于刚入门go语言的我来说,还有许多地方需要学习,有错误的地方欢迎大家指出,共同进步!!