17-chan原理1-创建

1. 并发模型

对于大量的服务请求,有两种服务模式:单一服务模式、多服务模式。对于单一服务模式,比较典型的如Redis,单进程,单线程服务。这种模式简单高效,但是适用场景有限,很容易达到服务能力的瓶颈。多服务模式有很多种实现方式。适应于不同的业务场景。
由于是多服务,不同服务间难免存在对同一资源的访问,这就涉及到服务间的同步问题。不同服务实现方式,同步的机制不同。
线程是操作系统对进程管理的最小粒度,程序通过线程实现对CPU、内存、网络、存储等资源的使用。因此线程是最基本的一种并发模式,是其他并发模式的基础。
多线程处于同一内存空间,可以方便的直接访问内次进行通信,可以通过互斥锁、自旋锁、信号量等方式进行同步。
由于资源访问控制的要求,需要进行锁互斥,或者进行IO访问出现阻塞时,线程都会处于阻塞状态,大量线程被阻塞时,服务会出现阻塞情况。为解决该问题,就有了异步调用、IO多路复用、协程等多种技术避免大量线程的同时阻塞。
一般随着数据量的增加、计算量的增加、以及高可用的考虑,会有不同并发规模。如单进程、单机、单机房、多机房、跨地市等不同的并发规模。针对不同的规模、业务要求,会有不同的并发模式。
并发模式是一个抽象的概念,可大可小。从编程语言的角度来看,并发主要考虑的是一个进程内的并发机制。超出进程外的,属于应用的系统架构,应用的架构需要基于编程语言提供的能力来构建。

并发模型的种类

  • Actor model
  • Petri net
  • Process calculi
    进程验算包括多种方式。
    1. Ambient calculus
    2. Calculus of communicating systems (CCS)
    3. Communicating sequential processes (CSP)
    4. π-calculus
    5. Join-calculus
  • Input/output automaton
  • Preemptive machine scheduling

并发交互与通信

不同计算单元之间,需要通信以保持同步。主要分为两种方式:

  1. 共享内存
  2. 消息传递
    Go语言实现的并发模型是基于CSP模型。

2. CSP模型

CSP 模型全称为 communicating sequential processes.CSP是一种形式化语言,用来描述并行计算中的交互模式。它是并行数学理论中的一员,基于通道(channel)传递消息。CSP对多种编程语言的设计中有很深的影响。如:occam,Limbo,Go等。
CSP最早由Tony Hoare于1978年的论文中描述。CSP已在工业上实际应用,作为规定(specifying)和验证(verifying)各种不同系统并发方面的工具。

CSP允许以独立运行的组件进程来描述系统,并且通过消息传递通信相互作用。

CSP与Actor区别

  1. CSP有两部分组成:process,channel。Actor由Actor实体和实体间关系组成。
  2. 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可以看出,主要分为三种形式:

  1. chan缓存大小==0
    没有指定大小,或者指定缓存大小为0。这时会将hchan.buf指向自己(&hchan.buf)。
  2. chan数据类型中不包含指针
    不含指针则申请一块连续内存,将将缓存和hchan放在一起。hchan.buf指向hchan结束位置。
  3. 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结构

  1. 如果chan是无缓存,则chan的读写是同步的。
  2. 有缓存的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  //互斥锁,
}

你可能感兴趣的:(Go,实现原理,chan,go,实现原理,chan)