zoukankan      html  css  js  c++  java
  • Goroutine和Channel

    线程和进程基本介绍

    1. 进程就是程序程序在操作系统中的次执行过程,是系统进行资源分配和调度的基本单位
    2. 线程是进程的一个执行实例,是程序执行的最小单元,它是比进程更小的能独立运行的基本单位
    3. 一个进程可以创建核销毁多个线程,同一个进程中的多个线程可以并发执行
    4. 一个程序至少有一个进程,一个进程至少有一个线程

    并发和并行

    1. 多线程程序在单核上运行,就是并发
    2. 多线程程序在多核上运行,就是并行

    Go协程

    Go主线程

    Go主线程:一个Go线程上,可以起多个协程,协程是轻量级的线程

    Go协程的特点

    1. 有独立的栈空间
    2. 共享程序堆空间
    3. 调度由用户控制
    4. 协程是轻量级的线程

    快速入门

    func test() {
    	for i := 0; i < 10; i++ {
    		fmt.Println("test() hello world" + strconv.Itoa(i))
    		time.Sleep(time.Second)
    
    	}
    }
    
    func main() {
        
        go test()
    
    	for i := 0; i < 10; i++ {
    		fmt.Println("main() hello world" + strconv.Itoa(i))
    		time.Sleep(time.Second)
    
    	}
    }
    

    快速入门总结

    1. 开启协程的关键字时go
    2. 主线程是一个物理线程,直接作用在cpu上的。是重量级的,非常消耗cpu资源
    3. 协程从主线程开启的,是轻量级的线程,是逻辑态。对资源消耗相对小
    4. Golang的协程机制是重要的特点,可以轻松的开启上万个协程。其他编程语言的并发机制是一般基于线程的,开启过多的线程,资源消耗大,这里就凸显Golang在并发的优势了

    goroutine的调度模型

    详见

    Golang运行的cpu数

    func main() {
    
    	num := runtime.NumCPU() 	// 获取当前系统的cpu的数量
    
    	runtime.GOMAXPROCS(num)		// 运行go程序的核数 go1.8后不需要设置,1.8以前需要设置
    	fmt.Println("num = ", num)
    }
    

    让出时间片

    我们可以在每个goroutine中控制何时主动出让时间片给其他goroutine,这可以使用runtime包中的Gosched()函数实现。
    实际上,如果要比较精细地控制goroutine的行为,就必须比较深入地了解Go语言开发包中runtime包所提供的具体功能。

    Channel

    1. channel本质就是一个数据结构-队列
    2. 数据是先进先出
    3. 线程安全,多goroutine访问时,不需要加锁,就是说channel本身就是线程安全的
    4. channel有类型的,一个string的channel只能放string类型的数据

    声明与创建

    声明语法:var 变量名 chan 数据类型

    var intChan chan int // 用于存放int数据
    var mapChan chan map[int]string // 存放 map[int]string 类型
    var perChan chan Person
    var perChan2 chan *Person
    

    创建:c := make(chan type, cap),最多可以放cap数量的数,如果不带cap,则容量为1

    1. channel是引用类型
    2. channel必须初始化才能写入数据,即make后才能使用
    3. 管道是由类型的
    var intChan chan int
    intChan = make(chan int, 3) 
    
    fmt.Printf("intChan 的值 = %v intChan 本身的地址 = %p 
    ", intChan, &intChan)
    
    // 写入数据,不使用协程,如果channel已满,继续写入会出现 dead loack
    num := 211
    intChan <- 10
    intChan <- 50
    intChan <- num
    intChan <- 100
    
    fmt.Printf("channel len = %v cap = %v 
    ", len(intChan), cap(intChan))
    
    // 读取数据,不使用协程,如果channel已空,继续读出会出现 dead loack
    num1 := <-intChan
    num2 := <-intChan
    num3 := <-intChan
    fmt.Println(num1, num2, num3)
    

    注意事项

    1. channel中只能存放指定的数据类型
    2. channel的数据放满后,就不能再放入了
    3. 如果从channel取出数据后,可以继续放入
    4. 在没有使用协程的情况下,如果channel数据取完了,再取,就会报dead lock

    channel的遍历和关闭

    1. channel的关闭
      使用内置函数close可以关闭channel,当channel关闭后,就不能再想channel写数据了,但是任然可以从该channel读取数据
    intChan := make(chan int, 3)
    intChan <- 100
    intChan <- 200
    close(intChan)
    
    // panic: send on closed channel
    // intChan <- 300 
    
    v := <-intChan
    fmt.Println(v)
    

    当channel关闭且数据都读完了,再读数据会读到该数据类型的零值,且第二个返回值为false

    intChan := make(chan int, 3)
    
    intChan <- 1
    intChan <- 2
    intChan <- 3
    
    var v int
    var ok bool
    
    v, ok = <-intChan
    fmt.Println(v, ok)
    
    // close(intChan)
    v, ok = <-intChan
    fmt.Println(v, ok)
    
    v, ok = <-intChan
    fmt.Println(v, ok)
    
    v, ok = <-intChan // 输出0, false
    fmt.Println(v, ok)
    
    1. channel的遍历
      channel支持for-range的方式进行遍历,请注意两个细节
    • 在遍历时,如果channel没有关闭,则会出现deadlock的错误
    • 在遍历时,如果channel已经关闭,则会正常遍历数据,遍历完后,就会�遍历
    intChan := make(chan int, 10)
    
    for i := 1; i <= 10; i++ {
    	intChan <- i
    }
    
    close(intChan)
    
    for v := range intChan {
    	fmt.Println(v)
    }
    

    Channel使用细节和注意事项

    1. channel可以声明为只读,或者只写的性质

    单向channel只能用于发送或者接收数据。 channel本身必然是同时支持读写的,否则根本没法用。假如一个channel真的只能读,那么肯定只会是空的,因为你没机会往里面写数据。同理,如果一个channel只允许写,即使写进去了,也没有丝毫意义,因为没有机会读取里面的数据。所谓的单向channel概念,其实只是对channel的一种使用限制。

    在将一个channel变量传递到一个函数时,可以通过将其指定为单向channel变量,从而限制 该函数中可以对此 channel的操作, 比如只能往这个 channel写,或者只能从这个channel读。

    从设计的角度考虑,所有的代码应该都遵循“最小权限原则”,从而避免没必要地使用泛滥问题,进而导致程序失控。单向channel也是起到这样的一种契约作用。

    // 1. 在默认情况下,管道时双向的
    var chan1 chan int // 可读可写
    
    // 2. 声明为只写
    var chan2 chan<- int
    
    // 3. 声明为只读
    var chan3 <-chan int
    
    1. 使用Select
      select的用法与switch语言非常类似,由select开始一个新的选择块,每个选择条件由case语句来描述。与switch语句可以选择任何可使用相等比较的条件相比, select有比较多的限制,其中最大的一条限制就是每个case语句里必须是一个IO操作,大致的结构如下:
    select {
    	case <-chan1:
    	// 如果chan1成功读到数据,则进行该case处理语句
    	case chan2 <- 1:
    	// 如果成功向chan2写入数据,则进行该case处理语句
    	default:
    	// 如果上面都没有成功,则进入default处理流程
    }
    

    可以看出, select不像switch,后面并不带判断条件,而是直接去查看case语句。每个case语句都必须是一个面向channel的操作。

    1. goroutine中使用recover,解决协程中出现的panic,导致程序崩溃的问题

    同步

    Go语言包中的sync包提供了两种锁类型: sync.Mutex和sync.RWMutex。
    Mutex是最简单的一种锁类型,同时也比较暴力,当一个goroutine获得了Mutex后,其他goroutine就只能乖乖等到这个goroutine释放该Mutex。
    RWMutex相对友好些,是经典的单写多读模型。在读锁占用的情况下,会阻止写,但不阻止读,也就是多个goroutine可同时获取读锁(调用RLock()方法;而写锁(调用Lock()方法)会阻止任何其他goroutine(无论读和写)进来,整个锁相当于由该goroutine独占。

    全局唯一性操作

    对于从全局的角度只需要运行一次的代码,比如全局初始化操作, Go语言提供了一个Once类型来保证全局的唯一性操作,具体代码如下:

    var a string
    var once sync.Once
    func setup() {
    	a = "hello, world"
    }
    func doprint() {
    	once.Do(setup)
    	print(a)
    }
    func twoprint() {
    	go doprint()
    	go doprint()
    }
    
    
  • 相关阅读:
    【docker】命令学习
    docker 安装mysql
    Docker DockerFile案例 自定义的tomcat9
    尚硅谷 Docker DockerFile案例 ONBUILD命令案例
    dockerfile 案例2 CMD ENTRYPOINT命令案例
    Dockerfile案例
    Dockerfile解析
    数据卷容器
    Dockfile添加数据卷
    容器数据卷
  • 原文地址:https://www.cnblogs.com/lxlhelloworld/p/14286062.html
Copyright © 2011-2022 走看看