四.专门详解Go并发编程相关知识
1.Go为什么天然支持高并发,纤程比线程的优势是什么?
Go语言在设计的时候就考虑了充分利用计算机的多核处理器,具体表现为,Go中开启一个并发的任务以操作系统的线程资源调度为单位的,而是Go的创造者们自己写了一套管理多个任务的机制,在这个机制下,每一个并发的任务线程叫做纤程,这个纤程的作用等同一个线程,也是并发执行的,只不过纤程是在应用程序管理的,懂底层的可以讲是在用户态的一个线程,而Java中调度的线程是属于操作系统,也就是操作系统内核态的线程。
用户态的纤程归属于用户编写的软件管理和调度,优点是可以根据情况灵活实现堆栈的内存分配,最优化其中的运行资源配置。
内核态的线程归属于操作系统调度和管理,他底层是有windows或者linux操作系统底层的代码管理的,那么他就不灵活,每个线程分配的资源可能造成浪费,创建的线程数肯定也有一定的限制。
Go的创造可以为自己的语言和任务灵活配置资源,Linux和windows操作系统的代码是通用的,总不能为你这个语言修改源代码把。
在实际程序运行中,一个操作系统的内核态线程可能管理着好几个甚至数十个纤程(根据实际情况和设置不同而不同),所以省去了线程时间片上下文切换的时间。
同时因为内部机制灵活,所以执行效率高,占用内存也少。
这就是Go语言的并发优势的核心所在。
2.并发和并行的区别?
并发是指的一个角色在一段时间内通过来回切换处理了多个任务。
并行是指两个或者多个角色同时处理自己的任务。
举例:
并发:在一个小时内,你写了10分钟语文作业,又写了10分钟数学,之后又写了10分中英语作业,然后再从语文10分钟,数学10分钟,英文10分钟又来一次。
这个叫做你并发的写语文数学英语作业。
你一个一段时间(一个小时内)通过切换(一会写数学,一会写语文。。。),处理了多个任务(写了三门课的作业)
并行:你和小明同时写自己的作业。你们俩同时运行的状态叫做并行运作状态,强调的是你们两个人同时在处理任务(做作业)。
你和小明(两个以上的角色)同时写作业(处理自己的任务)。
在计算机中,比如有4个cpu,4个cpu同时工作,叫做这4个cpu并行执行任务,每个cpu通过时间片机制上下文切换处理100个小任务,叫做每个cpu并发的处理100个任务。
3.Go是如何用Channel进行协程间数据通信数据同步的?
go中的线程相关的概念是Goroutines(并发),是使用go关键字开启。
Java中的线程是通过Thread类开启的。
在go语言中,一个线程就是一个Goroutines,主函数就是(主) main Goroutines。
使用go语句来开启一个新的Goroutines
比如:
普通方法执行
myFunction()
开启一个Goroutines来执行方法
go myFunction()
java中是
new Thread(()->{
//新线程逻辑代码
}).start();
参考下面的代码示例:
package main
import (
"fmt"
)
//并发开启新线程goroutine测试
//我的方法
func myFunction() {
fmt.Println("Hello!!!")
}
//并发执行方法
func goroutineTestFunc() {
fmt.Println("Hello!!! Start Goroutine!!!")
}
func main() {
/*
myFunction()
//go goroutineTestFunc()
//此时因为主线程有时候结束的快,goroutineTestFunc方法得不到输出,由此可以看出是开启了新的线程。
*/
//打开第二段执行
/*
go goroutineTestFunc()
time.Sleep(10*time.Second)//睡一段时间 10秒
myFunction()
*/
}
线程间的通信:
java线程间通信有很多种方式:
比如最原始的 wait/notify
到使用juc下高并发线程同步容器,同步队列
到CountDownLatch等一系列工具类
......
甚至是分布式系统不同机器之间的消息中间件,单机的disruptor等等。
Go语言不同,线程间主要的通信方式是Channel。
Channel是实现go语言多个线程(goroutines)之间通信的一个机制。
Channel是一个线程间传输数据的管道,创建Channel必须声明管道内的数据类型是什么
下面我们创建一个传输int类型数据的Channel
代码示例:
package main
import "fmt"
func main() {
ch := make(chan int)
fmt.Println(ch)
}
channel是引用类型,函数传参数时是引用传递而不是值拷贝的传递。
channel的空值和别的应用类型一样是nil。
==可以比较两个Channel之间传输的数据类型是否相等。
channel是一个管道,他可以收数据和发数据。
具体参照下面代码示例:
package main
import (
"fmt"
"time"
)
//channel发送数据和接受数据用 <-表示,是发送还是接受取决于chan在 <-左边还是右边
//创建一个传输字符串数据类型的管道
var chanStr = make(chan string)
func main() {
fmt.Println("main goroutine print Hello ")
//默认channel是没有缓存的,阻塞的,也就是说,发送端发送后直到接受端接受到才会施放阻塞往下面走。
//同样接收端如果先开启,直到接收到数据才会停止阻塞往下走
//开启新线程发送数据
go startNewGoroutineOne()
//从管道中接收读取数据
go startNewGoroutineTwo()
//主线程等待,要不直接结束了
time.Sleep(100*time.Second)
}
func startNewGoroutineOne() {
fmt.Println("send channel print Hello ")
//管道发送数据
chanStr <- "Hello!!!"
}
func startNewGoroutineTwo(){
fmt.Println("receive channel print Hello ")
strVar := <-chanStr
fmt.Println(strVar)
}
无缓存的channel可以起到一个多线程间线程数据同步锁安全的作用。
缓存的channel创建方式是
make(chan string,缓存个数)
缓存个数是指直到多个数据没有消费或者接受后才进行阻塞。
类似于java中的synchronized和lock
可以保证多线程并发下的数据一致性问题。
首先我们看一个线程不安全的代码示例:
package main
import (
"fmt"
"time"
)
//多线程并发下的不安全问题
//金额
var moneyA int =1000
//添加金额
func subtractMoney(subMoney int) {
time.Sleep(3*time.Second)
moneyA-=subMoney
}
//查询金额
func getMoney() int {
return moneyA;
}
func main() {
//添加查询金额
go func() {
if(getMoney()>200) {
subtractMoney(200)
fmt.Printf("200元扣款成功,剩下:%d元\n",getMoney())
}
}()
//添加查询金额
go func() {
if(getMoney()>900) {
subtractMoney(900)
fmt.Printf("900元扣款成功,剩下:%d元\n",getMoney())
}
}()
//正常逻辑,只够扣款一单,可以多线程环境下结果钱扣多了
time.Sleep(5*time.Second)
fmt.Println(getMoney())
}
缓存为1的channel可以作为锁使用:
示例代码如下:
package main
import (
"fmt"
"time"
)
//多线程并发下使用channel改造
//金额
var moneyA = 1000
//减少金额管道
var synchLock = make(chan int,1)
//添加金额
func subtractMoney(subMoney int) {
time.Sleep(3*time.Second)
moneyA-=subMoney
}
//查询金额
func getMoney() int {
return moneyA;
}
func main() {
//添加查询金额
go func() {
synchLock<-10
if(getMoney()>200) {
subtractMoney(200)
fmt.Printf("200元扣款成功,剩下:%d元\n",getMoney())
}
<-synchLock
}()
//添加查询金额
go func() {
synchLock<-10
if(getMoney()>900) {
subtractMoney(900)
fmt.Printf("900元扣款成功,剩下:%d元\n",getMoney())
}
synchLock<-10
}()
//这样类似于java中的Lock锁,不会扣多
time.Sleep(5*time.Second)
fmt.Println(getMoney())
}
4.Go中的Goroutine使用和GMP模型?
Go中的线程(实际是纤程)goroutine的底层管理和调度是在runtime包中自己实现的,其中遵循了GMP模型。
G就是一个goroutine,包括它自身的一些元信息。
M是指操作系统内核态的线程的一个虚拟表示,一个M就是操作系统内核态的一个线程。
P是一个组列表,P管理着多个goroutines,P还有一些用于组管理的元数据信息。
5.Go的select怎么用?
Go中的select是专门用于支持更好的使用管道(channel)的。
我们之前虽然讲了能从管道中读取数据,但是这有一个缺陷,就是我们在一个Goroutine中不能同时处理读取多个channel,因为在一个Goroutine中,一个channel阻塞后就无法继续运行了,所以无法在一个Goroutine处理多个channel,而select很好的解决了这个问题。
select相当于Java中Netty框架的多路复用器的功能。
举例代码示例:
package main
import "fmt"
func main() {
//创建一个缓存为1的chan
myChan := make(chan int,1)
for i:=1;i<=100;i++{
//select 的用法是,从上到下依次判断case 是否可执行,如果可执行,则执行完毕跳出select,如果不能执行,尝试下一个执行
//这里的可执行是指的不阻塞,也就是说,select从上到下开始挑选一个不阻塞的case执行,执行完毕后跳出,
//如果所有case都阻塞,则执行default
//如下输出结果,i=奇数的时候走case myChan<-i:,把奇数放入mychan
//走偶数的时候因为myChan中有数据了,则把上一个奇数打印出来。
//所以结果是 1 3 5 7 ...
select {
case data := <-myChan:
fmt.Println(data)
case myChan<-i:
default:
fmt.Println("default !!!")
}
}
}
6.Go中的互斥锁(类似于Java中的ReentrantLock)
先按线程不安全的数据错误的代码示例:
package main
import (
"fmt"
"sync"
)
//全局变量
var num int
var wait sync.WaitGroup
func main() {
wait.Add(5)
go myAdd()
go myAdd()
go myAdd()
go myAdd()
go myAdd()
wait.Wait()
//预期值等于5万,可是因为线程不安全错误,小于5万
fmt.Printf("num = %d\n",num)
}
func myAdd() {
defer wait.Done()
for i:=0 ;i<10000;i++ {
num+=1
}
}
打印输出结果:
num = 38626
互斥锁示例代码如下:
package main
import (
"fmt"
"sync"
)
//全局变量
var num int
var wait sync.WaitGroup
var lock sync.Mutex
func main() {
wait.Add(5)
go myAdd()
go myAdd()
go myAdd()
go myAdd()
go myAdd()
wait.Wait()
//预期值等于5万,可是因为线程不安全错误,小于5万
fmt.Printf("num = %d\n",num)
}
func myAdd() {
defer wait.Done()
for i:=0 ;i<10000;i++ {
lock.Lock()
num+=1
lock.Unlock()
}
}
7.Go中的读写锁(类似于Java中的ReentrantReadWriteLock)
读写锁用于读多写少的情况,多个线程并发读不上锁,写的时候才上锁互斥
读写锁示例代码如下:
package main
import (
"fmt"
"sync"
"time"
)
//金额
var moneyA = 1000
//读写锁
var rwLock sync.RWMutex;
var wait sync.WaitGroup
//添加金额
func subtractMoney(subMoney int) {
rwLock.Lock()
time.Sleep(3*time.Second)
moneyA-=subMoney
rwLock.Unlock()
}
//查询金额
func getMoney() int {
rwLock.RLock()
result := moneyA
rwLock.RUnlock()
return result;
}
func main() {
wait.Add(2)
//添加查询金额
go func() {
defer wait.Done()
if(getMoney()>200) {
subtractMoney(200)
fmt.Printf("200元扣款成功,剩下:%d元\n",getMoney())
}else {
fmt.Println("余额不足,无法扣款")
}
}()
//添加查询金额
go func() {
defer wait.Done()
if(getMoney()>900) {
subtractMoney(900)
fmt.Printf("900元扣款成功,剩下:%d元\n",getMoney())
}else {
fmt.Println("余额不足,无法扣款")
}
}()
wait.Wait()
fmt.Println(getMoney())
}
8.Go中的并发安全Map(类似于CurrentHashMap)
Go中自己通过make创建的map不是线程安全的,具体体现在多线程添加值和修改值下会报如下错误:
fatal error : concurrent map writes
这个错类似于java中多线程读写线程不安全的容器时报的错。
Go为了解决这个问题,专门给我们提供了一个并发安全的map,这个并发安全的map不用通过make创建,拿来即可用,并且他提供了一些不同于普通map的操作方法。
参考如下代码示例:
package main
import (
"fmt"
"sync"
)
//创建一个sync包下的线程安全map对象
var myConcurrentMap = sync.Map{}
//遍历数据用的
var myRangeMap = sync.Map{}
func main() {
//存储数据
myConcurrentMap.Store(1,"li_ming")
//取出数据
name,ok := myConcurrentMap.Load(1)
if(!ok) {
fmt.Println("不存在")
return
}
//打印值 li_ming
fmt.Println(name)
//该key有值,则ok为true,返回它原来存在的值,不做任何操作;该key无值,则执行添加操作,ok为false,返回新添加的值
name2, ok2 := myConcurrentMap.LoadOrStore(1,"xiao_hong")
//因为key=1存在,所以打印是 li_ming true
fmt.Println(name2,ok2)
name3, ok3 := myConcurrentMap.LoadOrStore(2,"xiao_hong")
//因为key=2不存在,所以打印是 xiao_hong false
fmt.Println(name3,ok3)
//标记删除值
myConcurrentMap.Delete(1)
//取出数据
//name4,ok4 := myConcurrentMap.Load(1)
//if(!ok4) {
// fmt.Println("name4=不存在")
// return
//}
//fmt.Println(name4)
//遍历数据
rangeFunc()
}
//遍历
func rangeFunc(){
myRangeMap.Store(1,"xiao_ming")
myRangeMap.Store(2,"xiao_li")
myRangeMap.Store(3,"xiao_ke")
myRangeMap.Store(4,"xiao_lei")
myRangeMap.Range(func(k, v interface{}) bool {
fmt.Println("data_key_value = :",k,v)
//return true代表继续遍历下一个,return false代表结束遍历操作
return true
})
}
9.Go中的AtomicXXX原子操作类(类似于Java中的AtocmicInteger之类的)
Go中的atomic包里面的功能和Java中的Atomic一样,原子操作类,原理也是cas,甚至提供了cas的api函数,这里不做过多讲解,
简单举一个代码示例,因为方法太多,详细的请参考api文档中的atomic包:
package main
import "sync/atomic"
func main() {
//简单举例
var num int64 = 20
atomic.AddInt64(&num,1)
}
10.Go中的WaitGroup(类似于Java中的CountDownLatch)
现在让我们看一个需求,比如我们开启三个并发任务,然后三个并发任务执行处理完毕后我们才让主线程继续往下面走。
这时候肯定不能用睡眠了,因为不知道睡眠多长时间。
这是Go中的sync包提供了一个WaitGroup的工具,他基本上和Java中的CountDownLatch的功能一致。
接下来让我们看代码示例:
package main
import (
"fmt"
"sync"
"time"
)
//获取类似于CountDownLatch的对象
var wait sync.WaitGroup
func main() {
//设置计数器任务为3,当3个任务全部done后,wait.Wait()才会松开阻塞
wait.Add(3)
go myFun1()
go myFun2()
go myFun3()
//阻塞
wait.Wait()
}
func myFun1() {
//计数器减1
defer wait.Done()
//睡眠五秒
time.Sleep(time.Second*5)
fmt.Println("fun1执行完毕")
}
func myFun2() {
//计数器减1
defer wait.Done()
//睡眠五秒
time.Sleep(time.Second*5)
fmt.Println("fun2执行完毕")
}
func myFun3() {
//计数器减1
defer wait.Done()
//睡眠五秒
time.Sleep(time.Second*5)
fmt.Println("fun3执行完毕")
}