67

一看就懂系列之Golang的goroutine和通道

 5 years ago
source link: https://studygolang.com/articles/18135?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u011957758/article/details/81159481

https://blog.csdn.net/u011957758/article/details/81159481

前言

如果说php是最好的语言,那么golang就是最并发的语言。

支持golang的并发很重要的一个是goroutine的实现,那么本文将重点围绕goroutine来做一下相关的笔记,以便日后快速留恋。

10s后,以下知识点即将靠近:

1.从并发模型说起

2.goroutine的简介

3.goroutine的使用姿势

4.通道(channel)的简介

5.重要的四种通道使用

6.goroutine死锁与处理

7.select的简介

8.select的应用场景

9.select死锁

正文

1.从并发模型说起

看过很多大神简介,各种研究高并发,那么就通俗的说下并发。

并发目前来看比较主流的就三种:

1.多线程

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

2.协程

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

3.基于异步回调的IO模型

说一个熟悉的,比如nginx使用的就是epoll模型,通过事件驱动的方式与异步IO回调,使得服务器持续运转,来支撑高并发的请求

为了追求更高效和低开销的并发,golang的goroutine来了。

2.goroutine的简介

定义:在go里面,每一个并发执行的活动成为goroutine。

详解:goroutine可以认为是轻量级的 线程 ,与创建线程相比,创建 成本和开销都很小 ,每个goroutine的堆栈只有 几kb ,并且堆栈可根据程序的需要增长和缩小(线程的堆栈需指明和固定),所以go程序从语言层面支持了高并发。

程序执行的背后:当一个程序启动的时候,只有一个goroutine来调用main函数,称它为主goroutine,新的goroutine通过go语句进行创建。

3.goroutine的使用姿势

3.1单个goroutine创建

在函数或者方法前面加上关键字go,即创建一个并发运行的新goroutine。

上代码:

package main

import (
    "fmt"
    "time"
)

func HelloWorld() {
    fmt.Println("Hello world goroutine")
}

func main() {
    go HelloWorld()      // 开启一个新的并发运行
    time.Sleep(1*time.Second)
    fmt.Println("我后面才输出来")
}

以上执行后会输出:

Hello world goroutine
我后面才输出来

需要注意的是,执行速度很快,一定要加sleep,不然你一定可以看到goroutine里头的输出。

这也说明了一个关键点: 当main函数返回时,所有的gourutine都是暴力终结的,然后程序退出。

3.2多个goroutine创建

package main

import (
    "fmt"
    "time"
)

func DelayPrint() {
    for i := 1; i <= 4; i++ {
        time.Sleep(250 * time.Millisecond)
        fmt.Println(i)
    }
}

func HelloWorld() {
    fmt.Println("Hello world goroutine")
}

func main() {
    go DelayPrint()    // 开启第一个goroutine
    go HelloWorld()    // 开启第二个goroutine
    time.Sleep(2*time.Second)
    fmt.Println("main function")
}

函数输出:

Hello world goroutine
1
2
3
4
5
main function

有心的同学可能会发现,DelayPrint里头有sleep,那么会导致第二个goroutine堵塞或者等待吗?

答案是:no

疑惑: 当程序执行go FUNC()的时候,只是简单的调用然后就立即返回了,并不关心函数里头发生的故事情节,所以不同的goroutine直接不影响,main会继续按顺序执行语句。

4.通道(channel)的简介

4.1简介

如果说goroutine是Go并发的执行体,那么”通道”就是他们之间的连接。

通道可以让一个goroutine发送特定的值到另外一个goroutine的通信机制。

uIJ7FjA.jpg!web

4.2声明&传值&关闭

声明

var ch chan int      // 声明一个传递int类型的channel
ch := make(chan int) // 使用内置函数make()定义一个channel

//=========

ch <- value          // 将一个数据value写入至channel,这会导致阻塞,直到有其他goroutine从这个channel中读取数据
value := <-ch        // 从channel中读取数据,如果channel之前没有写入数据,也会导致阻塞,直到channel中被写入数据为止

//=========

close(ch)            // 关闭channel

有没注意到关键字” 阻塞 “?,这个其实是默认的channel的接收和发送,其实也有非阻塞的,请看下文。

5.重要的四种通道使用

