Golang 里的并发指的是能让某个函数独立于其他函数运行的能力。当一个函数创建为 goroutine 时,Golang 会将其视为一个独立的工作单元。这个单元会被调度到可用的逻辑处理器上执行。Golang 的并发同步模型来自一个叫作通信顺序进程(Communicating Sequential Processes,CSP)的范型(paradigm)。Golang的并发同步通过CSP(一种消息传递模型)在 goroutine 之间传递数据来传递消息,而不是对数据进行加锁来实现同步访问。
用于在 goroutine 之间同步和传递数据的关键数据类型叫作通道(channel)。
进程
当运行一个应用程序的时候,操作系统会为这个应用程序启动一个进程。可以将这个进程看作一个包含了应用程序在运行中需要用到和维护的各种资源的容器。这些资源包括但不限于内存地址空间、文件和设备的句柄以及线程。
线程
一个线程是一个执行空间,这个空间会被操作系统调度来运行函数中所写的代码。每个进程至少包含一个线程,每个进程的初始线程被称作主线程。因为执行这个线程的空间是应用程序的本身的空间,所以当主线程终止时,应用程序也会终止。操作系统将线程调度到某个处理器上运行,这个处理器并不一定是进程所在的处理器。
协程也叫轻量级的线程,与传统的进程和线程相比,协程的最大特点是 "轻"!可以轻松创建上百万个协程而不会导致系统资源衰竭。
运行时rumtime就是程序运行的时候,在 Golang 1.5 及以后的版本中,运行时默认会为每个可用的物理处理器分配一个逻辑处理器,每个逻辑处理器都与一个操作系统线程绑定。
创建一个 goroutine 并准备运行,这个 goroutine 首先会被放到调度器的全局运行队列中。之后,调度器会将全局运行队列中的 goroutine 分配给一个逻辑处理器,并放到这个逻辑处理器的本地运行队列中。本地运行队列中的 goroutine 会一直等待直到被分配的逻辑处理器执行。每个逻辑处理器有一个本地运行队列。
Golang 运行时默认限制每个程序最多创建 10000 个线程。这个限制值可以通过调用 runtime/debug 包的 SetMaxThreads 方法来更改。如果程序试图使用更多的线程,就会崩溃。(“runtime/debug/grabage.go/”)
func SetMaxThreads(threads int) int {
return setMaxThreads(threads)
}
并发(concurrency)与并行(parallelism)不同
并行是让不同的代码片段同时在不同的物理处理器上执行,并行的关键是同时做很多事情,意味着物理处理器越多并行度越高。
并发是指同时管理很多事情,这些事情可能只做了一半就被暂停去做别的事情了(Golang 的并发通过切换多个线程达到减少物理处理器空闲等待的目的),高并发意味着资源利用率更高。
在很多情况下,并发的效果比并行好,因为操作系统和硬件的总资源一般很少,但能支持系统同时做很多事情。这种 "使用较少的资源做更多的事情" 的哲学,也是指导 Golang 设计的哲学。
WaitGroup 用来等待单个或多个 goroutines 执行结束。在主逻辑中使用 WaitGroup 的 Add 方法设置需要等待的 goroutines 的数量。在每个 goroutine 执行的函数中,需要调用 WaitGroup 的 Done 方法。最后在主逻辑中调用 WaitGroup 的 Wait 方法进行阻塞等待,直到所有 goroutine 执行完成。
使用方法可以总结为下面几点:
- 创建一个 WaitGroup 实例,例如:
var wg sync.WaitGroup
- 调用 wg.Add(n),其中 n 是等待的 goroutine 的数量
wg.Add(1)
- 在每个 goroutine 运行的函数中执行 defer wg.Done()
defer wg.Done()
- 调用 wg.Wait() 阻塞主逻辑
wg.Wait()
通过设置 runtime.GOMAXPROCS(1),强制让 goroutine 在一个逻辑处理器上并发执行。用同样的方式,我们可以设置逻辑处理器的个数等于物理处理器的个数,从而让 goroutine 并行执行(从 Golang 1.5 开始,GOMAXPROCS 的默认值已经等于可以使用的物理处理器的数量了)
runtime.GOMAXPROCS(runtime.NumCPU())
调用runtime.Gosched()将goroutine从当前线程退出。
Channel
Channel 是 Golang 在语言级别提供的 goroutine 之间的通信方式,可以使用 channel 在两个或多个 goroutine 之间传递消息。使用Channel 发送和接收所需的共享资源,可以在 goroutine 之间消除竞争条件。一个 channel 只能传递一种类型的值,这个类型需要在声明 channel 时指定。
channel声明
var Channelname chan type
例:
var ch chan int
var m map[string] chan bool
channel声明及初始化
ch := make(chan int) //创建不带缓冲区的channel
ch := make(chan int, 10) //创建带缓冲区的channel
向channel中写入数据,向 channel 中写入数据通常会导致程序阻塞,直到有其它 goroutine 从这个 channel 中读取数据。
ch <- value
从channel中读取数据,如果 channel 中没有数据,那么从 channel 中读取数据也会导致程序阻塞,直到 channel 中被写入数据为止。
value := <-ch
无缓冲代码示例hit ball:
// 这个示例程序展示如何用无缓冲的通道来模拟
//2个goroutine间的网球比赛
package main
import(
"math/rand"
"sync"
"time"
"fmt"
)
// wg用来等待程序结束
var wg sync.WaitGroup
func init() {
rand.Seed(time.Now().UnixNano())
}
// main是所有Go程序的入口
func main() {
// 创建一个无缓冲的通道
court := make(chan int)
// 计数加2,表示要等待两个goroutine
wg.Add(2)
// 启动两个选手
go player("Nick", court)
go player("Jack", court)
// 发球
court <- 1
// 等待游戏结束
wg.Wait()
}
// player 模拟一个选手在打网球
func player(name string, court chan int) {
// 在函数退出时调用Done来通知main函数工作已经完成
defer wg.Done()
for{
// 等待球被击打过来
ball, ok := <-court
if !ok {
// 如果通道被关闭,我们就赢了
fmt.Printf("Player %s Won\n", name)
return
}
// 选随机数,然后用这个数来判断我们是否丢球
n := rand.Intn(100)
if n%5 == 0 {
fmt.Printf("Player %s Missed\n", name)
// 关闭通道,表示我们输了
close(court)
return
}
// 显示击球数,并将击球数加1
fmt.Printf("Player %s Hit %d\n", name, ball)
ball++
// 将球打向对手
court <- ball
}
}
有缓冲channel示例
// 这个示例程序展示如何使用
// 有缓冲的通道和固定数目的
// goroutine来处理一堆工作
package main
import (
"fmt"
"math/rand"
"sync"
"time"
)
const(
numberGoroutines = 2 // 要使用的goroutine的数量
taskLoad = 5 // 要处理的工作的数量
)
// wg用来等待程序结束
var wg sync.WaitGroup
func init() {
rand.Seed(time.Now().UnixNano())
}
// main是所有Go程序的入口
func main() {
// 创建一个有缓冲的通道来管理工作
tasks := make(chan string, taskLoad)
// 启动goroutine来处理工作
wg.Add(numberGoroutines)
for gr := 1; gr <= numberGoroutines; gr++ {
go worker(tasks, gr)
}
// 增加一组要完成的工作
for post := 1; post <= taskLoad; post++ {
tasks <- fmt.Sprintf("Task: %d", post)
}
// 当所有工作都处理完时关闭通道
// 以便所有goroutine退出
close(tasks)
// 等待所有工作完成
wg.Wait()
}
// worker作为goroutine启动来处理
// 从有缓冲的通道传入的工作
func worker(tasks chan string, worker int) {
// 通知函数已经返回
defer wg.Done()
for{
// 等待分配工作
task, ok := <-tasks
if !ok{
// 这意味着通道已经空了,并且已被关闭
fmt.Printf("Worker: %d: Shutting Down\n", worker)
return
}
// 显示我们开始工作了
fmt.Printf("Worker: %d: Started %s\n", worker, task)
// 随机等一段时间来模拟工作
sleep := rand.Int63n(100)
time.Sleep(time.Duration(sleep)* time.Millisecond)
// 显示我们完成了工作
fmt.Printf("Worker: %d: Completed %s\n", worker, task)
}
}
Select
利用select处理channel超时
select {
case <-chan1: // 如果 chan1 成功读取到数据,则执行该 case 语句
case chan2 <- 1: // 如果成功向 chan2 写入数据,则执行该 case 语句
default: // 如果上面的条件都没有成功,则执行 default 流程
}
示例:
ch := make(chan int)
// 首先实现并执行一个匿名的超时等待函数
timeout := make(chan bool, 1)
go func() {
time.Sleep(1e9) // 等待 1 秒
timeout <- true
}()
// 然后把 timeout 这个 channel 利用起来
select {
case <-ch:
// 从 ch 中读取到数据
case <- timeout:
// 一直没有从 ch 中读取到数据,但从 timeout 中读取到了数据
fmt.Println("Timeout occurred.")
}
关闭channel
当通道关闭后,goroutine 依旧可以从通道接收数据,但是不能再向通道里发送数据。能够从已经关闭的通道接收数据这一点非常重要,因为这允许通道关闭后依旧能取出其中缓冲的全部值,而不会有数据丢失。
使用close(chan)即可关闭channel,读取channel中数据的同时还可以获取一个bool类型值,用于判断channel是否关闭。
r, ok := <-ch
if ok{
fmt.Println("channel is open")
}else{
fmt.Println("channel is closed")
}