• Golang的goroutine协程和channel通道


    一:简介

    因为并发程序要考虑很多的细节,以保证对共享变量的正确访问,使得并发编程在很多情况下变得很复杂。
    但是Go语言在开发并发时,是比较简洁的。它通过channel来传递数据。数据竞争这个问题在golang的设计上就进行了规避了。它提倡用通信的方式实现共享,而不要以共享方式来通信
    Go语言用2种手段来实现并发程序,goroutine和channel,其支持顺序通信进程(communicating sequential processes),简称为CSP。CSP是一种现代的并发编程模型,在这种编程模型中,值会在不同的运行实例(goroutine)中传递。

    二:Goroutine

    在Go语言中,每一个并发的执行单元就叫做goroutine。
    每个goroutine都对应一个非常简单的模型:它是一个并发的执行函数,并且在多个并发的goroutine间,资源是共享的。goroutine非常轻量,创建的开销很少。

    goroutine的用法:
    直接在函数前加上一个关键字:go。
    go func() {}

    例子:

    package main
    
    import (
    	"fmt"
    	"time"
    )
    
    func main() {
    	fmt.Println("In main")
    	go longSleep()
    	go shortSleep()
    
    	fmt.Println("sleep ")
    	time.Sleep(10 * 1e9)//ns,符号 1e9 表示 1 乘 10 的 9 次方,e=指数
    	fmt.Println("the end of main")
    }
    
    func longSleep() {
    	fmt.Println("longSleep begin")
    	time.Sleep(5 * 1e9)
    	fmt.Println("longSleep end")
    }
    
    func shortSleep() {
    	fmt.Println("shortSleep begin")
    	time.Sleep(2 * 1e9)
    	fmt.Println("shortSleep end")
    }
    

    运行结果:

    In main
    sleep
    longSleep begin
    shortSleep begin
    shortSleep end
    longSleep end
    the end of main
    

    main() ,longSleep() 和 shortSleep() 这3个函数都是独立的处理单元按顺序启动,然后开始并行运行。为了模拟
    运算时间的损耗,我们使用了sleep()函数,这个函数可以按照指定时间来暂停函数或协程执行。

    如果我们不在main()函数中sleep()较长的时间,那么main() 函数结束时,其他协程运行的程序也会结束。main()程序退出,它不会等待任何其他非main协程的结束。
    协程是独立的处理单元,一旦陆续启动一些协程,就无法确定他们是什么时候正在开始运行的。

    三:通道channel

    上面我们讲到,协程都是独立运行的,他们之间没有通信。
    协程可以使用共享变量来通信,但是不建议这么做。在Go中有一种特殊的类型channle通道,可以通过它来进行goroutine之间的通信,可以避免共享内存的坑。channel的通信保证了同步性。
    数据通过通道,同一时间只有一个协程可以访问数据,所以不会出现数据竞争,设计时就是这样的。

    3.1 channel语法

    channel也是通过make进行分配的,其返回的是指向底层相关数据结构的引用。

    • 1、基础语法
    var chan1 chan string
    chan1 = make(chan string)
    //or
    chan1 := make(chan string)
    
    //int 
    intchan := make(chan int)
    
    //函数也可以
    funcchan := chan func()
    
    
    • 2、不带缓冲的channel
    var chan2 chan string
    
    chan2 := make(chan string)
    
    chan3 := make(chan string, 0)
    
    • 3、带缓冲区的channel
    //在make第二个参数加上数字,就变成一个带缓冲的channel,
    //也是一个双向channel,既可以读也可以写
    chan3 := make(chan string, 4)
    
    • 4、单向channel
    //只发送的channel,在类型后面加上一个箭头 <-,只能向channel写数据
    var chan4 chan <-int
    
    chan4 := make(chan <-int)
    
    //只接收的channel,箭头放在chan前面,只能从channel读取数据
    var chan4 <-chan int
    
    chan4 := make(<-chan int) //初始化
    

    3.2 channel特性

    基础特性

    操作 值为 nil 的 channel 被关闭的 channel 正常的 channel
    close panic panic 成功关闭
    c<- 永远阻塞 panic 阻塞或成功发送
    <-c 永远阻塞 永远不阻塞 阻塞或成功接收

    happens-before 特性

    1. 无缓冲时,接收 happens-before 发送
    2. 任何情况下,发送 happens-before 接收
    3. close happens-before 接收

    3.3 channel用法

    3.3.1、无缓冲区

    channel无缓冲区,发送方和接收方需要一一配对,不然发送方会一直阻塞,直到数据被接收方取出。
    其实无缓冲区channel不管是存消息还是取消息,都会挂起当前goroutine,除非另外一端已经准备好。
    无缓冲区的channel永远不会存数据,只负责数据的流通。

    • 从无缓冲channel取数据,必须要有数据流进来才可以,否则当前协程阻塞
    • 数据流入无缓冲channel, 如果没有其他goroutine来拿走这个数据,那么当前协程阻塞

    注意:
    同步的channel不能只在一个协程中发送和接收,因为会被永远阻塞,数据不能到接收方那里。

    package main
    
    import "fmt"
    
    func main() {
    	chan1 := make(chan int)
    
    	go func() {
    		for d := range chan1 {
    			fmt.Println(d)
    		}
    	}()
    
    	chan1 <- 1 //发送要放在接收协程跑起来后面,因为发送后会阻塞等待接收
    	chan1 <- 2
    	chan1 <- 3
    
    	close(chan1)
    }
    
    
    import "fmt"
    
    func sum(s []int, c chan int) {
    	sum := 0
    	for _, v := range s {
    		sum += v
    	}
    	c <- sum // send sum to c
    }
    func main() {
    	s := []int{7, 2, 8, -9, 4, 0}
    	c := make(chan int)
    	go sum(s[:len(s)/2], c)
    	go sum(s[len(s)/2:], c)
    	x, y := <-c, <-c // receive from c
    	fmt.Println(x, y, x+y)
    }
    

    3.3.2、有缓冲区

    有缓冲区
    channel创建一个缓冲区,如果缓冲区已满,发送方的主进程或者协程会被阻塞,发送方只能在接收方取走数据后才能从阻塞状态恢复;如果未满就不会阻塞;如果为空,接收方的协程会被阻塞。
    上面的这种特性,比如可以控制主进程的退出,因为有时我们碰到主协程退出了,其他的子协程还没有运行完成。

    package main
    
    import (
    	"fmt"
    )
    
    //-------------
    var ichan = make(chan int, 3)
    var str string
    
    func f() {
    	str = "hello world"
    	ichan <- 0
    }
    
    func main() {
    	go f()
    	<-ichan  //这里有值,下面才会运行
    
    	fmt.Println(str)
    }
    
    package main
    
    import (
    	"fmt"
    )
    
    func main() {
    	chan1 := make(chan int, 3)
    	quit := make(chan bool) //阻塞主进程,防止未处理完的子协程
    
    	go func() {
    		for d := range chan1 { //如果data的缓冲区为空,这个协程会一直阻塞,除非被channel被close
    			fmt.Println(d)
    		}
    		quit <- true
    	}()
    
    	chan1 <- 1
    	chan1 <- 2
    	chan1 <- 3
    	chan1 <- 4
    	chan1 <- 5
    	close(chan1) //用完需要关闭,否则goroutine会被死锁,因为上面用range,它是不等到信道关闭是不会结束读取的
    	<-quit       //解除阻塞
    }
    

    3.3.3、 for...range

    上面有的例子是一个一个的取数据,其实golang还提供了for range 来读取channel中的数据。

    package main
    
    import (
    	"fmt"
    	"time"
    )
    
    func main() {
    
    	go func() {
    		time.Sleep(1 * time.Hour)
    	}()
    
    	c := make(chan int)
    	go func() {
    		for i := 0; i < 10; i = i + 1 {
    			c <- i
    		}
    		close(c)//如果把close(c)注释掉,程序会一直阻塞在for …… range那一行
    	}()
    
    	for i := range c {
    		fmt.Println(i)
    	}
    
    	fmt.Println("end!")
    }
    
    //range c 产生的迭代值为channel中发送的值,它会一直迭代直到channel被关闭。
    //注意:上面的例子中如果把close(c)注释掉,程序会一直阻塞在for …… range那一行
    

    3.3.4、select监听channel

    select监测各个channel的数据。
    如果有多个channel接收数据,select会随机选择一个case来处理。
    你还可以给select加上一个default语句,如果没有case需要处理,那么就会选择default语句。
    多个case情况下,如果没有default也没有case需要处理的,那么select会阻塞,只到某个case需要处理。
    注意:nil channel 的操作会一直被阻塞,如果没有default的话,select会一直被阻塞。

    package main
    
    import (
    	"fmt"
    )
    
    func foo(i int) chan int {
    	c := make(chan int)
    	go func() {
    		c <- i
    	}()
    	return c
    }
    
    func main() {
    	c1, c2, c3 := foo(1), foo(2), foo(3)
    
    	ichan := make(chan int)
    	//开一个goroutine监听各个channel数据输出并收集数据到channel
    	go func() {
    		for {//for语句循环处理select, 如果只有一个select,那么它只会选一个case处理就结束了
    			select { //监听c1,c2,c3流出,并全部流入到ichan
    			case v1 := <-c1:
    				ichan <- v1
    			case v2 := <-c2:
    				ichan <- v2
    			case v3 := <-c3:
    				ichan <- v3
    			}
    		}
    	}()
    
    	//阻塞主协程,取出ichan的数据
    	for i := 0; i < 3; i++ {
    		fmt.Println(<-ichan) // 从打印来看我们的数据输出并不是严格的1,2,3顺序
    	}
    
    	fmt.Println("end!")
    }
    

    输出结果:

    2
    1
    3
    end!
    

    3.3.5、超时处理

    select还有一个应用超时处理的功能。上面说到如果没有case需要处理,那么select会一直阻塞,这时候我们就可以在一个case下定义一个超时情况,其他case没有数据处理时,到时间点了这个超时case就会处理了,就不会一直阻塞。
    我们用time.After,它返回一个类型为 <-chan time 的单向channel,在指定时间发送一个当前时间给channel

    package main
    
    import (
    	"fmt"
    	"time"
    )
    
    func main() {
    	chan1 := make(chan string, 1)
    	go func() {
    		time.Sleep(time.Second * 3)
    		chan1 <- "res1"
    	}()
    
    	select {
    	case res := <-chan1: //3秒之后才会有数据进入槽chan1
    		fmt.Println(res)
    	case <-time.After(time.Second * 1)://定义超时情况,1秒后超时.这个超时时间比上面的case短,所以先运行这个case
    		fmt.Println("timeout 1")
    	}
    }
    
    

    输出:
    timeout 1

    3.4 close channel

    上面的特性我们列举了close channel的情况。

    • channel已经被关闭

    close()掉了,你继续往里面写数据,会出现panic。
    但是,从这个关闭的channel可以读出已发送的数据,还可以不断的读取零值。
    如果是通过range读取数据,channel关闭后for循环会跳出。

    通过i, ok := <-c 可以查看channel的状态,判断是零值还是正常读取的值。

    c := make(chan int, 10)
    close(c)
    i, ok := <-c
    fmt.Printf("%d, %t", i, ok) //0, false
    

    参考

    https://colobu.com/2016/04/14/Golang-Channels/

  • 相关阅读:
    i++ 与++i
    jquery下的domcument
    jquery
    MVC MVP MVVM
    两个for还是一个for?
    华为云服务器FTP连接
    vue-i18n 使用方法
    在本地运行vue build 文件
    vue项目中使用模拟数据 MOCK
    超简单 超详细 vue项目中使用svg图标 阿里巴巴图标库
  • 原文地址:https://www.cnblogs.com/jiujuan/p/11723586.html
Copyright © 2020-2023  润新知