1.无缓冲通道

说明:无缓冲通道上的发送操作将会被阻塞,直到另一个goroutine在对应的通道上执行接收操作,此时值才传送完成,两个goroutine都继续执行。

上代码:

package main

import (
    "fmt"
    "time"
)
var done chan bool
func HelloWorld() {
    fmt.Println("Hello world goroutine")
    time.Sleep(1*time.Second)
    done <- true
}
func main() {
    done = make(chan bool)  // 创建一个channel
    go HelloWorld()
    <-done
}

输出:

Hello world goroutine

由于main不会等goroutine执行结束才返回,前文专门加了sleep输出为了可以看到goroutine的输出内容,那么在这里由于是 阻塞 的,所以无需sleep。

(小尝试:可以将代码中”done <- true”和”<-done”,去掉再执行,看看会发生啥?)

2.管道

通道可以用来连接goroutine,这样一个的输出是另一个输入。这就叫做管道。

fAvIvie.jpg!web

例子:

package main

import (
    "fmt"
    "time"
)
var echo chan string
var receive chan string

// 定义goroutine 1 
func Echo() {
    time.Sleep(1*time.Second)
    echo <- "咖啡色的羊驼"
}

// 定义goroutine 2
func Receive() {
    temp := <- echo // 阻塞等待echo的通道的返回
    receive <- temp
}


func main() {
    echo = make(chan string)
    receive = make(chan string)

    go Echo()
    go Receive()

    getStr := <-receive   // 接收goroutine 2的返回

    fmt.Println(getStr)
}

在这里不一定要去关闭channel,因为底层的垃圾回收机制会根据它 是否可以访问来决定是否自动回收它 。(这里不是根据channel是否关闭来决定的)

3.单向通道类型

当程序则够复杂的时候,为了代码可读性更高,拆分成一个一个的小函数是需要的。

此时go提供了单向通道的类型,来实现函数之间channel的传递。

上代码:

package main

import (
    "fmt"
    "time"
)

// 定义goroutine 1
func Echo(out chan<- string) {   // 定义输出通道类型
    time.Sleep(1*time.Second)
    out <- "咖啡色的羊驼"
    close(out)
}

// 定义goroutine 2
func Receive(out chan<- string, in <-chan string) { // 定义输出通道类型和输入类型
    temp := <-in // 阻塞等待echo的通道的返回
    out <- temp
    close(out)
}


func main() {
    echo := make(chan string)
    receive := make(chan string)

    go Echo(echo)
    go Receive(receive, echo)

    getStr := <-receive   // 接收goroutine 2的返回

    fmt.Println(getStr)
}

程序输出:

咖啡色的羊驼

4.缓冲管道

goroutine的通道默认是是阻塞的,那么有什么办法可以缓解阻塞?

答案是:加一个缓冲区。

对于go来说创建一个缓冲通道很简单:

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

//=========
len(ch)   // 长度计算
cap(ch)   // 容量计算

yiQrMnF.jpg!web

6.goroutine死锁与友好退出

6.1goroutine死锁

来一个死锁现场一:

package main

func main() {
    ch := make(chan int)
    <- ch // 阻塞main goroutine, 通道被锁
}

输出:

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan receive]:
main.main()

死锁现场2:

package main

func main() {
    cha, chb := make(chan int), make(chan int)

    go func() {
        cha <- 1 // cha通道的数据没有被其他goroutine读取走,堵塞当前goroutine
        chb <- 0
    }()

    <- chb // chb 等待数据的写
}

为什么会有死锁的产生?

非缓冲通道上如果发生了流入无流出,或者流出无流入,就会引起死锁。

或者这么说:goroutine的非缓冲通道里头一定要一进一出,成对出现才行。

上面例子属于:一:流出无流入;二:流入无流出

当然,有一个例外:

func main() {
    ch := make(chan int)
    go func() {
       ch <- 1
    }()
}

执行以上代码将会发现,竟然没有报错。

what?

不是说好的一进一出就死锁吗?

仔细研究会发现,其实根本没等goroutine执行完,main函数自己先跑完了,所以就没有数据流入主的goroutine,就不会被阻塞和报错

6.2goroutine的死锁处理

有两种办法可以解决:

1.把没取走的取走便是

