协程
进程, 线程, 协程概念
-
进程:
- 概念: 资源分配的基本单位
- 通信: 进程之间的通信只能通过进程通信的方式进行
- 多进程: 拷贝,使用fork(),生成子进程。每个进程拥有独立的地址空间(代码段、堆栈段、数据段)
-
线程:
- 概念: 调度运行的最小单位
- 通信: 同一进程中的线程共享数据(比如全局变量,静态变量)
- 多线程: 同一个进程中的线程,它们之间共享大部分数据,使用相同的地址空间。当然线程是拥有自己的局部变量和堆栈(注意不是堆)
-
协程:
-
概念: 非抢占式调度。用户态模拟进程线程的切换的具体实现,并非OS内核提供的功能。由程序员主动控制协程之间的切换。
-
通信: 不要通过共享内存来通信,而应该通过通信来共享内存。
golang提供一种基于消息机制而非共享内存的通信模型。消息机制认为每个并发单元都是自包含的独立个体,并且拥有自己的变量,但在不同并发单元间这些变量不共享。每个并发单元的输入和输出只有一种,那就是消息。
-
GO协程
goroutine(go协程)是由Go runtime管理的轻量级线程。
说明协程是用户态, 由Go runtime管理而非OS内核管理
例子:
package main
import (
"fmt"
"time"
)
func say(s string) {
for i := 0; i < 5; i++ {
time.Sleep(100 * time.Millisecond)
fmt.Println(s)
}
}
func main() {
go say("world") //启动go routine
say("hello")
}
channel
概述
要想理解 channel 要先知道 CSP 模型。CSP 是 Communicating Sequential Process 的简称,中文可以叫做通信顺序进程,是一种并发编程模型,由 Tony Hoare 于 1977 年提出。简单来说,CSP 模型由并发执行的实体(线程或者进程)所组成,实体之间通过发送消息进行通信,这里发送消息时使用的就是通道,或者叫 channel。CSP 模型的关键是关注 channel,而不关注发送消息的实体。Go 语言实现了 CSP 部分理论,goroutine 对应 CSP 中并发执行的实体,channel 也就对应着 CSP 中的 channel。
Channel是Go中的一个核心类型,你可以把它看成一个管道,通过它并发核心单元就可以发送或者接收数据进行通讯(communication)。
channel基础知识
-
unBufferChan := make(chan int) //1 bufferChan := make(chan int, N) //2
-
上面的方式 1 创建的是无缓冲 channel,方式 2 创建的是缓冲 channel。如果使用 channel 之前没有 make,会出现 dead lock 错误。至于为什么是 dead lock,下文我们从源码里面看看。
-
func main() { var x chan int go func() { x <- 1 }() <-x }
-
$ go run channel1.go fatal error: all goroutines are asleep - deadlock! goroutine 1 [chan receive (nil chan)]: main.main() /Users/kltao/code/go/examples/channl/channel1.go:11 +0x60 goroutine 4 [chan send (nil chan)]: main.main.func1(0x0)
-
-
channel读写操作
-
ch := make(chan int, 10) // 读操作 x <- ch // 写操作 ch <- x
-
-
channel种类
-
channel 分为无缓冲 channel 和有缓冲 channel。两者的区别如下:
- 无缓冲:发送和接收动作是同时发生的。如果没有 goroutine 读取 channel (<- channel),则发送者 (channel <-) 会一直阻塞。
- 缓冲:缓冲 channel 类似一个有容量的队列。当队列满的时候发送者会阻塞;当队列空的时候接收者会阻塞。
-
-
关闭channel
-
ch := make(chan int) // 关闭 close(ch) // ok-idiom 用于区分channel中是默认值还是channel关闭了 val, ok := <-ch if ok == false { // channel closed }
-
关闭时要注意:
- 重复关闭 channel 会导致 panic。
- 向关闭的 channel 发送数据会 panic。
- 从关闭的 channel 读数据不会 panic,读出 channel 中已有的数据之后再读就是 channel 类似的默认值,比如 chan int 类型的 channel 关闭之后读取到的值为 0。
-
channel典型用法
-
goroutine通信
-
func main() { x := make(chan int) go func() { x <- 1 }() <-x }
-
-
select
-
select 一定程度上可以类比于 linux 中的 IO 多路复用中的 select。后者相当于提供了对多个 IO 事件的统一管理,而 Golang 中的 select 相当于提供了对多个 channel 的统一管理。当然这只是 select 在 channel 上的一种使用方法。
-
select { case e, ok := <-ch1: ... case e, ok := <-ch2: ... default: }
-
select 会阻塞,直到条件分支中的某个可以继续执行,这时就会执行那个条件分支。当多个都准备好的时候,会随机选择一个。
-
func receive(ch chan int) { for { <-ch } } func send(ch1, ch2, ch3 chan int) { for i := 0; i < 10; i++ { // sleep是为了保证所有的管道receiver都已阻塞等待数据 time.Sleep(1000 * time.Millisecond) select { case ch1 <- i: fmt.Printf("send %d to ch1\n", i) case ch2 <- i: fmt.Printf("send %d to ch2\n", i) case ch3 <- i: fmt.Printf("send %d to ch3\n", i) } } } func main() { ch1 := make(chan int) ch2 := make(chan int) ch3 := make(chan int) go receive(ch1) go receive(ch2) go receive(ch3) send(ch1, ch2, ch3) } //每次结果不一样
-
-
range channel
-
range channel 可以直接取到 channel 中的值。当我们使用 range 来操作 channel 的时候,一旦 channel 关闭,channel 内部数据读完之后循环自动结束。
-
func consumer(ch chan int) { //消费者 for x := range ch { fmt.Println(x) ... } } func producer(ch chan int) { //生产者 for _, v := range values { ch <- v } }
-
-
超时控制
-
select { case <- ch: // get data from ch case <- time.After(2 * time.Second) // read data from ch timeout } //timeAfter可以换成其他任何异常控制流
-
-
生产者-消费者模型, 如第三条显示
-
单向channel
-
单向 channel,顾名思义只能写或读的 channel。但是仔细一想,只能写的 channel,如果不读其中的值有什么用呢?其实单向 channel 主要用在函数声明中。
-
func send(c chan<- int) { fmt.Printf("send: %T\n", c) c <- 1 } func recv(c <-chan int) { fmt.Printf("recv: %T\n", c) fmt.Println(<-c) } func main() { c := make(chan int) fmt.Printf("%T\n", c) go send(c) go recv(c) time.Sleep(1 * time.Second) } /** * output: * chan int * send: chan<- int * recv: <-chan int * 1 */
-
同步(sync)
互斥锁
-
概述:用于主动控制Mutex类型的变量或者将Mutex类型作为struct的元素的变量在同一时间只被一个routine访问(即执行Lock()方法的代码块),这个Mutex带有2个方法:Lock()和Unlock()。互斥锁不区分读和写,即无论是print打印还是写操作都是互斥的
-
func main() { var mutex sync.Mutex fmt.Printf("%+v\n", mutex) mutex.Lock() fmt.Printf("%+v\n", mutex) mutex.Unlock() fmt.Printf("%+v\n", mutex) }
-
-
使用
// SafeCounter is safe to use concurrently.
type SafeCounter struct {
v map[string]int
mux sync.Mutex
}// Inc increments the counter for the given key.
func (c *SafeCounter) Inc(key string, id int) {
c.mux.Lock()
fmt.Printf("%d. Inc lock.\n", id)
// Lock so only one goroutine at a time can access the map c.v.
c.v[key]++
c.mux.Unlock()
fmt.Printf("%d. Inc unlock.\n", id)
}// Value returns the current value of the counter for the given key.
func (c *SafeCounter) Value(key string) int {
c.mux.Lock()
fmt.Println(“Value lock.”)
// Lock so only one goroutine at a time can access the map c.v.
defer fmt.Println(“Value unlock.”)
defer c.mux.Unlock()
return c.v[key]
}func main() {
c := SafeCounter{v: make(map[string]int)}
for i := 0; i < 10; i++ {
go c.Inc(“somekey”, i)
}time.Sleep(time.Second) fmt.Println(c.Value("somekey"))
}
-
-
已经锁定的Mutex与特定的goroutine无关联
-
已经锁定的Mutex并不与特定的goroutine相关联,这样可以利用一个goroutine对其加锁,再利用其他goroutine对其解锁
-
package main
import (
“fmt”
“sync”
“time”
)type MyStruct struct {
v int
mux sync.Mutex
}func (s *MyStruct) Lock() {
s.mux.Lock()
}func (s *MyStruct) Unlock() {
s.mux.Unlock()
}func main() {
s := MyStruct{v: 0}
s.v = 1
fmt.Printf("%+v\n", s)go s.Lock() time.Sleep(1 * time.Second) fmt.Printf("%+v\n", s) go s.Unlock() time.Sleep(1 * time.Second) fmt.Printf("%+v\n", s)
}
/*
{v:1 mux:{state:0 sema:0}}
{v:1 mux:{state:1 sema:0}}
{v:1 mux:{state:0 sema:0}}
*/3. 虽然互斥锁可以被直接的在多个Goroutine之间共享,但是我们还是强烈建议把对同一个互斥锁的成对的锁定和解锁操作放在同一个层次的代码块中。例如,在同一个函数或方法中对某个互斥锁的进行锁定和解锁。
-
读写锁
-
概述: 读写锁是针对于读写操作的互斥锁。它与普通的互斥锁最大的不同就是,它可以分别针对读操作和写操作进行锁定和解锁操作。
- 注意点:
- 同时只能有一个 goroutine 能够获得写锁定。
- 同时可以有任意多个 gorouinte 获得读锁定。
- 同时只能存在写锁定或读锁定(读和写互斥)。
- 注意点:
-
方法:
-
func (rw *RWMutex) Lock //写锁定 func (rw *RWMutex) Unlock //写解锁 func (rw *RWMutex) RLock //读锁定 func (rw *RWMutex) RUnlock //读解锁 //都实现了Locker接口 type Locker interface { Lock() Unlock() } //还有一个RLocker方法 func (rw *RWMutex) RLocker() Locker //返回实现了sync.Locker接口的值
-
这个RLocker()作用是,使用Lock()和Unlock()来进行读锁定和读解锁,而无需RLock()和RUnlock()来进行读锁定和读解锁
-
WaitGroup
WaitGroup用于等待一组goroutine结束, 有三个方法
func (wg *WaitGroup) Add(delta int)
func (wg *WaitGroup) Done()
func (wg *WaitGroup) Wait()
- Add 用来添加 goroutine 的个数
- Done 执行一次数量减 1
- Wait 用来等待结束
例子
func main() {
var wg sync.WaitGroup
fmt.Printf("init: %+v\n", wg)
for i := 1; i < 10; i++ {
// 计数加 1
wg.Add(1)
go func(i int) {
fmt.Printf("goroutine%d start: %+v\n", i, wg)
time.Sleep(11 * time.Second)
// 计数减 1
wg.Done()
fmt.Printf("goroutine%d end: %+v\n", i, wg)
}(i)
time.Sleep(time.Second)
}
// 等待执行结束
wg.Wait()
fmt.Printf("over: %+v\n", wg)
}
注意: wg.Add() 方法一定要在 goroutine 开始前执行
条件变量(cond)
与互斥量不同,条件变量的作用并不是保证在同一时刻仅有一个线程访问某一个共享数据,而是在对应的共享数据的状态发生变化时,通知其他因此而被阻塞的线程。条件变量总是与互斥量组合使用。互斥量为共享数据的访问提供互斥支持,而条件变量可以就共享数据的状态的变化向相关线程发出通知。
//声明
lock := new(sync.Mutex)
cond := sync.NewCond(lock)
//或者
cond := sync.NewCond(new(synv.Mutex))
方法
cond.L.Lock()
cond.L.Unlock() 也可以使用lock.Lock()和lock.Unlock(),完全一样,因为是指针转递
cond.Wait(): Unlock()->阻塞等待通知(即等待Signal()或Broadcast()的通知)->收到通知->Lock()
cond.Signal() : 通知一个Wait()了的,若没有Wait(),也不会报错。Signal()通知的顺序是根据原来加入通知列表(Wait())的先入先出
cond.Broadcast(): 通知所有Wait()了的,若没有Wait(),也不会报错
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pkpnhYul-1575184984806)(/home/eugeo/文档/学习笔记/Java-golang-learning/go/协程.assets/cond_4.png)]
示例代码
func main() {
cond := sync.NewCond(new(sync.Mutex))
condition := 0
// Consumer
go func() {
for {
cond.L.Lock()
for condition == 0 {
cond.Wait()
}
condition--
fmt.Printf("Consumer: %d\n", condition)
cond.Signal()
cond.L.Unlock()
}
}()
// Producer
for {
time.Sleep(time.Second)
cond.L.Lock()
for condition == 3 {
cond.Wait()
}
condition++
fmt.Printf("Producer: %d\n", condition)
cond.Signal()
cond.L.Unlock()
}
}
输出:
Producer: 1
Consumer: 0
Producer: 1
Consumer: 0
Producer: 1
Consumer: 0
Producer: 1
Consumer: 0
Producer: 1
Consumer: 0
该例子仅适用于单消费者和单生产者, 同时对condition的判断只有0和1这种布尔值状态
实际使用, 应该先channel再锁
func main() {
ch := make(chan int, 3)
v := 0
// Consumer
go func() {
for {
fmt.Printf("Consumer: %d\n", <-ch)
}
}()
// Producer
for {
v++
fmt.Printf("Producer: %d\n", v)
ch <- v
time.Sleep(time.Second)
}
}
临时对象池
堆和栈
程序会从操作系统申请一块内存,而这块内存也会被分成堆和栈。
func F() {
temp := make([]int, 0, 20) //临时变量将申请到栈上
...
}
栈可以简单得理解成一次函数调用内部申请到的内存,它们会随着函数的返回把内存还给系统。申请到栈内存好处:函数返回直接释放,不会引起垃圾回收,对性能没有影响。
func F() []int{
a := make([]int, 0, 20)
return a
}
而上面这段代码,申请的代码一模一样,但是申请后作为返回值返回了,编译器会认为变量之后还会被使用,当函数返回之后并不会将其内存归还,那么它就会被申请到堆上面了。申请到堆上面的内存才会引起垃圾回收。
func F() {
a := make([]int, 0, 20)
b := make([]int, 0, 20000)
l := 20
c := make([]int, 0, l)
}
a和b代码一样,就是申请的空间不一样大,但是它们两个的命运是截然相反的。a前面已经介绍过,会申请到栈上面,而b,由于申请的内存较大,编译器会把这种申请内存较大的变量转移到堆上面。即使是临时变量,申请过大也会在堆上面申请。
而c,对我们而言其含义和a是一致的,但是编译器对于这种不定长度的申请方式,也会在堆上面申请,即使申请的长度很短。
在项目中一般都是c用法而申请内存变成了慢语句,解决方法就是使用临时对象池
临时对象池例子:
// 一个[]byte的对象池,每个对象为一个[]byte
var bytePool = sync.Pool{
New: func() interface{} {
b := make([]byte, 1024)
return &b
},
}
func main() {
a := time.Now().Unix()
// 不使用对象池
for i := 0; i < 1000000000; i++ {
obj := make([]byte, 1024)
_ = obj
}
b := time.Now().Unix()
// 使用对象池
for i := 0; i < 1000000000; i++ {
obj := bytePool.Get().(*[]byte)
bytePool.Put(obj)
}
c := time.Now().Unix()
fmt.Println("without pool ", b-a, "s") //20s
fmt.Println("with pool ", c-b, "s") //15s
只有当每个对象占用内存较大时候,用pool才会改善性能
- 当每个对象的内存小于一定量的时候,不使用pool的性能秒杀使用pool;当内存处于某个量的时候,不使用pool和使用pool性能相当;当内存大于某个量的时候,使用pool的优势就显现出来了
- 不使用pool,那么对象占用内存越大,性能下降越厉害;使用pool,无论对象占用内存大还是小,性能都保持不变。可以看到pool有点像飞机,虽然起步比跑车慢,但后劲十足。
即:pool适合占用内存大且并发量大的场景。当内存小并发量少的时候,使用pool适得其反
使用场景
sync.Pool一种合适的方法是,为临时缓冲区创建一个池,多个客户端使用这个缓冲区来共享全局资源。另一方面,如果释放链表是某个对象的一部分,并由这个对象维护,而这个对象只由一个客户端使用,在这个客户端工作完成后释放链表,那么用Pool实现这个释放链表是不合适的。
在Put之前重置,在Get之后重置
bytePool.Put(obj)
}
c := time.Now().Unix()
fmt.Println("without pool ", b-a, "s") //20s
fmt.Println("with pool ", c-b, "s") //15s
**只有当每个对象占用内存较大时候,用pool才会改善性能**
> 1. 当每个对象的内存小于一定量的时候,不使用pool的性能秒杀使用pool;当内存处于某个量的时候,不使用pool和使用pool性能相当;当内存大于某个量的时候,使用pool的优势就显现出来了
> 2. 不使用pool,那么对象占用内存越大,性能下降越厉害;使用pool,无论对象占用内存大还是小,性能都保持不变。可以看到pool有点像飞机,虽然起步比跑车慢,但后劲十足。
>
> 即:pool适合占用内存大且并发量大的场景。当内存小并发量少的时候,使用pool适得其反
##### 使用场景
sync.Pool一种合适的方法是,为临时缓冲区创建一个池,多个客户端使用这个缓冲区来共享全局资源。另一方面,如果释放链表是某个对象的一部分,并由这个对象维护,而这个对象只由一个客户端使用,在这个客户端工作完成后释放链表,那么用Pool实现这个释放链表是不合适的。
在Put之前重置,在Get之后重置