1. 并发模型
对于大量的服务请求,有两种服务模式:单一服务模式、多服务模式。对于单一服务模式,比较典型的如Redis,单进程,单线程服务。这种模式简单高效,但是适用场景有限,很容易达到服务能力的瓶颈。多服务模式有很多种实现方式。适应于不同的业务场景。
由于是多服务,不同服务间难免存在对同一资源的访问,这就涉及到服务间的同步问题。不同服务实现方式,同步的机制不同。
线程是操作系统对进程管理的最小粒度,程序通过线程实现对CPU、内存、网络、存储等资源的使用。因此线程是最基本的一种并发模式,是其他并发模式的基础。
多线程处于同一内存空间,可以方便的直接访问内次进行通信,可以通过互斥锁、自旋锁、信号量等方式进行同步。
由于资源访问控制的要求,需要进行锁互斥,或者进行IO访问出现阻塞时,线程都会处于阻塞状态,大量线程被阻塞时,服务会出现阻塞情况。为解决该问题,就有了异步调用、IO多路复用、协程等多种技术避免大量线程的同时阻塞。
一般随着数据量的增加、计算量的增加、以及高可用的考虑,会有不同并发规模。如单进程、单机、单机房、多机房、跨地市等不同的并发规模。针对不同的规模、业务要求,会有不同的并发模式。
并发模式是一个抽象的概念,可大可小。从编程语言的角度来看,并发主要考虑的是一个进程内的并发机制。超出进程外的,属于应用的系统架构,应用的架构需要基于编程语言提供的能力来构建。
并发模型的种类
- Actor model
- Petri net
- Process calculi
进程验算包括多种方式。- Ambient calculus
- Calculus of communicating systems (CCS)
- Communicating sequential processes (CSP)
- π-calculus
- Join-calculus
- Input/output automaton
- Preemptive machine scheduling
并发交互与通信
不同计算单元之间,需要通信以保持同步。主要分为两种方式:
- 共享内存
- 消息传递
Go语言实现的并发模型是基于CSP模型。
2. CSP模型
CSP 模型全称为 communicating sequential processes.CSP是一种形式化语言,用来描述并行计算中的交互模式。它是并行数学理论中的一员,基于通道(channel)传递消息。CSP对多种编程语言的设计中有很深的影响。如:occam,Limbo,Go等。
CSP最早由Tony Hoare于1978年的论文中描述。CSP已在工业上实际应用,作为规定(specifying)和验证(verifying)各种不同系统并发方面的工具。
CSP允许以独立运行的组件进程来描述系统,并且只
通过消息传递通信相互作用。
CSP与Actor区别
- CSP有两部分组成:process,channel。Actor由Actor实体和实体间关系组成。
- CSP由process向channel发送、读取消息,process间不相关。 Actor向Actor发送消息,Actor间相关。
goroutine & chan
goroutine对应CSP模型中的process.
chan对应CSP模型中的channel.
3. chan
3.1 chan变量定义
ChannelType = ( "chan" | "chan" "<-" | "<-" "chan" ) ElementType .
<-
指定了channel的方向。没有指定就是双向。 chan<-
只能向channel发送数据。<-chan
只能从channel读取数据。
只读、只写一般用于通过函数传递的channel,函数内只允许做单向操作。
3.2 chan对象创建
chan通过make进行创建。
make(chan 变量类型)
make(chan 变量类型,容量)
3.3 make(chan)底层实现
由汇编代码可以看出,对于make(chan)会转化为runtime.makechan()
,变量类型转化为chantype
,返回一个hchan
指针。
var var_chan_1 = make(chan int)
var var_chan_2 = make(chan int,0)
var var_chan_3 = make(chan int,3)
//对应汇编
// var_chan_1
0x0032 00050 (channel.go:10) LEAQ type.chan int(SB), AX
0x0039 00057 (channel.go:10) MOVQ AX, (SP)
0x003d 00061 (channel.go:10) MOVQ $0, 8(SP)
0x0046 00070 (channel.go:10) CALL runtime.makechan(SB)
0x004b 00075 (channel.go:10) MOVQ 16(SP), AX
0x0050 00080 (channel.go:10) MOVQ AX, "".var_chan_1+72(SP)
//var_chan_2
0x0055 00085 (channel.go:11) LEAQ type.chan int(SB), AX
0x005c 00092 (channel.go:11) MOVQ AX, (SP)
0x0060 00096 (channel.go:11) MOVQ $0, 8(SP)
0x0069 00105 (channel.go:11) CALL runtime.makechan(SB)
0x006e 00110 (channel.go:11) PCDATA $2, $1
0x006e 00110 (channel.go:11) MOVQ 16(SP), AX
0x0073 00115 (channel.go:11) MOVQ AX, "".var_chan_2+64(SP)
//var_chan_3
0x0078 00120 (channel.go:12) LEAQ type.chan int(SB), AX
0x007f 00127 (channel.go:12) MOVQ AX, (SP)
0x0083 00131 (channel.go:12) MOVQ $3, 8(SP)
0x008c 00140 (channel.go:12) CALL runtime.makechan(SB)
0x0091 00145 (channel.go:12) MOVQ 16(SP), AX
0x0096 00150 (channel.go:12) MOVQ AX, "".var_chan_3+56(SP)
源码
由makechan
可以看出,主要分为三种形式:
- chan缓存大小==0
没有指定大小,或者指定缓存大小为0。这时会将hchan.buf指向自己(&hchan.buf)。 - chan数据类型中不包含指针
不含指针则申请一块连续内存,将将缓存和hchan放在一起。hchan.buf指向hchan结束位置。 - chan数据类型中包含指针
包含指针,则将hchan和缓存分开申请两块不同内存。
type chantype struct {
typ _type
elem *_type
dir uintptr
}
func makechan(t *chantype, size int) *hchan {
elem := t.elem
// compiler checks this but be safe.
if elem.size >= 1<<16 {
throw("makechan: invalid channel element type")
}
if hchanSize%maxAlign != 0 || elem.align > maxAlign {
throw("makechan: bad alignment")
}
mem, overflow := math.MulUintptr(elem.size, uintptr(size))
if overflow || mem > maxAlloc-hchanSize || size < 0 {
panic(plainError("makechan: size out of range"))
}
// Hchan does not contain pointers interesting for GC when elements stored in buf do not contain pointers.
// buf points into the same allocation, elemtype is persistent.
// SudoG's are referenced from their owning thread so they can't be collected.
// TODO(dvyukov,rlh): Rethink when collector can move allocated objects.
var c *hchan
switch {
case mem == 0:
// Queue or element size is zero.
c = (*hchan)(mallocgc(hchanSize, nil, true))
// Race detector uses this location for synchronization.
c.buf = c.raceaddr()
case elem.kind&kindNoPointers != 0:
// Elements do not contain pointers.
// Allocate hchan and buf in one call.
c = (*hchan)(mallocgc(hchanSize+mem, nil, true))
c.buf = add(unsafe.Pointer(c), hchanSize)
default:
// Elements contain pointers.
c = new(hchan)
c.buf = mallocgc(mem, elem, true)
}
c.elemsize = uint16(elem.size)
c.elemtype = elem
c.dataqsiz = uint(size)
return c
}
3.4 hchan结构
- 如果chan是无缓存,则chan的读写是同步的。
- 有缓存的chan,将buf作为一个循环队列使用。sendx记录chan最新可以保存数据位置,recvx记录chan中还未读取的数据位置,当缓存中无数据可读时,将接收方放到接收等待队列recvq。当缓存满的时候,将发送方放到发送等待队列sendq。
type hchan struct {
qcount uint // 队列中元素总量,total data in the queue
dataqsiz uint // 缓冲区大小,make(chan,N)中N大小。size of the circular queue
buf unsafe.Pointer // 缓冲区位置,无缓冲区时,指向自己。points to an array of dataqsiz elements
elemsize uint16 // 元素大小
closed uint32 // chan关闭标识
elemtype *_type // 元素类型 element type
sendx uint // 待发送元素在缓存中位置,send index
recvx uint // 待接收元素在缓存中位置,receive index
recvq waitq // 接收等待队列,用于阻塞接收协程,list of recv waiters
sendq waitq // 发送等待队列,用于阻塞发送协程,list of send waiters
// lock protects all fields in hchan, as well as several
// fields in sudogs blocked on this channel.
//
// Do not change another G's status while holding this lock
// (in particular, do not ready a G), as this can deadlock
// with stack shrinking.
lock mutex //互斥锁,
}