常见并发模型
- 进程&线程 ( Apache )
进程是一种系统运行行动, 就是代表计算机做某个事情的一系列行动的总和, 它是程序的执行实体, 一般情况下是一个程序一个进程, 多进程情况是一个程序多个进程
线程是运算调度的最小单元, 它的作用就是运算, 由系统内核控制大小, 同时运行多个任务, 进程可以包含多个线程, 因此在同一个进程中, 可以执行多个线程, 每个线程负责一个动作
系统最初的Web服务器都是基于进程&线程模型, 就是每新到一个请求就会分配一个进程或者线程, 每个进程只服务一个用户, 互联网初期用户的访问不多, 网站可以正常工作, 但是进程很昂贵, 一台服务器无法创建很多的进程, 随着互联网的发展, 网站越来越复杂, 一个页面可能就上百个请求, 操作系统就无法承受了 - 异步非阻塞 ( Nginx, Libevent, NodeJS )
一台服务器可以服务大量的用户, 并且资源消耗还很低, 但是其为了追求性能, 强行将线型的程序打乱, 开发和维护就变得很复杂, 调试也很困难 - 协程 ( Golang, Erlang, Lua)
协程 ( Coroutine ) 是轻量级的线程, 由用户来控制, 协程的内存消耗更小 ( 一个线程可以包含多个协程, 线程大约8MB的内存申请量, 协程大约2KB的内存申请量 ), 协程尚希文切换更快 ( 协程少一道手续, 线程申请内存需要走过内核, 协程申请内存不需要走过内核 )
== Goroutine 也是一种协程, 它是在协程的基础上做了进一步优化, 并加入了Go语言的一些特性, 去掉了冗余的协程生命周期管理 ( 协程创建, 协程完成, 协程重用 ), 降低额外的延迟和开销 ( 由于协程间频繁交互导致的 ) 使得 Goroutine 更加高效也降低对系统的负担, 降低加锁/解锁的频率 ( 降低一部分额外的开销 )==
降低了开发复杂度, 像写线型程序那样来书写全异步的程序, 其实协程底部就是线程, 但它比线程更轻量, 几十个协程体现在底层可能也就五六个线程, 协程可以理解为更高效更易用更轻量的线程
Golang 并发实现
多协程是指一段时间内协程的并行, 即某个任务使用多个协程同时进行处理, 多协程的必要条件: 协程任务之间有关联性 ( 相互之间组成整体任务, 相互之间有连续性 ), 有总分总的协调步骤过程
多协程任务步骤: 任务切分/分配, 启动多个协程, 合并多个协程的结果
多协程使用场景: 运算量比较多的流程上, 协程间依赖性比较弱
多协程的局限: 会增加额外的耗时, 会增加额外的内存消耗
协程等待: sync.WaitGroup
多协程生命周期: 协程的创建等全部生命历程的管理, 作用是为了便于协程的回收利用, 生命周期分类: 协程创建 ( 通过函数前加go关键字 ), 协程回收 ( Go语言的回收机制 ), 协程中断 ( Context 实现中断 )
package main
import (
"context"
"fmt"
"sync"
)
// 协程等待
var wg sync.WaitGroup
// 主函数 主 Goroutine
func main() {
// 初始化一个 context
parent := context.Background()
// 生成一个取消的 context
ctx, cancel := context.WithCancel(parent)
runTimes := 0
wg.Add(1)
// 启动 goroutine
go func(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("Goroutine Done")
return
default:
fmt.Println("Goroutine Running Times: ", runTimes)
runTimes++
}
if runTimes >= 5 {
cancel()
wg.Done()
}
}
}(ctx)
wg.Wait()
}
- 程序并发执行 ( goroutine ), 通过 goroutine 来保证程序的并发执行
foo() // 执行函数 foo, 程序等待函数 foo 返回
go foo() // 新建新的 goroutine 去执行函数 foo
bar() // 不用等待函数 foo 返回, 两个函数并发执行
- 通过管道 channel 来实现多个 goroutine 间的数据同步和通信
Channel 用于多个协程之间的通信
Channel的妙用: 传递方面 ( 消息传递, 任务发送, 事件广播 ), 控制方面 ( 资源争抢, 并发控制, 流程开关 )
Channel 关闭后, 可读不可写
channel 在使用之前, 必须进行make初始化, 否则它是一个nil
无缓冲区Channel, 使用时要同时具备输入和输出
资源争抢应用场景: 电商的秒杀活动, 出行的司机抢单, 互金的股票抢购, 系统的计算资源争抢
package main
import (
"context"
"fmt"
"sync"
"time"
)
// 协程等待
var wg sync.WaitGroup
// 主函数 主 Goroutine
func main() {
// 资源争抢, 100个人抢10个鸡蛋
eggs := make(chan int, 10) // 设定Channel缓冲区, 根据业务场景需求, 设置适合大小
// 超时 Context
ctx, cancel := context.WithTimeout(context.Background(), 10\*time.Second)
// 鸡蛋
for i := 0; i < 10; i++ {
eggs <- i
}
// close(eggs) 关闭后只读不可写, 用for range 可循环获取到通道内的值, 但是用for循环读取时, 即使通道已没有值, 也能获取到对应类型的零值, 一般 close 和 for range 搭配使用
// 100个人并行抢
for i := 0; i < 100; i++ {
wg.Add(1)
go func(n int, c context.Context) {
// 多管道select调度
select {
case egg := <-eggs:
fmt.Printf("people: %d get egg: %d\n", n, egg)
case <-c.Done():
fmt.Println("timeout")
cancel()
return
default:
}
wg.Done()
}(i, ctx)
}
wg.Wait()
}
// 初始化
c := make(chan string) // 创建一个通道 channel, 无缓冲区, 使用时要同时具备输入和输出, 即该管道要同时可写入和读取
go func (){
time.Sleep(1 \* time.Second)
// 输入
c <- "message" // 发送消息到 channel
// 关闭
close(c)
}()
// 输出
msg := <- c // 阻塞直到接收到数据
- 通过 select 在多个 channel 中选择数据读取或者写入
c1 := make(chan string, 1)
c2 := make(chan string, 1)
select {
// c1 和 c2 都有值, 则随机其中之一执行
case v := <-c1: // c1 管道有值则执行该条case
fmt.Println("channel1 sends", v)
case v := <-c2: // c2 管道有值则执行该条case
fmt.Println("channel2 sends", v)
default: // 可选
fmt.Println("neither channel was ready")
}
![img](https://img-blog.csdnimg.cn/img_convert/19813efce676500b30efc78490f0cb6b.png)
![img](https://img-blog.csdnimg.cn/img_convert/75a063c75b2d4f56c7f78f12a67f4d42.png)
![img](https://img-blog.csdnimg.cn/img_convert/b61c3450f6d4de1877c826c21196be46.png)
**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Go语言开发知识点,真正体系化!**
加入社区》https://bbs.csdn.net/forums/4304bb5a486d4c3ab8389e65ecb71ac0
664436191)]
[外链图片转存中...(img-K0JKymuk-1725664436191)]
**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Go语言开发知识点,真正体系化!**
加入社区》https://bbs.csdn.net/forums/4304bb5a486d4c3ab8389e65ecb71ac0