Golang基础篇-goroutine

1,145 阅读7分钟

本文主要介绍Go语言中goroutine。文中如有描述不对或则不合理的地方,请各位大佬积极留言,我会每日及时查看并核查纠正。

并发&并行

日常开发过程中沟通的并发,是指基于并行机制下的并发。

  • 并发: 任务数多余CPU核数,通过操作系统的各种任务调度算法实现多个任务一起执行;实际上任务之间切换执行;比如:单核CPU上的多任务系统

  • 并行: 任务数小于等于CPU核数,即任务真的一起执行的;前提条件必须是多核CPU。

进程&线程&协程

  • 进程: 一个程序在一个数据集中的一次动态执行过程,可以简单理解为“正在执行的程序”,它是CPU资源分配和调度的独立单位。

    • 进程一般由程序数据集进程控制块三部分组成
      • 程序 用来描述进程要完成哪些功能以及如何完成;
      • 数据集 则是程序在执行过程中所需要使用的资源;
      • 进程控制块 用来记录进程的外部特征,描述进程的执行变化过程,系统可以利用它来控制和管理进程,它是系统感知进程存在的唯一标志。
    • 进程的局限是创建、撤销和切换的开销比较大。
  • 线程: 操作系统能够进行运算调度的最小单位,它被包含在进程中,是进程的实际运作单位

    • 它是一个基本的CPU执行单元,也是程序执行过程中的最小单元,由线程ID、程序计数器、寄存器集合和堆栈共同组成。一个进程可以包含多个线程。

    • 线程的优点是减小了程序并发执行时的开销,提高了操作系统的并发性能,缺点是线程没有自己的系统资源,只拥有在运行时必不可少的资源,但同一进程的各线程可以共享进程所拥有的系统资源,如果把进程比作一个车间,那么线程就好比是车间里面的工人。不过对于某些独占性资源存在锁机制,处理不当可能会产生“死锁”。

    • 线程解决了问题

      • 最早的通用网关接口(CGI)程序很简单,通过脚本将原来单机版的程序包装在一个进程里,来一个用户就启动一个进程。但明显承载不了多少用户,并且如果进程间需要共享资源还得通过进程间通信机制

      • (图形用户界面)GUI出现后急切需要并发机制来保证用户界面的响应

    • 线程增加了复杂度问题

      • 竞态条件(race conditions): 线程之间的任务总有一些资源需要共享。

      • 依赖关系以及执行顺序: 线程之间的任务有依赖关系,需要等待以及通知机制来进行协调,保证顺序。

    • 解决所带来的复杂度问题

      • 线程锁(互斥锁,Mutex):

      • 信号量(semaphore):

    • 线程的成本

      • 内存: 线程的栈空间

      • 调度成本(context-switch): 切换成本和线程的栈空间使用大小有直接关系

    • 如何控制系统的线程个数

      • 线程池:

      • 设置和CPU核数相等的线程数,保证线程一直处于运行状态

        • 异步回调方案:

        • GreenThread/Coroutine/Fiber方案:

  • 协程: 是一种用户态的轻量级线程,又称微线程,英文名Coroutine,协程的调度完全由用户控制。人们通常将协程和子程序(函数)比较着理解。

    • 子程序调用总是一个入口,一次返回,一旦退出即完成了子程序的执行。

CSP

与主流语言通过共享内存来进行并发控制方式不同,Go 语言采用了 CSP 模式。这是一种用于描述两个独立的并发实体通过共享的通讯 Channel(管道)进行通信的并发模型。

goroutine

  • 调度器不能保证多个goroutine执行顺序,且进程退出后不会等待它们结束。
func test0001() {
	for i := 0; i < 10; i++ {
		// 创建10个子goroutine
		go func(i int) {
			// 打印结果是: 乱序打印0,1,2,3,4,5,6,7,8,9
			fmt.Println(i)
		}(i)
	} 
	// 延迟主goroutine执行时间,等待子goroutine执行完成
	time.Sleep(time.Millisecond)
}

channel

