协程goroutine
一个 goroutine 本身就是一个函数,当你直接调用时,它就是一个普通函数,如果你在调用前加一个关键字 go ,那你就开启了一个 goroutine。
//执行一个函数
func()
//开启一个协程执行这个函数
go func()
main相当于主线程,goroutine要在主线程结束前执行完,否者协程总结
package main
import "fmt"
func main() {
go test()
fmt.Println("hello world!")
}
func test() {
fmt.Println("hello china")
}
创建协程需要时间,在协程创建完之前,主线程main已经执行完,协程还没来得及执行
多个协程同时执行
package main
import (
"fmt"
"time"
)
func main() {
go assistance("协程1")
go assistance("协程2")
go assistance("协程3")
time.Sleep(time.Second)
}
func assistance(name string) {
for i:=0;i<10;i++ {
fmt.Printf("goroutine %s\n",name)
}
time.Sleep(time.Millisecond * 10)
}
信道channel
就是一个管道,连接多个goroutine程序,它是一种队列式的数据结构,遵循先进先出规则
var 信道实例 chan 信道类型
声明后的信道,其零值式nil,无法直接使用,必须make进行初始化
信道实例 = make(chan 信道类型)
上面两句合并成一句
信道实例 := make(chan 信道类型)
信道数据操作
a := make(chan int)
a <- 100//发送数据
<-a//取出数据
close(a)//关闭信道,避免一直等待,关闭收接收方仍然可以从信道中取到数据,知识接收到的永远是0
在这里插入代码片
package main
import "fmt"
func main() {
//make函数接收两个参数
//第一个参数必填,指定信道类型
//第二个选填,不填默认为0,指定信道容量
a := make(chan int,5)
//信道容量
fmt.Printf("信道可缓冲 %d 个数据\n",cap(a))
a<-1
//信道长度
fmt.Printf("信道当中由 %d 个数据\n",len(a))
}
当容量为0时,说明信道中不能存放数据,在发送数据时,必须要求立马有人接收,否则会报错。此时的信道称之为无缓冲信道。
当容量为1时,说明信道只能缓存一个数据,若信道中已有一个数据,此时再往里发送数据,会造成程序阻塞。 利用这点可以利用信道来做锁。
当容量大于1时,信道中可以存放多个数据,可以用于多个协程之间的通信管道,共享资源。
信道分为双向和单项,默认是双向
import (
"fmt"
"time"
)
func main() {
pipline := make(chan int)
go func() {
fmt.Println("准备发送数据: 100")
pipline <- 100
}()
go func() {
num := <-pipline
fmt.Printf("接收到的数据是: %d", num)
}()
// 主函数sleep,使得上面两个goroutine有机会执行
time.Sleep(time.Second)
}
//单信道可细分为只读信道和只写信道
//只读信道
var a = make(chan int)
type receice = <-chan int
var receiver receice = a
//只写信道
var b = make(chan int)
type write = chan<- int
var writer write = b
几个注意事项
- 关闭一个未初始化的 channel 会产生 panic
- 重复关闭同一个 channel 会产生 panic
- 向一个已关闭的 channel 发送消息会产生 panic
- 从已关闭的 channel 读取消息不会产生 panic,且能读出 channel 中还未被读取的消息,若消息均已被读取,则会读取到该类型的零值。
- 从已关闭的 channel 读取消息永远不会阻塞,并且会返回一个为 false 的值,用以判断该 channel 是否已关闭(x,ok := <- ch)
- 关闭 channel 会产生一个广播机制,所有向 channel 读取消息的 goroutine 都会收到消息
- channel 在 Golang 中是一等公民,它是线程安全的,面对并发问题,应首先想到 channel。
WaitGroup
var 实例名 sync.WaitGroup
实例化完成后,可以使用它 的几种方法
- add:初始值为0,你传入的值会往计数器上加,这里直接传入你子协程的数量
- Done:当某个子协程完成后,可调用此方法,会从计数器上减一,通常可以使用 defer 来调用。
- Wait:阻塞当前协程,直到实例里的计数器归零。
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
wg.Add(2)
go test(&wg)
go test(&wg)
fmt.Println("end")
wg.Wait()
}
func test(wg *sync.WaitGroup) {
defer wg.Done()
for i:=0;i<10;i++ {
fmt.Println(i)
}
}
互斥锁与读写锁
Mutex:互斥锁
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
wg.Add(3)
lock := &sync.Mutex{}
count := 0
go test(&count,&wg,lock)
go test(&count,&wg,lock)
go test(&count,&wg,lock)
fmt.Println("end")
wg.Wait()
fmt.Println("count:",count)
}
func test(count *int,wg *sync.WaitGroup,lock *sync.Mutex) {
defer wg.Done()
for i:=0;i<1000;i++ {
lock.Lock()
*count = *count + 1
lock.Unlock()
}
}
使用互斥锁注意点
- 同一协程里,不要在尚未解锁时再次使加锁
- 同一协程里,不要对已解锁的锁再次解锁
- 加了锁后,别忘了解锁,必要时使用 defer 语句
RWMutex:读写锁
它将程序对资源的访问分为读操作和写操作
- 为了保证数据的安全,它规定了当有人还在读取数据(即读锁占用)时,不允计有人更新这个数据(即写锁会阻塞)
- 为了保证程序的效率,多个人(线程)读取数据(拥有读锁)时,互不影响不会造成阻塞,它不会像 Mutex 那样只允许有一个人(线程)读取同一个数据。
RWMutex 里提供了两种锁,每种锁分别对应两个方法,为了避免死锁,两个方法应成对出现,必要时请使用 defer。
读锁:调用 RLock 方法开启锁,调用 RUnlock 释放锁
写锁:调用 Lock 方法开启锁,调用 Unlock 释放锁(和 Mutex类似)
package main
import (
"fmt"
"sync"
"time"
)
func main() {
lock := &sync.RWMutex{}
lock.Lock()
for a := 0; a < 4; a++ {
go func(a int) {
lock.RLock()
fmt.Printf("协程 %d 获得读锁\n",a)
time.Sleep(time.Second * 3)
lock.RUnlock()
}(a)
}
time.Sleep(time.Second * 2)
fmt.Println("协程在3秒后释放读锁,将不会再堵塞")
lock.Unlock()
// 由于会等到读锁全部释放,才能获得写锁
// 因为这里一定会在上面 4 个协程全部完成才能往下走
lock.Lock()
fmt.Println("程序退出...")
lock.Unlock()
}
死锁的情况
package main
import "fmt"
func main() {
pipline := make(chan string)
pipline <- "hello world"
fmt.Println(<-pipline)
}
解决此问题有两种方法:
使接收者代码在发送者之前执行
使用缓冲信道,而不使用无缓冲信道
package main
func hello(pipline chan string) {
<-pipline
}
func main() {
pipline := make(chan string)
go hello(pipline)
pipline <- "hello world"
}
这样子还是报错,虽然保证接收代码再发送者之前,
但是由于前面接收者一直再等待数据而处于阻塞状
态,所以无法执行到后面的发送数据,造成死锁,
慎用无缓冲区信道
package main
import "fmt"
func main() {
ch1 := make(chan string, 1)
ch1 <- "hello world"
ch1 <- "hello China"
fmt.Println(<-ch1)
}
每个缓冲信道都有容量,当数据量等于容量后,
再往信道发送数据,就会造成阻塞,
必须等有人从信道中消费数据后,程序才会往下执行
package main
import "fmt"
func main() {
pipline := make(chan string)
go func() {
pipline <- "hello world"
pipline <- "hello China"
// close(pipline)
}()
for data := range pipline{
fmt.Println(data)
}
}
当程序一直在等待从信道里读取数据,
而此时并没有人会往信道中写入数据。
此时程序就会陷入死循环,造成死锁。
只要在发送完数据后,手动关闭信道,
告诉 range 信道已经关闭,无需等待就行。
协程池
池化技术就是利用复用来提升性能的
为了减少线程频繁创建销毁还来的开销,通常我们会使用线程池来复用线程。
package main
import (
"fmt"
"time"
)
type Pool struct {
work chan func()//用于接收task任务
sem chan struct{}//设置协程池大小,可同时执行的协程数量
}
//创建一个协程池对象
func New(size int) *Pool {
return &Pool{
work: make(chan func()),//无缓冲通道
sem: make(chan struct{},size),//缓冲通道
}
}
//当第一次调用 NewTask 添加任务的时候,
//由于 work 是无缓冲通道,所以一定会
//走第二个 case 的分支:使用 go worker 开启一个协程。
func (p *Pool) NewTask(task func()) {
select{
case p.work<-task:
case p.sem <- struct{}{}:
go p.worker(task)
}
}
//如果传入的任务数大于设定的协程池数,
//并且此时所有的任务都还在运行中,
//那此时再调用 NewTask 传入 task ,
//这两个 case 都不会命中,会一直阻塞
//直到有任务执行完成,worker 函数里的
//work 通道才能接收到新的任务,继续执行。
func (p *Pool) worker(task func()) {
defer func() { <-p.sem}()
for{
task()
task = <- p.work
}
}
func main() {
pool := New(2)
for a := 1;a<5;a++ {
pool.NewTask(func() {
time.Sleep(time.Second * 2)
fmt.Println(time.Now())
})
}
time.Sleep(time.Second *5)
}
2021-09-12 10:41:00.299706 +0800 CST m=+2.018351501
2021-09-12 10:41:00.299706 +0800 CST m=+2.018351501
2021-09-12 10:41:02.3198102 +0800 CST m=+4.038455701
2021-09-12 10:41:02.3198102 +0800 CST m=+4.038455701
可以看到总共 4 个任务,由于协程池大小为 2,所以 4 个任务分两批执行