Golang的goroutine协程和channel通道

一:简介

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

二:Goroutine

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

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

例子:数据结构

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()函数,这个函数能够按照指定时间来暂停函数或协程执行。app

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

三:通道channel

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

3.1 channel语法

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

  • 一、基础语法
var chan1 chan string
chan1 = make(chan string)
//or
chan1 := make(chan string)

//int 
intchan := make(chan int)

//函数也能够
funcchan := chan func()
  • 二、不带缓冲的channel
var chan2 chan string

chan2 := make(chan string)

chan3 := make(chan string, 0)
  • 三、带缓冲区的channel
//在make第二个参数加上数字,就变成一个带缓冲的channel,
//也是一个双向channel,既能够读也能够写
chan3 := make(chan string, 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特性

基础特性code

操做 值为 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.一、无缓冲区

  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.二、有缓冲区

 有缓冲区
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.三、 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.四、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.五、超时处理

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/

相关文章
相关标签/搜索