Go语言的并发指的是能让某个函数独立于其他函数运行的能力。当一个函数创建为goroutine时,Go会将其视为一个独立的工作单元。这个单元会被调度到可用的逻辑处理器上执行。
Go语言运行时的调度器是一个复杂的软件,能管理被创建的所有goroutine并为其分配执行时间。这个调度器在操作系统之上,将操作系统的线程与语言运行时的逻辑处理器绑定,并在逻辑处理器上运行goroutine。调度器在任何给定的时间,都会全面控制哪个goroutine要在哪个逻辑处理器上运行。
Go语言的并发同步模型来自一个叫做通信顺序进程 (CSP)的范型。CSP是一种消息传递类型,通过在goroutine之间传递数据来传递消息,而不是对数据进行加锁来实现同步访问。用于在goroutine之间同步和传递数据的关键数据类型叫做通道

一、并发与并行
操作系统会在物理处理器上调度线程来运行,而Go语言的运行时会在逻辑处理器上调度goroutine来运行。每个逻辑处理器都分别绑定到单个操作系统线程。这些逻辑处理器会用于执行所有被创建的goroutine。即便只有一个逻辑处理器,Go也可以以神奇的效率和性能,并发调度无数个goroutine。
下图展示了Go调度器如何管理goroutine。
在这里插入图片描述
可以看到操作系统线程、逻辑处理器和本地运行队列之间的关系。如果创建一个goroutine并准备运行,这个goroutine就会被放到调度器的全局运行队列中。之后,调度器就将这些队列中的goroutine分配给一个逻辑处理器,并放到这个逻辑处理器对应的本地运行队列中。本地运行队列中的goroutine会一直等待直到自己被分配的逻辑处理器执行。
有时,正在运行的goroutine需要执行一个阻塞的系统调用,如打开一个文件。当这类调用发生时,线程和goroutine会从逻辑处理器上分离,该线程会继续阻塞,等待系统调用的返回。与此同时,这个逻辑处理器就失去了用来运行的线程。所以,调度器会创建一个新线程,并将其绑定到该逻辑处理器上。之后,调度器会从本地运行队列里选择另一个goroutine来执行。一旦被阻塞的系统调用执行完成并返回,对应的goroutine会放回到本地运行队列,而之前的线程会保存好,以便之后可以继续使用。
并发不是并行。并行是让不同的代码片段同时在不同的物理处理器上执行。并行的关键是同时做很多事情,而并发是同时管理很多事情,这些事情可能只做了一半就被暂停去做别的事情了。

二、goroutine
下面来看一个样例,创建两个goroutine以并发形式分别显示大写和小写的英文字母。

package mainimport ("fmt""runtime""sync"
)func main() {//分配一个逻辑处理器给调度器使用//GOMAXPROCS允许程序更改调度器可以使用的逻辑处理器的数量runtime.GOMAXPROCS(1)//wg用来等待程序完成、计数加2,表示要等待两个goroutinevar wg sync.WaitGroupwg.Add(2)fmt.Println("Start Goroutines")//声明一个匿名函数,并创建一个goroutinego func() {//在函数退出时调用Done来通知main函数工作已经完成defer wg.Done()//显示三次字母表for count := 0; count < 3; count++ {for char := 'a'; char <= 'z'; char++ {fmt.Printf("%c ", char)}}}()go func() {//在函数退出时调用Done来通知main函数工作已经完成defer wg.Done()for count := 0; count < 3; count++ {for char := 'A'; char <= 'Z'; char++ {fmt.Printf("%c ", char)}}}()//等待goroutine结束fmt.Println("Waiting To Finish")wg.Wait()fmt.Println("\nTerminating Program")
}

三、竞争状态
如果两个或者多个goroutine在互相没有同步的情况下,访问某个共享的资源,并试图同时读和写这个资源,就处于相互竞争的状态,这种情况被称作竞争状态。所以对一个共享资源的读和写操作必须是原子化的。

四、锁住共享资源
Go语言提供了传统的同步goroutine的机制,就是对共享资源加锁。如果需要顺序访问一个整型变量或者一段代码,atomic和sync包里的函数提供了很好的解决方案。
1、原子函数
原子函数能够以很底层的加锁机制来同步访问整型变量和指针。下面是一个使用原子函数修正竞争状态的示例。

package mainimport ("fmt""runtime""sync""sync/atomic"
)var (//counter是所有goroutine都要增加其值的变量counter int64//wg用来等待程序结束wg sync.WaitGroup
)func main() {//计数加2,表示要等待两个goroutinewg.Add(2)//创建两个goroutinego incCounter(1)go incCounter(2)//等待goroutiine结束wg.Wait()//显示最终的值fmt.Println("Final Counter:", counter)
}func incCounter(id int) {defer wg.Done()for count := 0; count < 2; count++ {//安全地对counter加1//AddInt方法强制同一时刻只能有一个goroutine运行并完成这个加法操作//类似的函数还有LoadInt64,StoreInt64atomic.AddInt64(&counter, 1)//当前goroutine从线程退出,并放回到队列runtime.Gosched()}
}

2、互斥锁
互斥锁用于在代码上创建一个临界区,保证同一时间只有一个goroutine可以执行这个临界区代码。

func incCounter(id int) {defer wg.Done()for count := 0; count < 2; count++ {//同一时刻只允许一个goroutine进入mutex.Lock(){//捕获counter的值value := counter//当前goroutine从线程退出,并放回队列runtime.Gosched()//增加本地value值value++//保存回countercounter = value}mutex.Unlock()//释放锁,允许其他正在等待的goroutine}
}

对counter的操作在Lock()和Unlock()函数调用定义的临界区里被保护起来。使用大括号不是必须的。同一时刻只有一个goroutine可以进入临界区。

五、通道
除了上述方法,你还可以使用通道,通过发送和接收需要共享的资源,在goroutine之间做同步。

使用make创建通道

//无缓冲的整型通道
unbuffere := make(chan int)
//有缓冲的字符串通道
buffered := make(chan string, 10)

向通道发送值

//有缓冲的字符串通道
buffered := make(chan string, 10)
//通过通道发送一个字符串
buffered <- "Gopher"
//从通道接受值
value := <-buffered

1、无缓冲的通道指接受前没有能力保存任何值的通道。这种类型的通道强制要求goroutine之间必须同时完成发送和接收。
2、有缓冲的通道是一种在被接收前能存储一个或多个值的通道。这种类型的通道并不强制要求goroutine之间必须同时完成发送和接收。