- 协程概念 协程(Coroutines)是一种比线程更加轻量级的存在,正如一个进程可以拥有多个线程一样,一个线程可以拥有多个协程。 协程具有以下几个特点
- 用户态执行,完全由程序所控制,不是被操作系统内核所管理的
- 适用于处理IO密集型任务,至于什么是IO密集型任务这里就不做详细介绍了,主要区别于CPU密集型任务
- 将线程中的竞争资源,转化成协作运行
- 通道(Channel)的方式进行协程间的通信
- 少量的上下文切换开销,主要是运行在线程上,对比进程的上下文切换是保存在栈资源当中,而协程是异步非阻塞的,相当于用户态线程中的队列任务,只需要利用channel作为回调即可,不需要在任务完成后二次的资源抢夺
Swoole 协程
单进程模型,实现方式相对简单 / 不用加锁 / 性能高,基于单线程的,无法利用CPU多核,运行在用户进程中
echo "main start ";
echo "coro ".co::getcid()." start
";
co::sleep(.1);
echo "coro ".co::getcid()." end
";
echo "coro ".co::getcid()." start
";
co::sleep(.1);
echo "coro ".co::getcid()." end
";
Golang 协程
MPG模型,运行在操作系统内核CPU上,因此可以利用CPU多核 M: 表示内核级线程,一个 M 就是一个线程,goroutine 跑在 M 之上的。 G: 表示一个 goroutine,它有自己的栈。 P: 全称是 Processor,处理器。它主要用来执行 goroutine 的,同时它也维护了一个 goroutine 队列。
Go 语言原生层面就支持协层,不需要声明协程环境。 这里还有一句著名的话 要通过共享内存来通信,相反,应该通过通信来共享内存
"fmt"
"sync"
"math/rand"
"container/ring"
"strings"
"time"
wg sync.WaitGroup // 用于goroutine计数
times = 2 // 每个选手发球次数
nums = 4 // 多少个选手
serveTotals = nums * times // 总发球次数
score_balls_A = make([]TableTennis, 0, serveTotals) // A的得分球
score_balls_B = make([]TableTennis, 0, serveTotals) // B的得分球
turn = ring.New(4) // 发球顺序
serveMetux sync.Mutex // 发球锁
catch_chanel_B = make(chan TableTennis, 0) // B队伍接球的通道
catch_chanel_A = make(chan TableTennis, 0) // A队伍接球的通道
balls_ids = make(chan int, serveTotals) // 球的id
id int
trail string // 球的轨迹
defer wg.Done()
// 初始化发球顺序
turn.Value = "A1"
turn = turn.Next()
turn.Value = "B1"
turn = turn.Next()
turn.Value = "A2"
turn = turn.Next()
turn.Value = "B2"
// 开始发球
for i := 0; i < times; i++ {
for j := 0; j < nums; j++ {
serveMetux.Lock() // 解锁时发下一个球
turn = turn.Next()
name := turn.Value.(string)
t := TableTennis{<-balls_ids, name + "-in"}
if name[0] == "A" {
catch_chanel_B <- t
} else {
catch_chanel_A <- t
}
}
}
time.Sleep(time.Second) // 等待player goroutine对catch_chanel的使用
close(catch_chanel_A)
close(catch_chanel_B)
defer wg.Done() // 延迟递减计数
for t := range catch_chanel_A {
// 2. 将球击打出去
rest := shot(rate)
// 3. 记录球的轨迹
t.trail += "-" + name + "-" + rest
// 球出界
if strings.Compare("out", rest) == 0 {
// 对方得分
score_balls_B = append(score_balls_B, t)
fmt.Println(t)
serveMetux.Unlock()
continue
}
// 4. 对面队伍准备接球
catch_chanel_B <- t
}
defer wg.Done() // 延迟递减计数
for t := range catch_chanel_B {
// 2. 将球击打出去
rest := shot(rate)
// 3. 记录球的轨迹
t.trail += "-" + name + "-" + rest
// 球出界
if strings.Compare("out", rest) == 0 {
// 对方得分
score_balls_A = append(score_balls_A, t)
fmt.Println(t)
serveMetux.Unlock()
continue
}
// 4. 对面队伍准备接球
catch_chanel_A <- t
}
if rand.Intn(100) < rate {
return "in"
} else {
return "out"
}
fmt.Println("比赛开始...")
// 初始化球的id
for i := 0; i < serveTotals; i++ {
balls_ids <- i + 1
}
// 初始化发球顺序
wg.Add(nums + 1) // 累加计数
go serve()
//time.Sleep(time.Second)
go playerA("A1", 45)
go playerA("A2", 60)
go playerB("B1", 50)
go playerB("B2", 90)
wg.Wait()
fmt.Println("比赛结束.")
fmt.Printf("A : B = (%d, %d)
", len(score_balls_A), len(score_balls_B))
for _, t := range score_balls_A {
fmt.Println(t)
}
fmt.Println()
for _, t := range score_balls_B {
fmt.Println(t)
}
另外补充:管道channel的实现 我们知道管道是操作系统中通信的一种方式,它的特点是半双工通信,同一时间只有单方向的数据传输 swoole和golang都实现了channel, 但是并不是基于操作系统的管道实现的,只用应用了相同的原理
本质上是运行在内存中的队列,数据结构 Channel,底层基于共享内存 + Mutex 互斥锁实现,实现用户态的高性能内存队列。 这篇文章有详细的swoole源码分析https://zhuanlan.zhihu.com/p/45020194 并且channel分为了无缓冲通道和有缓冲通道,通道中无数据或者缓冲满了都会形成阻塞。