常见并发模型
- 进程&线程 ( 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")
}
并发和并行
- 并发
指同一时刻, 系统通过调度, 来回切换交替的运行多个任务, "看起来"是同时进行
并发有一个好处, 假如任务A由于一些IO或者Sleep操作阻塞了, CPU就立即进行切换, 去执行任务B, 这样就可以充分利用CPU的资源, 提升整个程序的执行效率 - 并行
指同一时刻, 两个任务"真正的"同时进行
在Golang中, 通过在函数前加关键字go就可以轻松开启一个goroutine去实现并发, 不用去关心多个goroutine同时是并行还是并发的执行, 即多个goroutine执行到底是一个CPU核心通过不断的切换时间片去调度并发的执行, 还是将多个goroutine去分散到多个CPU上并行执行, 具体的底层细节就交给Golang去解决
在 Golang 中, 通常将一个复杂的任务拆分成一个个相互关系不大的小任务, 通过 goroutine 去并发执行, 然后再通过 channel 做数据通信, 这样就可以发挥出 Golang 的并发优势, 提升整个任务的执行效率
Golang 中的面向对象
在 Golang 中没有类和对象的概念, 但是可以通过结构体 struct 和接口 interface 来实现面向对象的三大特性( 封装, 继承, 多态 ), 进而实现面向对象编程
- 封装
// Foo 定义结构体, 相当于类
type Foo struct {
// 成员属性
baz string
}
// 接收者, 方法
func (f *Foo) echo() {
fmt.Println(f.baz)
}
func main() {
// 初始化结构体, 相当于实例化类
f := Foo{baz: "hello"}
// 调用方法
f.echo()
}
- 继承
// Foo 定义结构体, 相当于类
type Foo struct {
// 成员属性
baz string
}
// Bar 继承
type Bar struct {
// 拥有Foo所有的属性和方法
Foo
}
// 接收者, 方法
func (f *Foo) echo() {
fmt.Println(f.baz)
}
func main() {
// 初始化结构体, 相当于实例化类
b := Bar{Foo{baz: "chao"}}
// 调用父类的方法
b.echo()
}
- 多态
// Foo 定义接口, 需实现的方法
type Foo interface {
// 方法
qux()
}
// Bar 结构体
type Bar struct{}
// Baz 结构体
type Baz struct{}
// 实现接口, 只要实现了接口的所有方法, 即实现了该接口
func (b *Baz) qux() {
fmt.Println("baz")
}
func (b *Bar) qux() {
fmt.Println("bar")
}
func main() {
// 定义一个接口类型变量
var f Foo
// 实现了该接口, 即属于该接口类型的变量, 可以直接存到接口变量中
f = &Bar{}
f.qux()
f = &Baz{}
f.qux()
}