目录
并发与并行
并行: 多个cpu
处理多个任务,即多线程程序在多个核的cpu
上运行
并发: 一个cpu
处理多个任务,即多线程程序在一个核的cpu
上运行
进程和线程
- 进程是程序在操作系统中的一次执行过程,系统进行资源分配和调度的一个独立单位。
- 线程是进程的一个执行实体,是
CPU
调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。 - 一个进程可以创建和撤销多个线程;同一个进程中的多个线程之间可以并发执行。
为什么需要并发?
原因有很多,其中比较重要的原因如下:
- 不阻塞等待其他任务的执行,从而浪费时间,影响系统性能。
- 并行可以使系统变得简单些,将复杂的大任务切换成许多小任务执行,单独测试。
在开发中,经常会遇到为什么某些进程通常会相互等待呢?为什么有些运行慢,有些快呢?
通常受限来源于进程I/O
或CPU
。
-
进程I/O限制
如:等待网络或磁盘访问 -
CPU
限制
如:大量计算
Go并发
协程和线程
协程:独立的栈空间,共享堆空间,调度由用户自己控制,本质上有点类似于用户级线程,这些用户级线程的调度也是自己实现的。
线程:一个线程上可以跑多个协程,协程是轻量级的线程。
并发不是并行:
并发主要由切换时间片来实现"同时"运行,并行则是直接利用多核实现多线程的运行,go可以设置使用核数,以发挥多核计算机的能力。
goroutine
奉行通过通信来共享内存,而不是共享内存来通信。
协程Goroutines
golang
的特点就是语言层面支持并发,并且实现并发非常简单,只需在需要并发的函数前面添加关键字go
每个go
程序至少都有一个Goroutine
:主Goroutine
(在运行进程时自动创建)。以及程序中其他Goroutine
例如:下面程序创建了main
的Goroutine
及匿名的Goroutine
。
func main() {
go func() {
fmt.Println("hello goroutines !")
}()
}
sync 包提供了互斥锁这类的基本的同步原语.除 Once
和 WaitGroup
之外的类型大多用于底层库的例程。更高级的同步操作通过信道与通信进行.
WaitGroup
假设主线程要等待其余的goroutine
都运行完毕,不得不在末尾添加time.Sleep()
,但是这样会引发两个问题:
- 等待多长时间?
- 时间太长,影响性能?
在go
的sync
库中的WaitGroup可以帮助我们完成此项工作,WaitGroup
用于等待一组例程的结束。主例程在创建每个子例程的时候先调用Add
增加等待计数,每个子例程在结束时调用Done
减少例程计数。之后,主例程通过Wait
方法开始等待,直到计数器归零才继续执行。 - type WaitGroup
type WaitGroup struct {
// contains filtered or unexported fields
}
- func (*WaitGroup) Add
func (wg *WaitGroup) Add(delta int)
计数器增加delta
,如果计数器为零,则释放等待时阻塞的所有goroutine
- func (*WaitGroup) Done
func (wg *WaitGroup) Done()
计数器减少1
- func (*WaitGroup) Wait
func (*WaitGroup) Wait
等待直到计数器归零。如果计数器小于 0
,则该操作会引发panic
。
使用示例:等待多个goroutine
完成,可以使用一个等待组。
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done()
fmt.Printf("Worker %d starting\n", id)
time.Sleep(time.Second)
fmt.Printf("Worker %d done\n", id)
}
func main() {
var wg sync.WaitGroup
for i := 1; i <= 5; i++ {
wg.Add(1)
go worker(i, &wg)
}
wg.Wait()
fmt.Println("Done")
}
这里首先把wg
计数设置为1
, 每个for
循环运行完毕都把计数器减一,主函数中使用Wait()
一直阻塞,直到wg
为1
——也就是for
循环都运行完毕。
使用注意点:
- 计数器不能为负值,否则引发
panic
WaitGroup
对象不是引用类型
Once
Once的作用是多次调用但只执行一次,Once
只有一个方法,Once.Do()
,向Do
传入一个函数,这个函数在第一次执行 Once.Do()
的时候会被调用,以后再执行Once.Do()
将没有任何动作,即使传入了其它的函数,也不会被执行,如果要执行其它函数,需要重新创建一个Once
对象。即sync.Once
可以控制函数只能被调用一次,不能多次重复调用。
- type Once
type Once struct {
// contains filtered or unexported fields
}
- func (*Once) Do
func (o *Once) Do(f func())
使用示例:
package main
import (
"fmt"
"sync"
"time"
)
func main() {
o := &sync.Once{}
go myfun(o)
go myfun(o)
time.Sleep(time.Second * 2)
}
func myfun(o *sync.Once) {
fmt.Println("Begin function")
o.Do(func() {
fmt.Println("Working...")
})
fmt.Println("Function end")
}
互斥锁Mutex
互斥锁用来保证在任一时刻,只能有一个例程访问某对象。Mutex
的初始值为解锁状态。Mutex通常作为其它结构体的匿名字段使用,使该结构体具有Lock
和Unlock
方法。
- type Locker
type Locker interface {
Lock()
Unlock()
}
Locker
接口包装了基本的Lock
和UnLock
方法,用于加锁和解锁。
- type Mutex
type Mutex struct {
// contains filtered or unexported fields
}
- func (*Mutex) Lock
func (m *Mutex) Lock()
Lock
用于锁住m
,如果m
已经被加锁,则Lock
将被阻塞,直到m
被解锁。
- func (*Mutex) Unlock
func (m *Mutex) Unlock()
Unlock
用于解锁m
,如果m
未加锁,则该操作会引发panic
。
使用示例:
// SafeCounter 的并发使用是安全的。
type SafeCounter struct {
v map[string]int
mux sync.Mutex
}
// Inc 增加给定 key 的计数器的值。
func (c *SafeCounter) Inc(key string) {
c.mux.Lock()
defer c.mux.Unlock()
// Lock 之后同一时刻只有一个 goroutine 能访问 c.v
c.v[key]++
}
// Value 返回给定 key 的计数器的当前值。
func (c *SafeCounter) Value(key string) int {
c.mux.Lock()
// Lock 之后同一时刻只有一个 goroutine 能访问 c.v
defer c.mux.Unlock()
return c.v[key]
}
func main() {
c := SafeCounter{v: make(map[string]int)}
for i := 0; i < 1000; i++ {
go c.Inc("somekey")
}
time.Sleep(time.Second)
fmt.Println(c.Value("somekey"))
}
在这个例子中,使用了sync.Mutex
的Lock
与Unlock
方法。
RWMutex
sync.Mutex
读操作与写操作都会被阻塞。其实读操作的时候我们是不需要进行阻塞的,因此sync
中还有另一个锁:读写锁RWMutex
,这是一个单写多读模型,可以让多个例程同时读取某对象。RWMutex可以安全的在多个例程中并行使用。
sync.RWMutex
分为:读、写锁。在读锁占用下,会阻止写,但不会阻止读,多个goroutine
可以同时获取读锁,调用RLock()
函数即可,RUnlock()
函数释放。写锁会阻止任何goroutine
进来,整个锁被当前goroutine
,此时等价于Mutex
,写锁调用Lock
启用,通过UnLock()
释放。
- type RWMutex
type RWMutex struct {
// contains filtered or unexported fields
}
- func (*RWMutex) Lock
func (rw *RWMutex) Lock()
Lock
将 rw
设置为写锁定状态,禁止其他例程读取或写入
- func (*RWMutex) RLock
func (rw *RWMutex) RLock()
RLock
将rw
设置为读锁定状态,禁止其他例程写入,但可以读取
- func (*RWMutex) RLocker
func (rw *RWMutex) RLocker() Locker
RLocker
返回一个Locker
接口,该接口通过调用rw.RLock
和rw.RUnlock
来实现Lock
和Unlock
方法
- func (*RWMutex) RUnlock
func (rw *RWMutex) RUnlock()
RUnlock
撤消单个RLock
调用;它不会影响其他读锁调用。如果在读入RUnlock
时未锁定rw
以进行读取,则该操作会引发panic
。
- func (*RWMutex) Unlock
func (rw *RWMutex) Unlock()
Unlock
解除rw
的写锁定状态,如果 rw
未被写锁定,则该操作会引发panic
。
使用示例:对上述例子进行改写,读的时候用读锁,写的时候用写锁。
// SafeCounter 的并发使用是安全的。
type SafeCounter struct {
v map[string]int
rwmux sync.RWMutex
}
// Inc 增加给定 key 的计数器的值。
func (c *SafeCounter) Inc(key string) {
// 写操作使用写锁
c.rwmux.Lock()
defer c.rwmux.Unlock()
// Lock 之后同一时刻只有一个 goroutine 能访问 c.v
c.v[key]++
}
// Value 返回给定 key 的计数器的当前值。
func (c *SafeCounter) Value(key string) int {
// 读的时候加读锁
c.rwmux.RLock()
// Lock 之后同一时刻只有一个 goroutine 能访问 c.v
defer c.rwmux.RUnlock()
return c.v[key]
}
func main() {
c := SafeCounter{v: make(map[string]int)}
for i := 0; i < 1000; i++ {
go c.Inc("somekey")
}
time.Sleep(time.Second)
for i := 0; i < 10; i++ {
fmt.Println(c.Value("somekey"))
}
}
条件变量Cond
Cond用于在并发环境下routine
的等待和通知
- type Cond
type Cond struct {
// L is held while observing or changing the condition
L Locker
// contains filtered or unexported fields
}
在“检查条件”或“更改条件”时 L
应该锁定
- func NewCond
func NewCond(l Locker) *Cond
创建一个条件等待
- func (*Cond) Broadcast
func NewCond(l Locker) *Cond
Broadcast
唤醒所有等待的 Wait
,建议在“更改条件”时锁定 c.L
,更改完毕再解锁
- func (*Cond) Signal
func (c *Cond) Signal()
Signal
唤醒一个等待的Wait
,建议在“更改条件”时锁定c.L
,更改完毕再解锁。
- func (*Cond) Wait
func (c *Cond) Wait()
Wait
会解锁c.L
并进入等待状态,在被唤醒时,会重新锁定c.L
使用示例:
package main
import (
"fmt"
"sync"
"time"
)
func main() {
m := sync.Mutex{}
m.Lock()
c := sync.NewCond(&m)
go func() {
m.Lock()
defer m.Unlock()
fmt.Println("3. goroutine is owner of lock")
time.Sleep(2 * time.Second)
c.Broadcast() //唤醒所有等待的 Wait
fmt.Println("4. goroutine will release lock soon (deffered Unlock)")
}()
fmt.Println("1. main goroutine is owner of lock")
time.Sleep(1 * time.Second)
fmt.Println("2. main goroutine is still lockek")
c.Wait()
m.Unlock()
fmt.Println("Done")
}
Map
并发安全的map
- type Map
type Map struct {
// contains filtered or unexported fields
}
- func (*Map) Delete
func (m *Map) Delete(key interface{})
删除一个键值。
- func (*Map) Load
func (m *Map) Load(key interface{}) (value interface{}, ok bool)
加载方法,也就是提供一个键key
,查找对应的值value
,如果不存在,通过ok
反映。
- func (*Map) LoadOrStore
func (m *Map) LoadOrStore(key, value interface{}) (actual interface{}, loaded bool)
LoadOrStore
返回键的现有值(如果存在)。否则,它将存储并返回给定的值。如果已加载该值,则加载的结果为true
,如果已存储,则为false
- func (*Map) Range
func (m *Map) Range(f func(key, value interface{}) bool)
for … range map
是内建的语言特性,所以没有办法使用for range
遍历sync.Map
, 但是可以使用它的Range
方法,通过回调的方式遍历。
- func (*Map) Store
func (m *Map) Store(key, value interface{})
更新或者新增一个entry
使用示例:
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var m sync.Map
for i := 0; i < 3; i++ {
go func(i int) {
for j := 0; ; j++ {
m.Store(i, j)
}
}(i)
}
for i := 0; i < 10; i++ {
m.Range(func(key, value interface{}) bool {
fmt.Printf("%d: %d\t", key, value)
return true
})
fmt.Println()
time.Sleep(time.Second)
}
}
Pool
Pool用于存储临时对象,它将使用完毕的对象存入对象池中,在需要的时候取出来重复使用,目的是为了避免重复创建相同的对象造成GC
负担过重。其中存放的临时对象随时可能被GC
回收掉(如果该对象不再被其它变量引用)。
从Pool
中取出对象时,如果Pool
中没有对象,将返回nil
,但是如果给Pool.New
字段指定了一个函数的话,Pool
将使用该函数创建一个新对象返回。
Pool
可以安全的在多个例程中并行使用,但Pool
并不适用于所有空闲对象,Pool
应该用来管理并发的例程共享的临时对象,而不应该管理短寿命对象中的临时对象,因为这种情况下内存不能很好的分配,这些短寿命对象应该自己实现空闲列表。
切记,Pool
在开始使用之后,不能再被复制。
- type Pool
type Pool struct {
// New optionally specifies a function to generate
// a value when Get would otherwise return nil.
// It may not be changed concurrently with calls to Get.
New func() interface{}
// contains filtered or unexported fields
}
- func (*Pool) Get
func (p *Pool) Get() interface{}
从临时对象池中取出对象
- func (*Pool) Put
func (p *Pool) Put(x interface{})
向临时对象池中存入对象
使用示例:
package main
import (
"fmt"
"sync"
)
func main() {
p := &sync.Pool{
New: func() interface{} {
return 0
},
}
a := p.Get().(int)
p.Put(100)
b := p.Get().(int)
fmt.Println(a, b)
}
原子操作
原子操作即是进行过程中不能被中断的操作。针对某个值的原子操作在被进行的过程中,CPU
绝不会再去进行其他的针对该值的操作。 为了实现这样的严谨性,原子操作仅会由一个独立的CPU
指令代表和完成。
在sync/atomic 中,提供了一些原子操作,包括加法(Add
)、比较并交换(Compare And Swap
,简称 CAS
)、加载(Load
)、存储(Store
)和交换(Swap
)。
- 加法操作
提供了32/64
位有符号与无符号加减操作
var i int64
atomic.AddInt64(&i, 1)
fmt.Println("i = i + 1 =", i)
atomic.AddInt64(&i, -1)
fmt.Println("i = i - 1 =", i
- 比较并交换
CAS: Compare And Swap
如果addr
和old
相同,就用new
代替addr
。
func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool)
例如:
var a int32 = 1
var b int32 = 2
var c int32 = 3
ok := atomic.CompareAndSwapInt32(&a, a, b)
fmt.Printf("ok = %v, a = %v, b = %v\n", ok, a, b)
ok = atomic.CompareAndSwapInt32(&a, c, b)
fmt.Printf("ok = %v, a = %v, b = %v, c=%v\n", ok, a, b, c)
输出:
ok = true, a = 2, b = 2
ok = false, a = 2, b = 2, c = 3
- 交换
不管旧值与新值是否相等,都会通过新值替换旧值,返回的值是旧值。
func SwapInt32(addr *int32, new int32) (old int32)
例如:
var x int32 = 1
var y int32 = 2
old := atomic.SwapInt32(&x, y)
fmt.Println(x, old)
输出:2 1
- 加载
当读取该指针指向的值时,CPU
不会执行任何其它针对此值的读写操作
func LoadInt32(addr *int32) (val int32)
例如:
var x1 int32 = 1
y1 := atomic.LoadInt32(&x)
fmt.Println("x1, y1:", x1, y1)
- 存储
加载逆向操作。
例如:
var xx int32 = 1
var yy int32 = 2
atomic.StoreInt32(&yy, atomic.LoadInt32(&xx))
fmt.Println(xx, yy)
- 原子类型
sync/atomic
中添加了一个新的类型Value
。 例如:
v := atomic.Value{}
v.Store(1)
fmt.Println(v.Load())
通道Channel
Go
语言的并发模型是CSP
,CSP 是 Communicating Sequential Process
的简称,中文可以叫做通信顺序进程,是一种并发编程模型,由Tony Hoare 于1977
年提出。
简单来说是实体之间通过发送消息进行通信,这里发送消息时使用的就是通道,或者叫Channel
。Goroutine
对应并发实体。
Go
语言中的通道(channel
)是一种特殊的类型。通道像一个传送带或者队列,总是遵循先入先出(First In First Out
)的规则,保证收发数据的顺序。
channel
是一种类型,一种引用类型。声明通道类型的格式如下:
var 变量 chan 元素类型
举几个例子:
var ch1 chan int // 声明一个传递整型的通道
var ch2 chan bool // 声明一个传递布尔型的通道
var ch3 chan []int // 声明一个传递int切片的通道
创建channel
通道是引用类型,通道类型的空值是nil
。
var ch chan int
fmt.Println(ch) // <nil>
声明的通道后需要使用make
函数初始化之后才能使用。
创建channel
的格式如下:
make(chan 元素类型, [缓冲大小])
channel
的缓冲大小是可选的。
举几个例子:
ch4 := make(chan int)
ch5 := make(chan bool)
ch6 := make(chan []int)
channel
操作
通道有发送(send
)、接收(receive
)和关闭(close
)三种操作。
发送和接收都使用<-
符号。
现在我们先使用以下语句定义一个通道:
ch := make(chan int)
- 发送:
将一个值发送到通道中。
ch <- 10 // 把10发送到ch中
- 接收:
从一个通道中接收值。
x := <- ch // 从ch中接收值并赋值给变量x
<-ch // 从ch中接收值,忽略结果
- 关闭:
通过调用内置的close
函数来关闭通道。
close(ch)
关于关闭通道需要注意的事情是,只有在通知接收方goroutine
所有的数据都发送完毕的时候才需要关闭通道。通道是可以被垃圾回收机制回收的,它和关闭文件是不一样的,在结束操作之后关闭文件是必须要做的,但关闭通道不是必须的。
关闭后的通道有以下特点:
1.对一个关闭的通道再发送值就会导致panic。
2.对一个关闭的通道进行接收会一直获取值直到通道为空。
3.对一个关闭的并且没有值的通道执行接收操作会得到对应类型的零值。
4.关闭一个已经关闭的通道会导致panic。
Channel
分类
- 无缓冲的
Channel
发送与接受同时进行。如果没有Goroutine
读取Channel(<-Channel)
,发送者(Channel<-x
)会一直阻塞。
func main() {
ch := make(chan int)
ch <- 10
fmt.Println("发送成功")
}
上面这段代码能够通过编译,但是执行的时候会出现以下错误:
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]:
main.main()
.../src/github.com/pprof/studygo/day06/channel02/main.go:8 +0x54
为什么会出现deadlock
错误呢?
因为我们使用ch := make(chan int)
创建的是无缓冲的通道,无缓冲的通道只有在有人接收值的时候才能发送值。就像你住的小区没有快递柜和代收点,快递员给你打电话必须要把这个物品送到你的手中,简单来说就是无缓冲的通道必须有接收才能发送。
上面的代码会阻塞在ch <- 10
这一行代码形成死锁,那如何解决这个问题呢?
一种方法是启用一个goroutine
去接收值,例如:
func recv(c chan int) {
ret := <-c
fmt.Println("接收成功", ret)
}
func main() {
ch := make(chan int)
go recv(ch) // 启用goroutine从通道接收值
ch <- 10
fmt.Println("发送成功")
}
无缓冲通道上的发送操作会阻塞,直到另一个goroutine
在该通道上执行接收操作,这时值才能发送成功,两个goroutine
将继续执行。相反,如果接收操作先执行,接收方的goroutine
将阻塞,直到另一个goroutine
在该通道上发送一个值。
使用无缓冲通道进行通信将导致发送和接收的goroutine
同步化。因此,无缓冲通道也被称为同步通道。
- 有缓冲的
Channel
解决上面问题的方法还有一种就是使用有缓冲区的通道。
发送与接受并非同时进行。当队列为空,接受者阻塞;队列满,发送者阻塞。
可以在使用make
函数初始化通道的时候为其指定通道的容量,例如:
func main() {
ch := make(chan int, 1) // 创建一个容量为1的有缓冲区通道
ch <- 10
fmt.Println("发送成功")
}
只要通道的容量大于零,那么该通道就是有缓冲的通道,通道的容量表示通道中能存放元素的数量。就像你小区的快递柜只有那么个多格子,格子满了就装不下了,就阻塞了,等到别人取走一个快递员就能往里面放一个。
我们可以使用内置的len
函数获取通道内元素的数量,使用cap
函数获取通道的容量,虽然我们很少会这么做。
Select
- 每个
case
都必须是一个通信 - 所有
channel
表达式都会被求值 - 如果没有
default
语句,select
将阻塞,直到某个通信可以运行 - 如果多个
case
都可以运行,select
会随机选择一个执行
随机选择
select
特性之一:随机选择,下面会随机打印不同的case
结果。 例如:
ch := make(chan int, 1)
ch <- 1
select {
case <-ch:
fmt.Println("ch 1")
case <-ch:
fmt.Println("ch 2")
default:
fmt.Println("ch default")
}
假设chan
中没有值,有可能引发死锁。
例如: 下面执行后会引发死锁。
ch := make(chan int, 1)
select {
case <-ch:
fmt.Println("ch 1")
case <-ch:
fmt.Println("ch 2")
}
此时可以加上default
即可解决。
default:
fmt.Println("ch default")
另外,还可以添加超时。
timeout := make(chan bool, 1)
go func() {
time.Sleep(2 * time.Second)
timeout <- true
}()
ch := make(chan int, 1)
select {
case <-ch:
fmt.Println("ch 1")
case <-timeout:
fmt.Println("timeout 1")
case <-time.After(time.Second * 1):
fmt.Println("timeout 2")
}
检查chan
select+defaul
方式来确保channel
是否满
ch := make(chan int, 1)
ch <- 1
select {
case ch <- 1:
fmt.Println("channel value is ", <-ch)
fmt.Println("channel value is ", <-ch)
default:
fmt.Println("channel blocking")
}
如果要调整channel
大小,可以在make
的时候改变size
,这样就可以在case
中往channel
继续写数据。
选择循环
当多个channel
需要读取数据的时候,就必须使用for+select
例如:下面例子需要从两个channel
中读取数据,当从channel1
中数据读取完毕后,会像signal channel
中输入stop
,此时终止for+select
。
func f1(c chan int, s chan string) {
for i := 0; i < 10; i++ {
time.Sleep(time.Second)
c <- i
}
s <- "stop"
}
func f2(c chan int, s chan string) {
for i := 20; i >= 0; i-- {
time.Sleep(time.Second)
c <- i
}
s <- "stop"
}
func main() {
c1 := make(chan int)
c2 := make(chan int)
signal := make(chan string, 10)
go f1(c1, signal)
go f2(c2, signal)
LOOP:
for {
select {
case data := <-c1:
fmt.Println("c1 data is ", data)
case data := <-c2:
fmt.Println("c2 data is ", data)
case data := <-signal:
fmt.Println("signal is ", data)
break LOOP
}
}
}
简单应用
通过sync实现单例
package singleton
import (
"sync"
)
type singleton struct {
}
var instance *singleton
var once sync.Once
func GetInstance() *singleton {
once.Do(func() {
instance = &singleton{}
})
return instance
}
访问多个url
package main
import (
"fmt"
"io/ioutil"
"log"
"net/http"
"sync"
)
func main() {
urls := []string{
"https://github.com",
"https://golang.org/",
"https://golang.org/doc/",
}
jsonResponses := make(chan string)
var wg sync.WaitGroup
wg.Add(len(urls))
for _, url := range urls {
go func(url string) {
defer wg.Done()
res, err := http.Get(url)
if err != nil {
log.Fatal(err)
} else {
defer res.Body.Close()
body, err := ioutil.ReadAll(res.Body)
if err != nil {
log.Fatal(err)
} else {
jsonResponses <- string(body)
}
}
}(url)
}
go func() {
for response := range jsonResponses {
fmt.Println(response)
}
}()
wg.Wait()
}