并发编程
一、并发与并行区别
并发:两队人排队接一个咖啡机的咖啡
并行:两队人同时接两个咖啡机的咖啡
并发使并行变得容易,并发提供了一种构造解决方案的方法,并行一般伴随着多核。并发一般伴随这CPU切换轮训。
二、为什么需要并发
1.不阻塞、等待其他任务的执行,从而浪费时间,影响系统性能
2.并发可以使得系统变得简单,将复杂的大任务切换成许多小任务执行,单独测试。
Q:在开发中,经常会遇到为什么某些进程通常会相互等待呢?为什么有些运行慢,有些快呢?
A: 通常受限来源于进程I/O限制(eg:等待网络或磁盘访问)或CPU限制(eg:大量计算)
三、Go并发原语
1.协程
每个go程序至少都有一个Goroutine:主Goroutine(在运行进程时自动创建)。以及程序中其他Goroutine 例如:下面程序创建了main的Goroutine及匿名的Goroutine。
func main() {
go func() {
fmt.Println("you forgot me !")
}()
}
在go中有个package是sync,里面包含了:WaitGroup、Mutex、Cond、Once、Pool,下面依次介绍:
1.waitgroup
假设主线程要等待其余的goroutine都运行完毕,不得不在末尾添加time.Sleep(),但是这样会引发两个问题:①等待多长时间? ②时间太长,影响性能?
在go的sync库中的WaitGroup可以帮助我们完成此项工作,Add(n)把计数器设置为n,Done()会将计数器每次减1,Wait()函数会阻塞代码运行,直到计数器减0。
等待多个goroutine完成,可以使用一个等待组。 例如:
代码如下(示例):
// 这是我们将在每个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()
}
2.Once
sync.Once可以控制函数只能被调用一次,不能多次重复调用。
3.互斥锁Mutex
互斥锁是并发程序对共享资源进行访问控制的主要手段,在go中的sync中提供了Mutex的支持。
例如:使用互斥锁解决多个Goroutine访问同一变量。
// 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"))
}
4.条件变量Cond
sync.Cond是条件变量,它可以让一系列的 Goroutine 都在满足特定条件时被唤醒。
条件变量通常与互斥锁一起使用,条件变量可以在共享资源的状态变化时通知相关协程。 经常使用的函数如下:
创建条件变量:func NewCond(l Locker) *Cond
广播通知:func (c *Cond) Broadcast()
单播通知:func (c *Cond) Signal()
只唤醒一个等待c的goroutine
等待通知:func (c *Cond) Wait()
除非被signal或者broadcast唤醒,否则wait不会返回
5.原子操作
原子操作即是进行过程中不能被中断的操作。针对某个值的原子操作在被进行的过程中,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
②.比较与交换
func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool)//如果addr和old相同就用new代替addr
③.交换:不管旧值与新值是否相等,都会通过新值替换旧值,返回的值是旧值。
func SwapInt32(addr *int32, new int32) (old int32)
③.加载:当读取该指针指向的值时,CPU不会执行任何其他针对此值的读写操作
func LoadInt32(addr *int32) (val int32)
④.存储:加载逆向操作
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())
⑥.临时对象池pool
sync.Pool 可以作为临时对象的保存和复用的集合
P是Goroutine中的重要组成之一,例如:P实际上在操作时会为它的每一个goroutine相关的P生成一个本地P。 本地池没有,则会从其它的 P 本地池获取,或者全局P取。
sync.Pool对于需要重复分配、回收内存的地方,sync.Pool 是一个很好的选择。减少GC负担,如果Pool中有对象,下次直接取,不断服用对象内存,减轻 GC 的压力,提升系统的性能。
var pool *sync.Pool
type Foo struct {
Name string
}
func Init() {
pool = &sync.Pool{
New: func() interface{} {
return new(Foo)
},
}
}
func main() {
fmt.Println("Init p")
Init()
p := pool.Get().(*Foo)
fmt.Println("第一次取:", p)
p.Name = "bob"
pool.Put(p)
fmt.Println("池子有对象了,调用获取", pool.Get().(*Foo))
fmt.Println("池子空了", pool.Get().(*Foo))
}
2.通道channel
①.channel
CSP 是 Communicating Sequential Process 的简称,中文可以叫做通信顺序进程,是一种并发编程模型
简单来说是实体之间通过发送消息进行通信,这里发送消息时使用的就是通道,或者叫 Channel。Goroutine对应并发实体。
1,使用:通过Mark创建
unBufferChan := make(chan int) //无缓冲的channel
bufferChan := make(chan int, x) //有缓冲的channel
创建完成后需要进行读写操作
ch := make(chan int, 1)
// 读操作
x <- ch
// 写操作
ch <- x
//关闭
close(ch)
2.Channel分类:
无缓冲的channel:发送与接收同时进行,如果没有Goroutine读取Channel(<-Channel),发送者(Channel<-x)会一直阻塞。
有缓冲的channel:发送与接受并非同时进行。当队列为空,接受者阻塞;队列满,发送者阻塞。
②.select
- 每个case 都必须是一个通信
- 所有channel表达式都会被求值
- 如果没有default语句,select将阻塞,直到某个通信可以运行
- 如果多个case都可以运行,select会随机选择一个执行
1.随机选择
2.检查chan:select+defaul方式来确保channel是否满
3.选择循环:多个channel需要读取数据时,必须使用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
}
}
}