package main

func main() {
    cha, chb := make(chan int), make(chan int)

    go func() {
        cha <- 1 // cha通道的数据没有被其他goroutine读取走,堵塞当前goroutine
        chb <- 0
    }()

    <- cha // 取走便是
    <- chb // chb 等待数据的写
}

2.创建缓冲通道

package main

func main() {
    cha, chb := make(chan int, 3), make(chan int)

    go func() {
        cha <- 1 // cha通道的数据没有被其他goroutine读取走,堵塞当前goroutine
        chb <- 0
    }()

    <- chb // chb 等待数据的写
}

这样的话,cha可以缓存一个数据,cha就不会挂起当前的goroutine了。除非再放两个进去,塞满缓冲通道就会了。

7.select的简介

定义:在golang里头select的功能与epoll(nginx)/poll/select的功能类似,都是坚挺IO操作,当IO操作发生的时候,触发相应的动作。

select有几个重要的点要强调:

1.如果有多个case都可以运行,select会随机公平地选出一个执行,其他不会执行

上代码:

package main

import "fmt"

func main() {
    ch := make (chan int, 1)

    ch<-1
    select {
    case <-ch:
        fmt.Println("咖啡色的羊驼")
    case <-ch:
        fmt.Println("黄色的羊驼")
    }
}

输出:

(随机)二者其一

2.case后面必须是channel操作,否则报错。

上代码:

package main

import "fmt"

func main() {
    ch := make (chan int, 1)
    ch<-1
    select {
    case <-ch:
        fmt.Println("咖啡色的羊驼")
    case 2:
        fmt.Println("黄色的羊驼")
    }
}

输出报错:

2 evaluated but not used
select case must be receive, send or assign recv

3.select中的default子句总是可运行的。所以没有default的select才会阻塞等待事件

上代码:

package main

import "fmt"

func main() {
    ch := make (chan int, 1)
    // ch<-1   <= 注意这里备注了。
    select {
    case <-ch:
        fmt.Println("咖啡色的羊驼")
    default:
        fmt.Println("黄色的羊驼")
    }
}

输出:

黄色的羊驼

4.没有运行的case,那么江湖阻塞事件发生报错(死锁)

package main

import "fmt"

func main() {
    ch := make (chan int, 1)
    // ch<-1   <= 注意这里备注了。
    select {
    case <-ch:
        fmt.Println("咖啡色的羊驼")
    }
}

输出报错:

fatal error: all goroutines are asleep - deadlock!

8.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代表多少时间后执行输出东西
        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已经满啦,塞不下东西了!")
    }
}

3.退出机制

package main

import (
    "fmt"
    "time"
)

func main() {
    i := 0
    ch := make(chan string, 0)
    defer func() {
        close(ch)
    }()

    go func() {
        DONE: 
        for {
            time.Sleep(1*time.Second)
            fmt.Println(time.Now().Unix())
            i++

            select {
            case m := <-ch:
                println(m)
                break DONE // 跳出 select 和 for 循环
            default:
            }
        }
    }()

    time.Sleep(time.Second * 4)
    ch<-"stop"
}

输出:

1532390471
1532390472
1532390473
stop
1532390474

这边要强调一点:退出循环一定要用break + 具体的标记,或者goto也可以。否则其实不是真的退出。

package main

import (
    "fmt"
    "time"
)

func main() {
    i := 0
    ch := make(chan string, 0)
    defer func() {
        close(ch)
    }()

    go func() {

        for {
            time.Sleep(1*time.Second)
            fmt.Println(time.Now().Unix())
            i++

            select {
            case m := <-ch:
                println(m)
                goto DONE // 跳出 select 和 for 循环
            default:
            }
        }
        DONE:
    }()

    time.Sleep(time.Second * 4)
    ch<-"stop"
}

输出:

1532390525
1532390526
1532390527
1532390528
stop

9.select死锁

select不注意也会发生死锁,前文有提到一个,这里分几种情况,重点再次强调:

1.如果没有数据需要发送,select中又存在接收通道数据的语句,那么将发送死锁

package main
func main() {  
    ch := make(chan string)
    select {
    case <-ch:
    }
}

预防的话加default。

空select,也会引起死锁

package main

func main() {  
    select {}
}

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK