0. CSP--Communicating Sequential Process

Don't communicate by sharing memory; share memory by communicating.

CSP模型是上个世纪七十年代提出的,用于描述两个独立的并发实体通过共享的通讯 channel(管道)进行通信的并发模型。 CSP中channel是第一类对象,它不关注发送消息的实体,而关注与发送消息时使用的channel。CSP的设计理念,是依赖一个信息通道来完成两个通信实体之间的通信协调。

虽然可以使用共享内存进行数据交换,但是共享内存在不同的goroutine中容易发生竞态问题。为了保证数据交换的正确性,必须使用互斥量对内存进行加锁,这种做法势必造成性能问题。Go语言CSP(Communicating Sequential Processes),提倡通过通信共享内存而不是通过共享内存而实现通信。

如果说goroutine是Go程序并发的执行体,channel就是它们之间的连接。channel是可以让一个goroutine发送特定值到另一个goroutine的通信机制。

Go 语言中的通道(channel)是一种特殊的类型。通道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序。每一个通道都是一个具体类型的导管,也就是声明channel的时候需要为其指定元素类型。

Golang以CSP 并发模型做为并发基础,底层使用goroutine做为并发实体,goroutine非常轻量级可以创建几十万个实体。实体间通过 channel传递匿名消息使之解耦,在语言层面实现了自动调度,这样屏蔽了很多内部细节,对外提供简单的语法关键字,大大简化了并发编程的思维转换和管理线程的复杂性。

1. go协程(go routine)

go原生支持并发:goroutine和channel。

go协程是与其他函数或方法一起并发运行的函数和方法。go协程可以看作是轻量级线程。

func runtime.GOMAXPROCS(n) int -- sets the maximum number of CPUs that can be executing simultaneously and returns the previous setting. 

调用函数或者方法时,在前面加上关键字go,可以让一个新的GO协程并发地运行。

  • l  启动一个新的协程时,协程的调用会立即返回。与函数不同,程序控制不会去等待 Go 协程执行完毕。在调用 Go 协程之后,程序控制会立即返回到代码的下一行,忽略该协程的任何返回值。
  • l  如果希望运行其他 Go 协程,Go 主协程必须继续运行着。如果 Go 主协程终止,则程序终止,于是其他 Go 协程也不会继续运行。

常见的问题:go协程引用外部循环变量(参考:协程引用循环变量的问题

//mu := &sync.Mutex
for i := 0; i < 10; i++ {
  go func() {
    // mu.Lock()
    // mu.Unlock()
    fmt.Println(i)
  } ()
}

理想状态时输出1到10,无论是否有mutex,都不可能输出正确结果。大部分输出的数字是10,原因:

主协程的循环很快就跑完了,而各个协程才开始跑,此时i的值已经是10了,所以各协程都输出了10。(输出7或其他数字的协程,在开始输出的时候主协程的i值刚好是7,这个结果每次运行输出都不一样)

如果通过go vet main.go会报如下警告: main.go:24:16: loop variable i captured by func literal

for i := 0; i < 10; i++ {
    go func(i0 int) {
        fmt.Println(i0)
    } (i) // 
}

变量i已经有了一个副本,协程中针对副本处理。

使用goroutine原则:(管理goroutine生命周期)

1)goroutine什么时候终止;2)怎样终止goroutine。

2. 无缓冲信道channel

信道可以想象成Go协程之间通信的管道。

chan T 表示 T 类型的信道。信道与类型相关,只能运输该类型的数据。

信道是一种引用类型,信道的零值为 nil。信道的零值没有什么用,应该像对 map 和切片所做的那样,用 make 来定义信道或初始化信道。

var ch chan int // 声明信道
fmt.Println(ch) // <nil>
ch = make(chan int) //初始化
fmt.Println(ch) // 0xc000020180

data := <- a // 读取信道 a a <- data // 写入信道 a

无缓冲信道发送与接收默认是阻塞的,即无缓冲信道是同步的

信道会产生死锁。当Go协程给一个信道发送数据,而没有信道接收数据时,程序触犯panic,形成死锁。

信道数据都是单传递的,当一个接收信道收到数据后,其他信道接收者就不能接收到相同数据,即信道数据只能由任意一个且仅有一个信道接收者获取到。

对一个nil信道发送或者接收数据都会造成永远阻塞。例如没有make初始化或定义的信道。

单向信道

sendch := make(chan<- int)    // 定义单向信道,定义只写数据的信道,<-指向chan
只写通道:chan<- T
只读通道:<-chan T

有方向的channel不能被关闭。

可以把一个双向信道转换成唯送信道或者唯收信道(send only or receive only),但反过来不可以。

golang并发基础
package main

import (
    "fmt"
    "time"
    "os"
)

func main(){
    
    data := make(chan int)
    go func(out chan<- int){
        time.Sleep(2* time.Second)
        out <- 1
    }(data)

    <- data

    fmt.Println("Receive data, first")

    go func(out <-chan int){
        time.Sleep(2 * time.Second)
        <-out
        fmt.Println("Receive data, Second")
        os.Exit(0)
    }(data)

    data <- 2
    for {
        time.Sleep(1 * time.Second)
    }   
}
single channel

相关文章: