Go基础语法(九)

作者: kakarotto | 来源:发表于2018-11-27 12:28 被阅读0次

    Go语言并发

    Go 是并发式语言,而不是并行式语言。

    并发是指立即处理多个任务的能力。

    Go 编程语言原生支持并发。
    Go 使用 Go 协程(Goroutine) 和信道(Channel)来处理并发。

    Go 协程

    Go 协程是与其他函数或方法一起并发运行的函数或方法。
    Go 协程可以看作是轻量级线程。
    与线程相比,创建一个 Go 协程的成本很小。
    因此在 Go 应用中,常常会看到有数以千计的 Go 协程并发地运行。

    优势

    Go 协程相比于线程的优势:

    • Go 协程的成本极低,堆栈大小只有若干 kb,且可以根据应用的需求进行增减。而线程必须指定堆栈的大小,其堆栈是固定不变的。
    • Go 协程会复用数量更少的 OS 线程。即使程序有数以千计的 Go 协程,也可能只有一个线程。如果该线程中的某一 Go 协程发生了阻塞(比如说等待用户输入),那么系统会再创建一个 OS 线程,并把其余 Go 协程都移动到这个新的 OS 线程。所有这一切都在运行时进行,程序员没有直接面临这些复杂的细节,而是有一个简洁的 API 来处理并发。
    • Go 协程使用信道(Channel)来进行通信。信道用于防止多个协程访问共享内存时发生竞态条件(Race Condition)。信道可以看作是 Go 协程之间通信的管道。

    启动一个 Go 协程

    调用函数或者方法时,在前面加上关键字 go,可以让一个新的 Go 协程并发地运行

    package main
    
    import (
        "fmt"
    )
    
    func hello() {
        fmt.Println("Hello world goroutine")
    }
    func main() {
        go hello()
        fmt.Println("main function")
    }
    

    解释代码:go hello() 启动了一个新的 Go 协程。现在 hello() 函数与 main() 函数会并发地执行。
    主函数会运行在一个特有的 Go 协程上,它称为 Go 主协程(Main Goroutine)

    执行上边代码,你会发现程序只是打印出了main function 而未打印hello函数中的内容。这是因为:

    • 启动一个新的协程时,协程的调用会立即返回。与函数不同,程序控制不会去等待 Go 协程执行完毕。在调用 Go 协程之后,程序控制会立即返回到代码的下一行,忽略该协程的任何返回值。

    • 如果希望运行其他 Go 协程,Go 主协程必须继续运行着。如果 Go 主协程终止,则程序终止,于是其他 Go 协程也不会继续运行。

    增加一行代码延迟结束主协程:

    time.Sleep(1 * time.Second)
    

    这只是用于测试可以这样写,事实上后边我们会使用信道解决这个问题。

    为了更好地理解 Go 协程,我们再编写一个程序,启动多个 Go 协程。

    package main
    
    import (  
        "fmt"
        "time"
    )
    
    func numbers() {  
        for i := 1; i <= 5; i++ {
            time.Sleep(250 * time.Millisecond)
            fmt.Printf("%d ", i)
        }
    }
    func alphabets() {  
        for i := 'a'; i <= 'e'; i++ {
            time.Sleep(400 * time.Millisecond)
            fmt.Printf("%c ", i)
        }
    }
    func main() {  
        go numbers()
        go alphabets()
        time.Sleep(3000 * time.Millisecond)
        fmt.Println("main terminated")
    }
    

    解释代码:

    1. 启动了两个 Go 协程。现在,这两个协程并发地运行。
    2. numbers 协程首先休眠 250 微秒,接着打印 1,然后再次休眠,打印 2,依此类推,一直到打印 5 结束。
    3. alphabete 协程同样打印从 a 到 e 的字母,并且每次有 400 微秒的休眠时间。
    4. Go 主协程启动了 numbers 和 alphabete 两个 Go 协程,休眠了 3000 微秒后终止程序。

    来张图更加清晰的看协程之间相互关系:


    image.png

    蓝色的图表示 numbers 协程,
    褐红色的图表示 alphabets 协程,
    绿色的图表示 Go 主协程,
    黑色的图把以上三种协程合并了,表明程序是如何运行的。

    信道

    信道:信道可以想像成 Go 协程之间通信的管道。如同管道中的水会从一端流到另一端,通过使用信道,数据也可以从一端发送,在另一端接收。

    信道声明:所有信道都关联了一个类型。信道只能运输这种类型的数据,而运输其他类型的数据都是非法的。

    chan T 表示 T类型的信道。
    信道的零值为 nil

    信道的零值没有什么用,应该像对 map 和切片所做的那样,用 make 来定义信道。

    package main
    
    import "fmt"
    
    func main() {  
        var a chan int
        if a == nil {
            fmt.Println("channel a is nil, going to define it")
            a = make(chan int)
            fmt.Printf("Type of a is %T", a)
        }
    }
    

    简短声明通常也是一种定义信道的简洁有效的方法:

    a := make(chan int)
    
    通过信道进行发送和接收
    data := <- a // 读取信道 a  
    a <- data // 写入信道 a
    

    在第一行,箭头对于 a 来说是向外指的,因此我们读取了信道 a 的值,并把该值存储到变量 data。

    在第二行,箭头指向了 a,因此我们在把数据写入信道 a。

    发送与接收默认是阻塞的

    • 当把数据发送到信道时,程序控制会在发送数据的语句处发生阻塞,直到有其它 Go 协程从信道读取到数据,才会解除阻塞。
    • 当读取信道的数据时,如果没有其它的协程把数据写入到这个信道,那么读取过程就会一直阻塞着。

    信道的这种特性能够帮助 Go 协程之间进行高效的通信,不需要用到其他编程语言常见的显式锁或条件变量。

    代码示例:

    package main
    
    import (  
        "fmt"
    )
    
    func hello(done chan bool) {  
        fmt.Println("Hello world goroutine")
        done <- true
    }
    func main() {  
        done := make(chan bool)
        go hello(done)
        <-done
        fmt.Println("main function")
    }
    

    解释代码:

    1. 创建了一个 bool 类型的信道 done,并把 done 作为参数传递给了 hello 协程
    2. <-done 这行代码通过信道 done 接收数据,但并没有使用数据或者把数据存储到变量中。这完全是合法的。我们通过信道 done 接收数据。这一行代码发生了阻塞,除非有协程向 done 写入数据,否则程序不会跳到下一行代码。
    3. 现在我们的 Go 主协程发生了阻塞,等待信道 done 发送的数据。

    写个demo示例,需求:定义一个整数,该程序会计算一个数中每一位的平方和与立方和,然后把平方和与立方和相加并打印出来。

    构建程序:

    • 一个单独的 Go 协程计算平方和
    • 一个协程计算立方和,
    • 在 Go 主协程把平方和与立方和相加。
    package main
    
    import (  
        "fmt"
    )
    
    func calcSquares(number int, squareop chan int) {  
        sum := 0
        for number != 0 {
            digit := number % 10
            sum += digit * digit
            number /= 10
        }
        squareop <- sum
    }
    
    func calcCubes(number int, cubeop chan int) {  
        sum := 0 
        for number != 0 {
            digit := number % 10
            sum += digit * digit * digit
            number /= 10
        }
        cubeop <- sum
    } 
    
    func main() {  
        number := 589
        sqrch := make(chan int)
        cubech := make(chan int)
        go calcSquares(number, sqrch)
        go calcCubes(number, cubech)
        squares, cubes := <-sqrch, <-cubech
        fmt.Println("Final output", squares + cubes)
    }
    

    死锁

    当 Go 协程给一个信道发送数据时,照理说会有其他 Go 协程来接收数据。如果没有的话,程序就会在运行时触发 panic,形成死锁。同样的反之亦然。

    package main
    
    func main() {  
        ch := make(chan int)
        ch <- 5
    }
    

    这段代码就会触发 panic :

    fatal error: all goroutines are asleep - deadlock!
    
    goroutine 1 [chan send]:  
    main.main()  
        /tmp/sandbox249677995/main.go:6 +0x80
    

    单向信道

    之前介绍的信道都是双向信道,即通过信道既能发送数据,又能接收数据。

    其实也可以创建单向信道,这种信道只能发送或者接收数据。
    代码:

    package main
    
    import "fmt"
    
    func sendData(sendch chan<- int) {  
        sendch <- 10
    }
    
    func main() {  
        sendch := make(chan<- int)
        go sendData(sendch)
        fmt.Println(<-sendch)  
    }
    

    创建了唯送(Send Only)信道 sendch。chan<- int 定义了唯送信道,因为箭头指向了 chan。 fmt.Println(<-sendch) 编译器会报错。

    信道转换(Channel Conversion)

    把一个双向信道转换成唯送信道或者唯收(Receive Only)信道都是行得通的,但是反过来就不行。

    package main
    
    import "fmt"
    
    func sendData(sendch chan<- int) {  
        sendch <- 10
    }
    
    func main() {  
        cha1 := make(chan int)
        go sendData(cha1)
        fmt.Println(<-cha1)
    }
    

    解释代码:
    函数 sendData 里的参数 sendch chan<- int把 cha1 转换为一个唯送信道。于是该信道在 sendData 协程里是一个唯送信道,而在 Go 主协程里是一个双向信道。该程序最终打印输出 10。

    关闭信道和使用 for range 遍历信道

    数据发送方可以关闭信道,通知接收方这个信道不再有数据发送过来。

    当从信道接收数据时,接收方可以多用一个变量来检查信道是否已经关闭。

    v, ok := <- ch
    
    package main
    
    import (  
        "fmt"
    )
    
    func producer(chnl chan int) {  
        for i := 0; i < 10; i++ {
            chnl <- i
        }
        close(chnl)
    }
    func main() {  
        ch := make(chan int)
        go producer(ch)
        for {
            v, ok := <-ch
            if ok == false {
                break
            }
            fmt.Println("Received ", v, ok)
        }
    }
    

    producer 协程会从 0 到 9 写入信道 chn1,然后关闭该信道。主函数有一个无限的 for 循环(第 16 行),使用变量 ok(第 18 行)检查信道是否已经关闭。如果 ok 等于 false,说明信道已经关闭,于是退出 for 循环。如果 ok 等于 true,会打印出接收到的值和 ok 的值。

    for range 循环用于在一个信道关闭之前,从信道接收数据。
    package main
    
    import (  
        "fmt"
    )
    
    func producer(chnl chan int) {  
        for i := 0; i < 10; i++ {
            chnl <- i
        }
        close(chnl)
    }
    func main() {  
        ch := make(chan int)
        go producer(ch)
        for v := range ch {
            fmt.Println("Received ",v)
        }
    }
    
    package main
    
    import (  
        "fmt"
    )
    
    func digits(number int, dchnl chan int) {  
        for number != 0 {
            digit := number % 10
            dchnl <- digit
            number /= 10
        }
        close(dchnl)
    }
    func calcSquares(number int, squareop chan int) {  
        sum := 0
        dch := make(chan int)
        go digits(number, dch)
        for digit := range dch {
            sum += digit * digit
        }
        squareop <- sum
    }
    
    func calcCubes(number int, cubeop chan int) {  
        sum := 0
        dch := make(chan int)
        go digits(number, dch)
        for digit := range dch {
            sum += digit * digit * digit
        }
        cubeop <- sum
    }
    
    func main() {  
        number := 589
        sqrch := make(chan int)
        cubech := make(chan int)
        go calcSquares(number, sqrch)
        go calcCubes(number, cubech)
        squares, cubes := <-sqrch, <-cubech
        fmt.Println("Final output", squares+cubes)
    }
    

    如果本文对您有帮助,记得点个小赞~~~
    关注作者持续更新~~~

    相关文章

      网友评论

        本文标题:Go基础语法(九)

        本文链接:https://www.haomeiwen.com/subject/bifeqqtx.html