Do not communicate by sharing memory; instead, share memory by communicating. 不要通过共享内存来通信,而要通过通信来实现内存共享

  • goroutine之间的通道,它可以让goroutine之间相互通信。

  • Go从语言层面保证同一个时间只有一个goroutine能够访问channel里面的数据,为开发者提供了一种优雅简单的工具,所以Go的做法就是使用channel来通信,通过通信来传递内存数据,使得内存数据在不同的goroutine中传递,而不是使用共享内存来通信。

  • 信道

    • 无缓冲信道: 同步模式,没有能力保存任何值。这种类型的信道只有发送和接收方同时准备好,才能进行下次信道的操作,否则阻塞状态。

      // 接收方 <说明> 参数ch: 仅仅只能用于接收数据
      func receive(ch <-chan int) {
          for {
              // 顺序打印 1, 2, 3
              fmt.Printf("ch received data of %d \n", <-ch)
          }
      }
      
      func test00015() {
          ch := make(chan int)
          go receive(ch)
          ch <- 1
          ch <- 2
          ch <- 3
          time.Sleep(time.Millisecond)
      }
      
      • 引发死锁问题
      // 接收方
      func receive(ch <-chan int) {
          for {
              fmt.Printf("ch received data of %d \n", <-ch)
          }
      }
      
      func test00016() {
          ch := make(chan int)
          // 向ch发送3个数据: 准备发送第一个数据,阻塞状态,检测到为无缓冲信道,并且没有其他goroutine运行接收方,故直接deadlock
          ch <- 1
          ch <- 2
          ch <- 3
          go receive(ch)
      }
      
    • 有缓冲信道: 异步模式,被创建时就开辟能存储n个值的信道。这种类型并不要求发送与接收方同时进行,只要缓冲区有未使用空间用于发送数据,或还包含可以接收的数据,那么其通信就会无阻塞地进行。

  • 创建channel:

ch := make(chan int)
  • 申明方式
chan T // 声明一个双向通道
chan<- T // 声明一个只能用于发送的通道
<-chan T // 声明一个只能用于接收的通道
  • 比较channel:
func test0004() {
	ch01 := make(chan int)
	ch02 := make(chan int)
	fmt.Println(ch01 == ch02)  // false

	var ch03 chan int
	fmt.Println(ch03 == nil)  // true
}
  • 接收数据
func worker3(ch chan int) {
	for data := range ch {
		fmt.Printf("receive %c\n", data)
	}
}
  • 传递

package main

import (
    "fmt"
)

func main() {
    ch1 := make(chan int)
    fmt.Printf("%T,%p\n",ch1,ch1)

    test1(ch1)
}

func test1(ch chan int){
    fmt.Printf("%T,%p\n",ch,ch)
}

关键源码路径

分析

  • 现象一: 睡1秒,资源交给receq goroutine先执行 <-ch,没有sendq,阻塞,资源交出,给main goroutine
    • ch <- 100后确定有receq,所以唤醒recvq,但是不一定是让receq goroutine其执行;存在资源竞争,下一个资源交给main还是recvq不一定

    • 有可能是 receq goroutine 拿到资源,打印receive data

    • 有可能是 main goroutine 拿到资源,打印 main goroutine

  • 现象二: 先执行 ch <- 10,没有receq,阻塞,资源交出,给其他goroutine,但是这个案例只有一个goroutine,即receq goroutine,所以交给它,睡眠1秒;资源交给main goroutine吗?不行,因为main goroutine已经阻塞,不满足唤醒条件;只能在当前goroutine继续等待,直到1秒结束
    • 执行<-ch,确定有sendq,唤醒main goroutine;只是一个唤醒操作,下一步还是存在资源竞争,即当前资源交出,开始竞争
    • 可能main goroutine拿到,打印 main goroutine
    • 可能继续它拿到,收到数据,打印receive data

  • deadlock,因为ch <- 100,调用chansend,没有receq,资源交出,但是由于当前仅仅只有一个main goroutine,交出不了;直接deadlock