• 协程概念 协程(Coroutines)是一种比线程更加轻量级的存在,正如一个进程可以拥有多个线程一样,一个线程可以拥有多个协程。 协程具有以下几个特点
  1. 用户态执行,完全由程序所控制,不是被操作系统内核所管理的
  2. 适用于处理IO密集型任务,至于什么是IO密集型任务这里就不做详细介绍了,主要区别于CPU密集型任务
  3. 将线程中的竞争资源,转化成协作运行
  4. 通道(Channel)的方式进行协程间的通信
  5. 少量的上下文切换开销,主要是运行在线程上,对比进程的上下文切换是保存在栈资源当中,而协程是异步非阻塞的,相当于用户态线程中的队列任务,只需要利用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分为了无缓冲通道和有缓冲通道,通道中无数据或者缓冲满了都会形成阻塞。