1、goroutine
协程也叫轻量级线程,为什么说是一个轻量级的线程呢?协程可以轻松创建上百万个而不会导致系统资源衰竭,而线程和进程通常不能超过1万个。在Go语言提供所有系统调用操作,都会出让CPU给其他goroutine,让轻量级线程的切换管理不依赖于系统的线程和进程,也不依赖CPU的核心数量。
goroutine是Go语言轻量级线程实现,由Go运行时(runtime)管理的。
栗子:假设我们需要实现一个函数sum(),它把两个参数相加,并且结果打印,代码如下:
func sum(x, y int) {
z := x + y
fmt.Println(z)
}
那么我们如何让这个函数并发执行呢?非常简单,代码如下:
go sum(1,1)
在一个函数调哦那个前面加上go关键字,这次调用就会在一个新的goroutine中并发执行。如果调用返回时,那么goroutine也自动结束了。(如该函数有返回值,那么这个返回值会被丢弃)
回到之前sum函数的栗子,代码如下:
func sum(x, y int) {
z := x + y
fmt.Println(z)
}
func main() {
for i := 0; i < 10; i++ {
go sum(i,i)
}
}
上面代码,我们使用for循环中调用10次sum()函数,它们是并发执行,但是发现运行后,控制台啥也没有输出?这是为什么呢?要解释这个现象,我们需要了解Go语言程序执行机制。
Go程序从初始化main package并执行main()函数开始,当main()函数返回时,程序退出, 且程序并不等待其他goroutine(非主goroutine)结束。
对于上面的例子,主函数启动了10个goroutine,然后返回,这时程序就退出了,而被启动的执行sum(i, i)的goroutine没有来得及执行,所以程序没有任何输出。
相信大家问题找到了,那么如何解决?
相信大家想到使用sleep循环等待来所有线程执行完毕,但是Go语言有自己的推荐方式。我们要让主函数等待所有goroutine退出后再返回,但是不知道goroutine啥时候退出,这时需要了解goroutine之间通信问题。
栗子1:
package main
import "fmt"
func demo(count int) {
for i := 0; i < 100; i++ {
fmt.Println(count,":",i)
}
}
func main() {
for i := 0; i < 5; i++ {
go demo(i)
}
}
运行后,发现什么也不输出,为什么?解释这个现象是需要我们了解Go语言的程序执行机制。
首先Go程序从main方法执行完,就直接退出,它是不会等待goroutine结束,那么我们应该给它一个休眠等待也可以。
协程Coroutine:
- 是一种轻量级“线程”
- 最重要的是非抢占式多任务处理,有协程主动交出控制权。(非抢占就是正在执行的不允许中断)
- 编译器/解析器/虚拟机层面的多任务
- 多个协程肯在一个多个线程上运行
func main() {
var a [10]int
for i := 0; i < 10; i++ {
go func(i int) {//匿名函数
for {
a[i]++
runtime.Gosched()
}
}(i)
}
time.Sleep(time.Millisecond)
fmt.Println(a)
}
goroutine可能的切换点
- I/O,select
- channel
- 等待锁
- 函数调用(有时)
- runtime.Gosched()
- 只是参考,不能保证切换,不能保证在其他地方不切换
面试题:
- Go协程,Goroutine阻塞
- Go map底层,扩容机制
- new 和 make的区别
- 有缓存和无缓存channel的区别
- 2个协程交替打印字母和数字
2、WaitGroup简介
Go中sync包提供基本同步基元,如互斥锁等,除了Once和WaitGroup类型,大部分只适用于低水平程序线程,高水平同步线程使用channel通信会更好。
WaitGroup翻译为等待组,其实就是计数器,只要计数器中有内容将一直阻塞。
Go语言标准库中WaitGroup只有三个方法
- Add(delta int)表示向内部计数器添加增量(delta)其中参数delta可以是负数
- Done()表示减少WaitGroup计数器的值,应当在程序最后执行,相当于Add(-1)
- Wait()表示阻塞直到WaitGroup计数器为
栗子:互斥锁使用
func demo(count int) {
for i := 0; i < 10; i++ {
fmt.Println(count,":",i)
}
}
func main() {
var wg sync.WaitGroup
wg.Add(5)
for i := 0; i < 5; i++ {
go demo(i)
wg.Done()
}
//time.Sleep(time.Millisecond)//不使用
wg.Wait()
fmt.Println("ok")
}
3、互斥锁和读写锁
在Go语言中,当多个协程操作一个变量时可能会出现冲突问题,也许会导致程序出异常也许不会,我们可以使用go run -race
查看是否有竞争。
任何一个新问题出现,我们会想如何解决?
使用sync.Mutex
对内容加锁
栗子:互斥锁使用
var (
num = 100
wg sync.WaitGroup
m sync.Mutex
)
func demo1() {
m.Lock()
for i := 0; i < 10; i++ {
num = num - i
}
m.Unlock()
wg.Done()
}
func main() {
wg.Add(10)
for i := 0; i < 10; i++ {
go demo1()
}
wg.Wait()
fmt.Println(num)
fmt.Println("ok")
}
读写锁(RWMutex)
先看Go语言标准库中的API
type{
func (rw *RWMutex) Lock() //禁止其他协程读
func (rw *RWMutex) Unlock()
func (rw *RWMutex) RLock() //禁止其他协程写入,只能读取
func (rw *RWMutex) RUnlock()
func (rw *RWMutex) RLocker()
}
互斥锁的锁事同一时间只能一个goroutine运行,而读写锁表示在锁范围内数据的读写操作
栗子:map在并发下读写结合读写锁完成
func main() {
var rwm sync.RWMutex
var wg sync.WaitGroup
wg.Add(10)
m := make(map[int]int)
for i := 0; i < 10; i++ {
go func(j int) {
rwm.Lock()
m[j] = j
fmt.Println(m)
rwm.Unlock()
wg.Done()
}(i)
}
wg.Wait()
fmt.Println("ok")
}
4、channel
先了解并发通信概念
在工程上,有两种最常见的并发通信模型:共享数据和消息
上述例子,我们可以使用加锁处理,但是,实现一个如此简单的功能,却写出如此臃肿而且难以理解的代码,显然不符合GO语言风格,那么加锁方式这里就不演示,直接介绍Go语言提供的goroutine间通信方式channel。
channel是Go提供goroutine间的通信方式,使用channel可以使多个goroutine之间通信。channel是进程内的通信方式,通过channel传递对象的过程和调用函数时的参数传递行为比较一致,比如也可以传递指针等。如需跨进程通信,Go建议用分布式系统的方法来解决,如使用Socket或者HTTP等通信协议,Go语言在网络方面也有非常完善的支持。
channel是类型相关的,一个channel只能传递一种类型的值,这个类型需要在声 明channel时指定。接下来,了解channel语法,简单一个例子,代码如下:
package main
import "fmt"
func Count(ch chan int) {
ch <- 1
fmt.Println("Counting")
}
func main() {
chs := make([]chan int,10) //定义10个channel的数组
for i := 0; i < 10; i++ { //数组中的每个channel分配给10个不同的goroutine
chs[i] = make(chan int)
go Count(chs[i])
}
for _,ch := range(chs) {
<-ch
}
}
channel基本语法
var channame chan ElementType
var channame chan <- ElementType //只写
var channame <- chan ElementType //只读
chanName := make(chan int) //无缓存channel
chanName := make(chan in,0) //无缓存channel
chanName := make(chan int,100) //有缓存channel
栗子说明:
ch <- 1 //向ch添加一个值为1
<- ch //从ch取出一个值
a := <- ch //从ch取出一个值并赋值给a
a,b := <- ch //从ch取出一个值赋值给a,如果ch已经关闭或ch没有值,b为false
注:如果channel之前没有写入数据,那么从channel中读取数据也会导致程序阻塞,直到channel 中被写入数据为止,我们也可以控制channel只读或写,后面讲解。
栗子1:同步,主协程和子协程之间通信
func main(){
ch := make(chan int)
go func() {
ch <- 996 //向ch添加元素
}()
a := <- ch
fmt.Println(a)
fmt.Println("程序结束!")
}
栗子2:两个子协程的通信
使用channel实现两个goroutine之间通信。(go语言中没有使用共享内存完成线程通信,而是使用channel)代码如下:
func two() {
tc := make(chan string)
ch := make(chan int)
// 第一个协程
go func() {
tc <- "协程A,我在添加数据"
ch <- 1
}()
// 第二个协程
go func() {
content := <- tc
fmt.Printf("协程B,我在读取数据:%s\n",content)
ch <- 2
}()
<- ch
<- ch
fmt.Println("程序结素!")
}
func main(){
two()
}
注:我们在往出取数据时,都是箭头,数据多的时候很麻烦呀,怎么办?使用for range获取channel中的内容。
func zs() {
ch1 := make(chan string)
ch2 := make(chan int)
go func() {
for i := 97; i <= 97+26; i++ {
// ch1 <- strconv.Itoa(i)
// 把int的值通过ASCII转换成字符串
ch1 <- fmt.Sprintf("%c",i)
}
ch2 <- 1
}()
go func() {
for n := range ch1{
fmt.Println(n)
}
ch2 <- 2
}()
<- ch2
}
go出现deadlck死锁情况,如:
func main() {
ch := make(chan int,3)
ch <- 1
//<- ch
ch <- 1
ch <- 1
ch <- 1
fmt.Println("ok")
}
想channel添加数据超过缓存,会出现死锁。
使用Select来进行调度
Select 和 swath结构很像,但是select中的case的条件只能是I/O。
func main() {
ch1 := make(chan int,1)
ch2 := make(chan string,1)
ch1 <- 1
ch2 <- "Hello world"
select {
case a1 := <- ch1:
fmt.Println(a1)
case a2 := <- ch2:
fmt.Println(a2)
default:
fmt.Println("default")
}
}
select里面case是随机执行的,如果都不满足条件,那么久执行default
栗子:实现一个一直接收消息
func main() {
ch := make(chan int)
for i := 1; i <= 10; i++ {
go func(j int) {
ch <- j
}(i)
}
for {
select {
case a1 := <- ch:
fmt.Println(a1)
default:
}
}
}
select总结:
- 每个case必须是一个I/O操作
- case是随机执行的
- 如果所有case不能执行,那么会执行default
- 如果所有case不能执行,且没有default,会出现阻塞
5、Go语言中的GC
Go语言采用stop The World方式,早起版本1.5之前垃圾回收效率不算高,在1.5开始支持并发收集,到1.8版本,Go能把STW时间优化到100微秒级。
怎么时候Go会触发GC?
- 当需要申请内存时,发现GC是上次GC两倍时会触发
- 每2分钟自动运行一次GC
Go语言常用GC算法:
- 三色标记法,是mark And Sweep改进版,将逻辑上分为白色区(未搜索),灰色区(正搜索),黑色区(已搜索)
GC调优:
- 少对象复用,局部变量尽量少声明,多个小对象可以放入到结构体中,这样方便GC扫描
- 少用string的"+"