- 协程概念 协程(Coroutines)是一种比线程更加轻量级的存在,正如一个进程可以拥有多个线程一样,一个线程可以拥有多个协程。 协程具有以下几个特点
- 用户态执行,完全由程序所控制,不是被操作系统内核所管理的
- 适用于处理IO密集型任务,至于什么是IO密集型任务这里就不做详细介绍了,主要区别于CPU密集型任务
- 将线程中的竞争资源,转化成协作运行
- 通道(Channel)的方式进行协程间的通信
- 少量的上下文切换开销,主要是运行在线程上,对比进程的上下文切换是保存在栈资源当中,而协程是异步非阻塞的,相当于用户态线程中的队列任务,只需要利用channel作为回调即可,不需要在任务完成后二次的资源抢夺
Swoole 协程
单进程模型,实现方式相对简单 / 不用加锁 / 性能高,基于单线程的,无法利用CPU多核,运行在用户进程中
echo "main start\n";
-
go(function () {
-
echo "coro ".co::getcid()." start\n";
-
co::sleep(.1);
-
echo "coro ".co::getcid()." end\n";
-
});
-
echo "main flag\n";
-
go(function () {
-
echo "coro ".co::getcid()." start\n";
-
co::sleep(.1);
-
echo "coro ".co::getcid()." end\n";
-
});
-
echo "end\n";
Golang 协程
MPG模型,运行在操作系统内核CPU上,因此可以利用CPU多核 M: 表示内核级线程,一个 M 就是一个线程,goroutine 跑在 M 之上的。 G: 表示一个 goroutine,它有自己的栈。 P: 全称是 Processor,处理器。它主要用来执行 goroutine 的,同时它也维护了一个 goroutine 队列。
Go 语言原生层面就支持协层,不需要声明协程环境。 这里还有一句著名的话 要通过共享内存来通信,相反,应该通过通信来共享内存
-
package main
-
import (
-
"fmt"
-
"sync"
-
"math/rand"
-
"container/ring"
-
"strings"
-
"time"
-
)
-
var (
-
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
-
)
-
// 乒乓球
-
type TableTennis struct {
-
id int
-
trail string // 球的轨迹
-
}
-
func serve() {
-
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)
-
}
-
// A队选手
-
func playerA(name string, rate int) {
-
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
-
}
-
}
-
// B队选手
-
func playerB(name string, rate int) {
-
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
-
}
-
}
-
// 击球
-
func shot(rate int) string {
-
if rand.Intn(100) < rate {
-
return "in"
-
} else {
-
return "out"
-
}
-
}
-
func main() {
-
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)\n", 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分为了无缓冲通道和有缓冲通道,通道中无数据或者缓冲满了都会形成阻塞。
来源:oschina
链接:https://my.oschina.net/u/2007165/blog/4301468