协程和线程

  1. os线程:每几毫秒,硬件计时器会中断处理器,然后调用schedule的内核函数,挂起当前执行的线程,将状态保存到寄存器中。检查可执行线程列表并决定下一次运行的线程。并从内存中恢复寄存器信息继续执行。因为os线程是被内核锁调度,所以一个线程执行状态切换到另一个线程的执行状态需要切换上下文。也就是说,存储用户线程的状态到寄存器,和回复寄存器中读取状态,更新调度器的速度是很慢的。因为其内部局限性很差,需要访问内存好几次,增加运行运行cpu的周期。

  2. 协程调度:

    1. Go有自己的调度器,m:n,n个os线程上调度m个协程。调度器的工作和内核的调度是相似的,但调度器只关注go程序中的协程。
    2. 和os线程调度不同的是,go并不是一个硬件定时器,而是被go“建筑”本身进行调度的。
      1. 比如一个协程执行了time.Sleep()或channel调用或mutex操作阻塞时。调度器会使其进入休眠,并开始执行另一个协程,直到时机到了再去唤醒第一个协程。因为这种调度不需要进入内核的上下文。所以重新调度一个协程要比调度一个线程的代价要低得多。
  3. 区别:

    1. 线程是由系统操作系统(os)进行调度的,需要上下文切换
    2. 协程一般是由开发者显式调度,避免了上下文切换,从而简化了高并发。[Go并发的实现原理]
  4. 传统是多线程,共享内存来实现通信, 例如JAVA,CPP;而go采用CSP,通过通信方式来共享内存。

  5. Java中在访问共享变量(Map,List等)时需要加锁进行访问,从而也衍生出一种方便操作的数据类型,即线程安全的集合,例如java.util.concurrent.包中的数据结构。

  6. Go采用CSP,通信来共享内存方式

  7. Channel 实现原理

    Go channel 实现原理分析 - golang开发笔记 - SegmentFault 思否

    1. 数据结构,实际上是基于 RingBuffer Wiki

      数据结构的实现

      1. dataqsiz // 缓冲区容量,循环队列
      2. qcount // 缓冲区的元素个数
      3. Buf unsafe.Pointer // 指向缓冲区,表示一个已经发送但还未接收的数据。
      4. sendx // 下一个可发送的地址
      5. recvx // 下一个可接收的地址
      6. sendq // 发送的sudog列表,当一个发送操作执行时,若缓冲区满了且无接收者在接送,则协程挂起,并将sudog放入sendq中 sudo G - G 代表Goroutine
      7. recvq // 接收的sudog列表,当一个接受操作执行时。若缓冲区无数据且无发送者在等待,则协程挂起,并将sudog放入recvq中
      8. closed // 管道是否关闭
      9. elemsize // 元素大小
      10. elemtype // *_type 元素类型
      11. lock mutex // 锁,用于hchan加锁, 限制只能一个goroutine同时访问一个channel
    2. 创建channel

      1. 创建channel时可以执行是否包含缓冲器。
    3. 写入数据流程

      1. 先判断recvq是否非空?Y - 取出goroutine,将值写入,唤醒G,gounpark
      2. N - 判断buf中是否有空位? Y - 写入空位
      3. N - 将输入方法sendq中,park G,挂起G,等待唤醒
    4. 读数据

      1. 先判断sendq中是否非空 - N 再判断 countq >0 Y - 从buf中读取一个元素执行;否则将当前的goroutine加入recvq,中等待唤醒。当唤醒时,证明有数据写入。执行结束
      2. Y - 判断是否有缓冲区 N- 从sendq中读取一个G,读取G中的数据,唤醒G,结束
      3. Y 从缓冲区队首取出数据,然后从sendq取出数据,写入缓冲区队尾中,然后唤醒G,执行,结束。
    5. 关闭channel 会panic

      1. 关闭值为nil的channel
      2. 关闭已经关闭的channel
      3. 向已经关闭的channel 写数据
    6. 常见的用法

      1. 单向channel,仅用来发送或接收数据
        1. func readChan(chanName <-chan int) //接收数据
        2. func writeChan(chanName chan<- int) //写入数据
      2. select
        1. 使用select 可以同时监控多个channel (多路复用)
        2. 当同时满足时,select会随机选择一个读取数据。
        3. select的case语句读channel不会阻塞,尽管channel中没有数据。这个由于case语句编译后调用读channel时,会明确传入不阻塞的参数,此时读不到数据中石,不会将当前goroutine加入等待队列,而是直接返回。
      3. range
        1. 一直读取channel中的数据,就像遍历slice一样。当没有数据可处理时,阻塞当前的goroutine,此时写channel的goroutine退出了,系统检测这种情况会发生panic,否则range将永久阻塞。