go并发编程实践笔记 | 青训营

68 阅读5分钟

这是我在学习go语言的并发编程的过程中理解到的一些东西的分享

Goroutinue

go语言的协程,创建时为2kb或者4kb,所以成本是非常低的。Goroutine本身的语法非常非常简单。我们可以事先定义一个函数f() ,那么就可以通过go f()语句来开启一个协程。当然,闭包也是可以的。\

package main  
  
func main() {  
    go func() {  
        for i := 0; i < 10; i++ {  
            println("hello")  
        }  
    }()  
}

这里就开启了一个协程,开启的关键字就是go。根据我们的实际需求,也可以同时开启上千个协程。
但是如果你实际执行了这份代码,会发现没有任何的输出。这是因为go开启协程之后,main函数还会继续执行,而我在开启线程之后没有写别的代码,故程序直接就结束了运行。
那么,要怎么让程序能够输出东西呢?

package main  
  
import "time"  
  
func main() {  
    go func() {  
        for i := 0; i < 10; i++ {  
            println("hello")  
        }  
    }()  
    time.Sleep(10 * time.Millisecond)  
}

我们可以让程序Sleep那么10ms,给协程时间完成这个过程。使用Sleep的方式来保证协程的完整执行,是可行的,但显然是不可靠的,也许我们的协程中并不是打印十次hello而是别的需要较长时间运行的功能呢?Sleep两个小时?但要是这个协程根据输入的不同可能是1微秒也可能是24个小时呢?这时候,就可以用上接下来介绍的功能了。

Channel

channel 是一个非常非常好用的东西,它可以作为一个灵活的工具来进行线程的操纵,而绝不是仅仅是作为一个线程间通信的载体那么简单。
channel有多种类型,但是常用的似乎就是第一种,既可以用来发送,也可以用来接收指定类型数据的channel

chan T          // 可以接收和发送类型为 T 的数据
chan<- float64  // 只可以用来发送 float64 类型的数据
<-chan int      // 只可以用来接收 int 类型的数据

可以使用chan := make(chan int, 100) 语句来声明一个被命名为chan,可以接收和发送int类型,大小为100的channel。
我们可以使用<-来操作channel\

package main  
  
import (  
"fmt"  
)  
  
func main() {  
    ch := make(chan int, 1)  
    ch <- 1  
    ret := <-ch  
    fmt.Println(ret)  
}

上面的代码创建了一个大小为1的channel,首先往channel里放了一个整形数据1,然后又从中取出并赋值给了ret。
使用channel可以让我们刚刚写的协程程序运行起来更加优雅,不用再使用不太靠谱的sleep函数.

package main  
  
func main() {  
    ch := make(chan int, 1)  
    go func() {  
        for i := 0; i < 10; i++ {  
        println("hello")  
        }  
        ch <- 1  
    }()  
    <-ch  
}

新的代码结构非常简单,就是在之前的基础上写了一个channel来传输整形数据。启动go routine后,<- ch语句会让main函数保持阻塞状态,等待channel的结果。这样我们就可以有充足的时间用来执行打印十次"hello"的操作。而随着操作的完成,我们向管道中传入数据,就能结束阻塞状态,宣告程序的结束。

我们也可以用管道来维持多个协程的运行

package main  
  
func main() {  
ch := make(chan int, 1)  
    for i := 0; i < 3; i++ {  
        go func() {  
            println("hello")  
            ch <- 1  
        }()  
    }  
    for i := 0; i < 3; i++ {  
        <-ch  
    }  
}

在这段代码中开启三个协程,他们各自打印一个hello,总共能够打印三个hello

Select

go语言提供了Select关键字用于读取channel中的信息并运行指定的代码块。使用起来会有类似很多编程语言中的switch语句的感觉,但是select与switch语句存在一个很明显的差异,那就是select会从各个情况中随机选取一个进行channel信息的读取和执行。而不是像switch语句一样从上往下遍历,看到哪一个满足条件就执行哪个。(在学校里写c/c++应该都会有忘记写break的经历吧)当然,select语句的随机选取的范围局限于有内容物的channel,没有东西的当然不会随机到

package main  
  
import "fmt"  
  
func main() {  
    ch1 := make(chan int, 3)  
    ch2 := make(chan int, 3)  
    ch3 := make(chan int, 3)  
    for i := 0; i < 3; i++ {  
        ch1 <- 1  
        ch2 <- 2  
        ch3 <- 3  
    }  
    for {  
        select {  
            case <-ch1:  
                fmt.Println("ch1")  
            case <-ch2:  
                fmt.Println("ch2")  
            case <-ch3:  
                fmt.Println("ch3")  
            default:  
        }  
    }  
}

该代码的运行结果,可以很清楚地看到,select的执行没有是随机的,没有确定的顺序。 image.png 值得注意的是,我在select语句的底部添加了一个空default,我写它并不是为了让他和switch看起来更像。实际上,这是为了防止这个程序报错。当select语句中的所有管道都被搬空了的时候,他就会执行default语句,即使default语句中没有任何代码,都可以让它反复地执行下去,直到再次在管道中得到信息。
一直让他这样执行的意义是什么呢?也许我们实际写的程序也会用到select来接收多个管道信息,但是如果程序暂时没有接到请求,管道都空了,程序就会报错崩溃。这绝对不是我们想要看到的,所以我们需要提供一个default字段,即使是空的也行,让程序能够等待请求的到来,在没人使用自己的时候也能耐得住寂寞😋