理解Golang的goroutine和channel

烂笔头啊烂笔头......

golang是最好的并发语言,拒绝任何反驳。

说到golang的并发,必须要提一个概念--协程

开销:进程>线程>协程 三者之间的关系可自行百度

目前并发比较流行的三种模式:

1、多线程:每个线程一次处理一个请求,线程越多可并发处理的请求数就越多,但是在高并发下,多线程开销会比较大。

2、协程:无需抢占式的调度,开销小,可以有效的提高线程的并发性,从而避免了线程的缺点的部分。

3、基于异步回调的IO模式,说个比较流行的快速建站语言--nodejs,通过事件驱动的方式与异步IO回调,使得服务器持续运转,来支撑高并发的请求

说正题.....

goroutine

goroutine就是golang中的协程,也可以叫做轻量级的现成,与线程比较,创建的成本以及开销都小很多,每个goroutine的堆栈只有几kb,并且堆栈可以根据程序的需要增长和缩小,而线程的堆栈是需要指明和固定的。所以golang从语言层面上支持高并发。

创建格式 go func()

goroutine正确的使用姿势一:主协成结束时,其他协程都会强制结束!

package main

import (
    "fmt"
    "time"
)

func out() {
    fmt.Println("我想先出来")
}
func main() {
    go out()
    time.Sleep(1 * time.Second)
    fmt.Println("好吧,看在sleep哥的份上,让你先出来")
}

执行结果:

我想先出来
好吧,看在sleep哥的份上,让你先出来

如果把time.Sleep注释了,main就不会再等其他协程执行完了,你可能就看不到out的输出了。

goroutine正确的使用姿势二:不同的goroutine之间不会相互影响

package main

import (
    "fmt"
    "strconv"
    "time"
)

func PrintMany() {
    for i := 1; i <= 3; i++ {
        time.Sleep(250 * time.Millisecond)
        fmt.Println("打印第" + strconv.Itoa(i) + "个")
    }
}
func PrintSingle() {
    fmt.Println("我就打印一个")
}
func main() {
    go PrintMany()
    go PrintSingle()
    time.Sleep(2 * time.Second)
    fmt.Println("我是最后一个")
}

执行结果:

我就打印一个
打印第1个
打印第2个
打印第3个
我是最后一个

PrintMany里面有Sleep,是否会导致第二个goroutine阻塞或者等待呢?no。因为两个goroutine之间不会相互影响,main会继续按顺序执行下去

goroutine伴侣--channel

回到姿势一中的例子,把Sleep注释后,你可能就看不见go out()的输出了。因为main非常之霸道,这时候就需要channel去阻止它。

package main

import (
    "fmt"
)

var bol chan bool

func One() {
    fmt.Println("I am One")
    bol <- true
}
func main() {
    bol = make(chan bool)
    go One()
    <-bol
    fmt.Println("我又要最后一个出来了")
}

执行结果:

I am One
我又要最后一个出来了

<-bol 能起到了阻塞作用。

对channe阻塞的理解:
发送者角度:对于同一个通道,发送操作(协程或者函数中的),在接收者准备好之前是阻塞的。如果chan中的数据无人接收,就无法再给通道传入其他数据。因为新的输入无法在通道非空的情况下传入。所以发送操作会等待 chan 再次变为可用状态:就是通道值被接收时(可以传入变量)。

package main

import (
    "fmt"
)

var bol chan bool

func One() {
    fmt.Println("I am One")
    bol <- true
}
func main() {
    bol = make(chan bool)
    go One()
    bol <- false //bol中的数据还未取出,无法再传入数据,所以会报错:deadlock!
    fmt.Println("我又要最后一个出来了")
}

接收者角度:对于同一个通道,接收操作是阻塞的(协程或函数中的),直到发送者可用:如果通道中没有数据,接收者就阻塞了。

package main

import (
    "fmt"
)

var bol chan bool

func One() {
    fmt.Println("I am One")
}
func main() {
    bol = make(chan bool)
    go One()
    <- bol //deadlock!
    fmt.Println("我又要最后一个出来了")
}

Channel类型的三种定义格式:

chan int //既可以发送数据也可以接受数据
<- chan int  //只允许从chan接受int类型数据
chan <- int //只允许发送int类型数据到chan

Channel默认是阻塞的。如果要“缓解”阻塞,就要给channel加一个缓冲区。

ch := make(chan string, 3) // 创建了缓冲区为3的通道

//=========
len(ch)   // 长度计算  计算缓冲区已填充的长度
cap(ch)   // 容量计算  计算缓冲区的长度

select关键字用法

select和常用的switch用法类似,但select只用来监听和channel有关的IO操作,当IO操作发生时,触发相应的动作。select的case机会均等,既如果多个case都满足运行条件,则select会随机选出一个执行,其他不会执行。没有default的select有几率发生死锁事件。

select的应用场景
1、实现timeout机制

package main

import (
    "fmt"
    "time"
)

func main() {
    timeout := make (chan bool, 1)
    go func() {
        time.Sleep(1*time.Second) // 休眠1s,如果超过1s还没I操作则认为超时,通知select已经超时啦~
        timeout <- true
    }()
    ch := make (chan int)
    select {
    case <- ch:
    case <- timeout:
        fmt.Println("超时啦!")
    }
}
//====================
package main

import (
    "fmt"
    "time"
)

func main() {
    ch := make (chan int)
    select {
    case <-ch:
    case <-time.After(time.Second * 1): // 利用time来实现,After代表多少时间后执行输出东西;After返回一个Time类型的只读channel
        fmt.Println("超时啦!")
    }
}

2.判断channel是否阻塞(或者说channel是否已经满了)

package main

import (
    "fmt"
)

func main() {
    ch := make (chan int, 1)  // 注意这里给的容量是1
    ch <- 1
    select {
    case ch <- 2:
    default:
        fmt.Println("通道channel已经满啦,塞不下东西了!")
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。