一、基本概念
在学习Goroutine编程思想之前,先来了解几个关键的概念:
1. 进程和线程
- 进程是程序在操作系统中的一次执行过程,系统进行资源分配和调度的一个独立单位。
- 线程是进程的一个执行实体,是CPU调度和分派的基本单位,它是比进程更小的独立运行的基本单位。
- 一个进程可以创建和撤销多个线程,同一个进程中的多个线程是可以并发执行的。
2. 并发和并行
- 多线程程序在一个核的CPU上运行,就是并发
- 多线程程序在多核CPU上运行,就是并行
3. 线程和协程
- 协程: 独立的栈空间,共享堆空间,调度由用户自身控制,本质上有点类似于用户级线程,这些用户级线程的调度也是自己实现的。
- 线程: 一个线程上可以跑多个协程,协程是轻量级的线程。
4. 互斥锁和读写锁
写比较频繁的时候,我们用互斥锁。
读比较频繁的时候,我们用读写锁。
5. 实践
先说一下个人理解,我们将会写一个demo,创建一个协程,而我们运行程序是跑在一个线程上的,换句话说,我们写的线程和协程是同时,并发的去运行的,所以有可能会出现一个情况,主线程结束了,但是协程还没有结束,缺被强行终止了。
写个demo看一下:
func main() {
go test()
}
func test() {
var i int
for{
fmt.Print(i)
time.Sleep(time.Second)
}
}
运行之后是什么都不打印出来,原因很简单,主线程结束的时候,协程还没来得及去打印。
我们如果想让他打印出来,可以在主线程加一个sleep操作也可以。
6. goRoutine之间的通信
那么,如果我们有多个goRoutine要怎么进行通信操作呢?
方法有两个:
- 全局变量和锁同步
我们先不写锁,先把程序解释一下,我们要实现的是一个阶乘,现在要把数字n和n的阶乘存放进map里,将map设置为全局变量,方便添加参数。
//设置全局变量
var(
m=make(map[int]uint64)
)
type task struct{
n int
}
func calc(t *task){//计算阶乘结果
var sum uint64
sum=1
for i:=1;i<t.n;i++{
sum *=uint64(i)
}
m[t.n] = sum
}
func main() {
for i:=0;i<10;i++{
t := &task{n:i}
go calc(t)
}
time.Sleep(time.Second*10)
for k,v := range m {
fmt.Printf("%d → %v \n",k,v)
}
}
然后运行程序报错:
意思是无法并发的对map进行写入操作。这是因为我们让协程都同时对一个map进行的写操作。所以我们要修改这个bug,就要加锁,一次只用一个协程去写,就不会错了。修改之后的程序如下,其实就是加了加锁和去锁的代码:
//设置全局变量
var(
m=make(map[int]uint64)
lock sync.Mutex
)
type task struct{
n int
}
func calc(t *task){
var sum uint64
sum=1
for i:=1;i<t.n;i++{
sum *=uint64(i)
}
lock.Lock()
m[t.n] = sum
lock.Unlock()
}
func main() {
for i:=0;i<10;i++{
t := &task{n:i}
go calc(t)
}
time.Sleep(time.Second*10)
lock.Lock()
for k,v := range m {
fmt.Printf("%d → %v \n",k,v)
}
lock.Unlock()
}
- Channel
先说说channel的概念:
(1)类似unix的管道(pipe)
(2)先进先出
(3)线程安全,多个goroutine同时访问,不需要加锁
(4)channel是有类型的,一个整数的channel只能存放整数
channel的声明方式为:var 变量名 chan 类型
然后也写一个demo来看
比如我们要创建一个存储map类型的channel
func main() {
var mapChannel chan map[string]string
//初始化channel
mapChannel = make(chan map[string]string,10)
m:=make(map[string]string)
m["1214"] = "SB"
m["SB"] = "4121"
mapChannel <- m
}
然后我们再创建一个结构体,然后根据结构体创建一个相关的channel
type student struct {
name string
}
func main() {
var stuChan chan student
stuChan = make(chan student,10)
stu:=student{name:"ljy"}
stuChan <- stu
}
也可以这么写
type student struct {
name string
}
func main() {
var stuChan chan *student
stuChan = make(chan *student,10)
stu:=student{name:"ljy"}
stuChan <- &stu
}
以上都是对将数据写入channel的写法,然后再来说明一下从channel中读数据的写法
a:=<-stuChan
7.GoRoutine与Channel结合
先说一下,我们的目的,现在我们有多个协程,而且多个协程之间是相互有关联的,我们可以想成一个生产者和消费者模型来理解。
我们的goroutine意在是让主线程连同多个协程异步执行。而Channel的意义在于让多个协程之间,搭配着执行。
而难点就在于,我们要如何实现相互搭配着执行,比如,生产后消费,没有消费的就阻塞,等待生产这种。
下面就写一个生产者消费者的demo
我们将生产的数据,放入channel,然后让消费者去读
func readChannel(ch chan int) {
for{
var b int
b =<-ch
fmt.Printf("get data: %d\n",b)
}
}
func writeChannel(ch chan int) {
for i := 0; i < 10; i++ {
fmt.Printf("put data: %d\n",i)
ch <-i
}
}
func main() {
intChan :=make(chan int,10)
go writeChannel(intChan)
go readChannel(intChan)
time.Sleep(time.Second * 10)
}
写成这样就结束了,但是这只是循环了10个数,而且channel的容量也设置的是10,假如我们的生产速度的百万,那么就会出现,去消费还没来得及生产的数据或者消费的速度赶不上生产,生产了10个数据之后阻塞的情况。
这就需要我们去设置一个channel的阻塞功能。
之前我们实现阻塞,主要是通过lock去实现的,但是lock的弊端也很明显,我们无法灵活控制锁。
现在我们将channel的容量改为100,但是数据的产生上限改为100,还是使用生产者-消费者模型去计算1-100的质数,生产数据之后,对数据进行计算,然后存入另一个channel
func calculator(intChan chan int, resultChan chan int) {
for v:= range intChan{
flag:=true
for i:=2;i<v;i++{
if v%i == 0{
flag=false
break
}
}
if flag{
resultChan <- v
}
}
}
func main() {
intChan := make(chan int,100)
resultChan := make(chan int,100)
for i := 0; i < 100; i++ {
intChan <- i
}
for j := 0; j < 8; j++ {
go calculator(intChan,resultChan)
}
go func() {
for i:=range resultChan{
fmt.Println(i)
}
}()
time.Sleep(time.Second * 10)
}