美文网首页自学Go入门与进阶
Go基础语法(十一)

Go基础语法(十一)

作者: kakarotto | 来源:发表于2018-12-02 22:56 被阅读1次

    select

    select 语句用于在多个发送/接收信道操作中进行选择。
    select 语句会一直阻塞,直到发送/接收操作准备就绪。
    如果有多个信道操作准备完毕,select 会随机地选取其中之一执行。
    该语法与 switch 类似,所不同的是,这里的每个 case 语句都是信道操作。

    package main
    
    import (  
        "fmt"
        "time"
    )
    
    func server1(ch chan string) {  
        time.Sleep(6 * time.Second)
        ch <- "from server1"
    }
    func server2(ch chan string) {  
        time.Sleep(3 * time.Second)
        ch <- "from server2"
    
    }
    func main() {  
        output1 := make(chan string)
        output2 := make(chan string)
        go server1(output1)
        go server2(output2)
        select {
        case s1 := <-output1:
            fmt.Println(s1)
        case s2 := <-output2:
            fmt.Println(s2)
        }
    }
    

    select应用

    默认情况

    在没有 case 准备就绪时,可以执行 select 语句中的默认情况(Default Case)。通常用于防止 select 语句一直阻塞。

    package main
    
    import (  
        "fmt"
        "time"
    )
    
    func process(ch chan string) {  
        time.Sleep(10500 * time.Millisecond)
        ch <- "process successful"
    }
    
    func main() {  
        ch := make(chan string)
        go process(ch)
        for {
            time.Sleep(1000 * time.Millisecond)
            select {
            case v := <-ch:
                fmt.Println("received value: ", v)
                return
            default:
                fmt.Println("no value received")
            }
        }
    
    }
    

    死锁与默认情况

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

    由于没有 Go 协程向该信道写入数据,因此 select 语句会一直阻塞,导致死锁。该程序会触发运行时 panic

    如果存在默认情况,就不会发生死锁,因为在没有其他 case 准备就绪时,会执行默认情况,例:

    package main
    
    import "fmt"
    
    func main() {  
        ch := make(chan string)
        select {
        case <-ch:
        default:
            fmt.Println("default case executed")
        }
    }
    

    如果 select 只含有值为 nil 的信道,也同样会执行默认情况。

    package main
    
    import "fmt"
    
    func main() {  
        var ch chan string
        select {
        case v := <-ch:
            fmt.Println("received value", v)
        default:
            fmt.Println("default case executed")
    
        }
    }
    

    select 随机选取

    当 select 由多个 case 准备就绪时,将会随机地选取其中之一去执行。

    package main
    
    import (  
        "fmt"
        "time"
    )
    
    func server1(ch chan string) {  
        ch <- "from server1"
    }
    func server2(ch chan string) {  
        ch <- "from server2"
    
    }
    func main() {  
        output1 := make(chan string)
        output2 := make(chan string)
        go server1(output1)
        go server2(output2)
        time.Sleep(1 * time.Second)
        select {
        case s1 := <-output1:
            fmt.Println(s1)
        case s2 := <-output2:
            fmt.Println(s2)
        }
    }
    

    空 select

    package main
    
    func main() {  
        select {}
    }
    

    Mutex

    通过 Mutex 和信道来处理竞态条件(Race Condition)。

    临界区:
    当程序并发地运行时,多个 Go 协程不应该同时访问那些修改共享资源的代码。这些修改共享资源的代码称为临界区。

    package main  
    import (  
        "fmt"
        "sync"
        )
    var x  = 0  
    func increment(wg *sync.WaitGroup, m *sync.Mutex) {  
        m.Lock()
        x = x + 1
        m.Unlock()
        wg.Done()   
    }
    func main() {  
        var w sync.WaitGroup
        var m sync.Mutex
        for i := 0; i < 1000; i++ {
            w.Add(1)        
            go increment(&w, &m)
        }
        w.Wait()
        fmt.Println("final value of x", x)
    }
    

    Mutex 是一个结构体类型,我们在第 15 行创建了 Mutex 类型的变量 m,其值为零值。在上述程序里,我们修改了 increment 函数,将增加 x 的代码(x = x + 1)放置在 m.Lock()m.Unlock()之间。现在这段代码不存在竞态条件了,因为任何时刻都只允许一个协程执行这段代码。

    传递 Mutex 的地址很重要。如果传递的是 Mutex 的值,而非地址,那么每个协程都会得到 Mutex 的一份拷贝,竞态条件还是会发生。

    使用信道处理竞态条件

    package main  
    import (  
        "fmt"
        "sync"
        )
    var x  = 0  
    func increment(wg *sync.WaitGroup, ch chan bool) {  
        ch <- true
        x = x + 1
        <- ch
        wg.Done()   
    }
    func main() {  
        var w sync.WaitGroup
        ch := make(chan bool, 1)
        for i := 0; i < 1000; i++ {
            w.Add(1)        
            go increment(&w, ch)
        }
        w.Wait()
        fmt.Println("final value of x", x)
    }
    

    创建了容量为 1 的缓冲信道,将它传入 increment 协程。该缓冲信道用于保证只有一个协程访问增加 x 的临界区。具体的实现方法是在 x 增加之前,传入 true 给缓冲信道。由于缓冲信道的容量为 1,所以任何其他协程试图写入该信道时,都会发生阻塞,直到 x 增加后,信道的值才会被读取。实际上这就保证了只允许一个协程访问临界区。

    Mutex 和 信道 该如何选择

    当 Go 协程需要与其他协程通信时,可以使用信道。而当只允许一个协程访问临界区时,可以使用 Mutex。

    就我们上面解决的问题而言,我更倾向于使用 Mutex,因为该问题并不需要协程间的通信。所以 Mutex 是很自然的选择。

    相关文章

      网友评论

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

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