并发,就是让程序在同一时刻做多件事情。

go语言天生自带并发属性,使得并发编程十!分!方!便!,我们只需要 go 函数名() 即可!\(^o^)/

进程和线程

进程

程序启动时,系统会为其创建一个进程

线程

是进程的执行空间,一个进程可以包含多个线程,线程被操作系统调度执行 一个程序启动,对应的进程会被创建,同时也会创建一个线程(主线程),主线程结束,整个程序也就退出了。 我们可以从主线程创建其他的子线程,这就是多线程并发

协程 Goroutine

goroutine比线程更加轻盈,被Go runtime调度。 启动协程: go function()

// 这里启动了两个goroutine, 一个是用go关键字触发的,另一个是 main goroutine(主线程)
func main(){
  go fmt.Println("Hello goroutine.")
  fmt.Println("Main goroutine.")
  time.Sleep(time.Second)
}

Channel

多个goroutine之间,使用 channel进行通信

声明一个channel

// 直接使用 make 创建一个channel,接受的数据类型是string
ch := make(chan string)
// 一个channel的操作只有两种:
// - 发送,向chan中发送值: chan<-
// - 接受,从chan中获取值: <-chan

demo

func main(){
  ch := make(chan string)
  go func(){
    fmt.Println("Message in goroutine")
    ch <- "goroutine finished."	// send message to channel.
  }()
  fmt.Println("Main goroutine.")
  v := <-ch		// receive message from channel
  fmt.Println("Message from channel: ", v)
}

在上面的示例中,我们在新启动的 goroutine 中向 chan 类型的变量 ch 发送值;在 main goroutine 中,从变量 ch 接收值;如果 ch 中没有值,则阻塞等待到 ch 中有值可以接收为止。

通过 make 创建的 chan 中没有值,而 main goroutine 又想从 chan 中获取值,获取不到就一直等待,等到另一个 goroutine 向 chan 发送值为止。

channel 有点像在两个 goroutine 之间架设的管道,一个 goroutine 可以往这个管道里发送数据,另外一个可以从这个管道里取数据,有点类似于我们说的队列。

无缓冲channel

上述的channel就是一个无缓冲的channel,容量为0,无法存储数据,只能传输。 它的发送和接受操作是同时的,可以称为同步channel

有缓冲channel

类似一个可阻塞的队列,内部的元素先进先出,通过make第二个参数指定channel容量大小:

ch := make(chan int, 5)  // 创建一个容量为5的channel

上述创建一个容量为5的channel,可以存放最多5个int类型的元素,其具备以下特点:

  • 有缓冲channel内部有一个缓冲队列
  • 发送操作是向队尾插入元素,如果队列已满,则阻塞等待,直到另一个goroutine执行接收操作,释放channel的空间
  • 接收操作是从队头获取一个元素,如果队列已空,阻塞等待,直到有goroutine插入新的元素

关闭channel

使用内置的close函数关闭:

close(ch)

如果一个channel被关闭了,就不能向里面发送数据了,继续发送会引发panic异常。

但是可以接受已关闭channel里面的数据,无数据则接受的是元素类型的零值。

单向channel

只能发送不能接收,或者只能接收不能发送的channel,成为单向channel

onlySend := make(chan<- int)			// 单发
onlyReceive := make(<-chan int)		// 单收

这样的channel一般在函数或者方法的参数声明中使用:

func counter(sendCh chan<- int){
  // 只能往sendCh中发送数据
  // num := <-sendCh  // 不能从单发channel中接受数据,编译会不通过
}

select + channel 示例

// 结构示例
select {
  case i1 = <-ch1:
  	// todo
  case ch2 <- i2:
  	// todo
  default:
  	// default process
}

整体结构与switch很像,有case和default,但是select的case是一个个可以操作的channel

// select 下载例子
func main() {
  firstCh := make(chan string)
  secondCh := make(chan string)
  thirdCh := make(chan string)
  
  go func() { firstCh <- downloadFile("firstCh") }()
  go func() { secondCh <- downloadFile("secondCh") }()
  go func() { thirdCh <- downloadFile("thirdCh") }()
  
  // select 多路复用,哪个channel最先获取到值,
  // 就说明当前channel下载好了
  select {
  case filePath := <-firstCh:
    	fmt.Println(filePath)
  case filePath := <-secondCh:
    	fmt.Println(filePath)
  case filePath := <-thirdCh:
    fmt.Println(filePath)
  }
}

func downloadFile(filename string) string {
  time.Sleep(time.Second)
  return "filepath:" + filename
}

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

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