Skip to content

Latest commit

 

History

History
860 lines (669 loc) · 31.1 KB

File metadata and controls

860 lines (669 loc) · 31.1 KB

三、通道和信息

第 2 章理解 Goroutines中,我们了解了 Goroutines 是如何工作的,如何以并行方式使用它们,以及可能出现的一些常见错误。它们的使用和推理都很简单,但它们受到限制,因为它们能够生成其他 goroutine 并等待系统调用。事实上,goroutines 比前一章中展示的功能更强,为了充分挖掘其潜力,我们需要了解如何使用频道,这也是本章的目的。在这里,我们将关注以下主题:

  • 控制并行性
  • 信道和数据通信
  • 通道类型
  • 关闭和多路复用通道

控制并行性

我们知道,生成的 goroutine 将以同步方式尽快开始执行。然而,当所述 goroutine 需要在一个公共源上工作时,存在一个固有的风险,该源对它可以处理的同时任务的数量有一个较低的限制。这可能会导致公共源速度显著减慢,或者在某些情况下甚至失败。正如你可能猜到的,这在计算机科学领域不是一个新问题,有很多方法可以解决它。正如我们将在本章中看到的,Go 提供了以简单直观的方式控制并行性的机制。让我们先看一个例子来模拟负载公共源的问题,然后继续解决它。

想象一下,一个出纳必须处理订单,但每天只能处理 10 张订单。让我们看看如何将其表示为一个程序:

// cashier.go 
package main 

import ( 
    "fmt" 
    "sync" 
) 

func main() { 
    var wg sync.WaitGroup 
    // ordersProcessed & cashier are declared in main function 
    // so that cashier has access to shared state variable 'ordersProcessed'. 
    // If we were to declare the variable inside the 'cashier' function, 
    // then it's value would be set to zero with every function call. 
    ordersProcessed := 0 
    cashier := func(orderNum int) { 
        if ordersProcessed < 10 { 
            // Cashier is ready to serve! 
            fmt.Println("Processing order", orderNum) 
            ordersProcessed++ 
        } else { 
            // Cashier has reached the max capacity of processing orders. 
            fmt.Println("I am tired! I want to take rest!", orderNum) 
        } 
        wg.Done() 
    } 

    for i := 0; i < 30; i++ { 
        // Note that instead of wg.Add(60), we are instead adding 1 
        // per each loop iteration. Both are valid ways to add to WaitGroup as long as we can ensure the right number of calls. 
        wg.Add(1) 
        go func(orderNum int) { 
            // Making an order 
            cashier(orderNum) 
        }(i) 

    } 
    wg.Wait() 
} 

程序的可能输出可能如下所示:

Processing order 29
Processing order 22
Processing order 23
Processing order 13
Processing order 24
Processing order 25
Processing order 21
Processing order 26
Processing order 0
Processing order 27
Processing order 14
I am tired! I want to take rest! 28
I am tired! I want to take rest! 1
I am tired! I want to take rest! 7
I am tired! I want to take rest! 8
I am tired! I want to take rest! 2
I am tired! I want to take rest! 15
...

前面的输出显示了一名出纳在接受 10 张订单后不知所措。然而,值得注意的一点是,如果多次运行前面的代码,可能会得到不同的输出。例如,所有 30 个订单都可能在一次运行中处理!

这是因为所谓的竞赛条件而发生的。当多个参与者(在我们的例子中是 goroutine)试图访问和修改公共共享状态时,就会发生数据争用(或争用条件),这会导致 goroutine 的错误读写。

我们可以尝试用两种方法来解决这个问题:

  • 增加处理订单的限制
  • 增加出纳人数

提高限额只有在一定程度上才可行,超过这一限度将开始降低系统质量,或者对于出纳来说,工作既没有效率,也没有 100%的准确性。相反,通过增加出纳人数,我们可以在不改变限额的情况下连续处理更多订单。这有两种方法:

  • 无通道分布式工作
  • 使用通道的分布式工作

无通道分布式工作

为了在收银员之间平均分配工作,我们需要事先知道我们将收到的订单数量,并确保每个收银员收到的工作在其限额内。这不是最实际的解决方案,因为在现实世界中,我们需要跟踪每个出纳处理了多少订单,并将剩余订单转移给其他出纳的情况下,这将失败。然而,在我们寻找正确的解决方法之前,让我们花时间更好地理解不受控制的并行性问题,并尝试解决它。以下代码试图以一种天真的方式解决它,这应该为我们提供一个良好的开端:

// wochan.go 

package main 

import ( 
   "fmt" 
   "sync" 
) 

func createCashier(cashierID int, wg *sync.WaitGroup) func(int) { 
   ordersProcessed := 0 
   return func(orderNum int) { 
         if ordersProcessed < 10 { 
               // Cashier is ready to serve! 
               //fmt.Println("Cashier ", cashierID, "Processing order", orderNum, "Orders Processed", ordersProcessed) 
               fmt.Println(cashierID, "->", ordersProcessed) 
               ordersProcessed++ 
         } else { 
               // Cashier has reached the max capacity of processing orders. 
               fmt.Println("Cashier ", cashierID, "I am tired! I want to take rest!", orderNum) 
         } 
         wg.Done() 
   } 
} 

func main() { 
   cashierIndex := 0 
   var wg sync.WaitGroup 

   // cashier{1,2,3} 
   cashiers := []func(int){} 
   for i := 1; i <= 3; i++ { 
         cashiers = append(cashiers, createCashier(i, &wg)) 
   } 

   for i := 0; i < 30; i++ { 
         wg.Add(1) 

         cashierIndex = cashierIndex % 3 

         func(cashier func(int), i int) { 
               // Making an order 
               go cashier(i) 
         }(cashiers[cashierIndex], i) 

         cashierIndex++ 
   } 
   wg.Wait() 
} 

以下是一种可能的输出:

Cashier 2 Processing order 7
Cashier 1 Processing order 6
Cashier 3 Processing order 8
Cashier 3 Processing order 29
Cashier 1 Processing order 9
Cashier 3 Processing order 2
Cashier 2 Processing order 10
Cashier 1 Processing order 3
...

我们将可用的 30 份订单在出纳员123之间进行了拆分,所有订单都成功处理,没有人抱怨疲劳。但是,请注意,实现这项功能的代码需要我们做大量的工作。我们必须创建一个函数生成器来创建出纳,通过cashierIndex跟踪使用哪个出纳,等等。最糟糕的是前面的代码不正确!从逻辑上讲,它似乎在做我们想做的事情;但是,请注意,我们正在生成多个 goroutine,这些 goroutine 正在处理共享状态为ordersProcessed的变量!这是我们前面讨论过的竞争条件。好消息是我们可以通过两种方式在wochan.go中检测到它:

  • createCashier功能中,将fmt.Println("Cashier ", cashierID, "Processing order", orderNum)替换为fmt.Println(cashierID, "->", ordersProcessed)。以下是一个可能的输出:
     3 -> 0
     3 -> 1
     1 -> 0
     ...
     2 -> 3
     3 -> 1 # Cashier 3 sees ordersProcessed as 1 but three lines above, Cashier 3 
 was at ordersProcessed == 4!
     3 -> 5
     1 -> 4
     1 -> 4 # Cashier 1 sees ordersProcessed == 4 twice.
     2 -> 4
     2 -> 4 # Cashier 2 sees ordersProcessed == 4 twice.
     ...
  • 前一点证明代码不正确;然而,我们必须猜测代码中可能存在的问题,然后进行验证。Go 为我们提供了检测数据竞争的工具,因此我们不必担心此类问题。要检测数据竞争,我们所要做的就是测试、运行、构建或安装带有-race标志的包(在运行的情况下是文件)。让我们在我们的程序上运行这个,并查看输出:
      $ go run -race wochan.go 
      Cashier 1 Processing order 0
      Cashier 2 Processing order 1
      ==================
      WARNING: DATA RACE
      Cashier 3 Processing order 2
      Read at 0x00c4200721a0 by goroutine 10:
      main.createCashier.func1()
     wochan.go:11 +0x73

      Previous write at 0x00c4200721a0 by goroutine 7:
      main.createCashier.func1()
     wochan.go:14 +0x2a7

      Goroutine 10 (running) created at:
      main.main.func1()
     wochan.go:40 +0x4a
      main.main()
     wochan.go:41 +0x26e

      Goroutine 7 (finished) created at:
      main.main.func1()
     wochan.go:40 +0x4a
      main.main()
     wochan.go:41 +0x26e
      ==================
      Cashier 2 Processing order 4
      Cashier 3 Processing order 5
      ==================
      WARNING: DATA RACE
      Read at 0x00c420072168 by goroutine 9:
      main.createCashier.func1()
     wochan.go:11 +0x73

      Previous write at 0x00c420072168 by goroutine 6:
      main.createCashier.func1()
     wochan.go:14 +0x2a7

      Goroutine 9 (running) created at:
      main.main.func1()
     wochan.go:40 +0x4a
      main.main()
     wochan.go:41 +0x26e

      Goroutine 6 (finished) created at:
      main.main.func1()
     wochan.go:40 +0x4a
      main.main()
     wochan.go:41 +0x26e
      ==================
      Cashier 1 Processing order 3
      Cashier 1 Processing order 6
      Cashier 2 Processing order 7
      Cashier 3 Processing order 8
      ...
      Found 2 data race(s)
      exit status 66

可以看出,-race标志帮助我们检测数据竞争。

这是否意味着我们无法在共享状态下分配任务?我们当然可以!但我们需要使用 Go 为此提供的机制:

  • 互斥锁、信号量和锁
  • 通道

互斥锁是一种互斥锁,它为我们提供了一种同步机制,只允许一个 goroutine 在任何给定的时间点访问特定的代码或共享状态。如前所述,对于同步问题,我们可以使用互斥或通道,Go 建议为正确的作业使用正确的构造。然而,在实践中,使用通道为我们提供了更高级别的抽象和更大的通用性,尽管互斥也有它的用途。正是出于这个原因,在本章和本书中,我们将利用各种通道。

使用通道的分布式工作

我们现在确定了三件事:我们希望在收银员之间正确分配订单,我们希望确保每个收银员处理的订单数量正确,我们希望使用通道来解决这个问题。在讨论如何使用通道解决出纳问题之前,让我们先看看通道的基本语法和用法。

什么是频道?

通道是一种通信机制,允许我们在 goroutine 之间传递数据。它是 Go 中的内置数据类型。可以使用一种基本数据类型传递数据,也可以使用结构创建自己的复杂数据类型。

下面是一个简单的示例,演示如何使用频道:

// simchan.go 
package main 

import "fmt" 

// helloChan waits on a channel until it gets some data and then prints the value. 
func helloChan(ch <- chan string) { 
    val := <- ch 
    fmt.Println("Hello, ", val) 
} 

func main() { 
    // Creating a channel 
    ch := make(chan string) 

    // A Goroutine that receives data from a channel 
    go helloChan(ch) 

    // Sending data to a channel. 
    ch <- "Bob" 
} 

如果我们运行前面的代码,它将打印以下输出:

Hello, Bob

使用通道的基本模式可以通过以下步骤来解释:

  1. 创建通道以接受要处理的数据。
  2. 启动正在通道上等待数据的 goroutine。
  3. 然后,我们可以使用main函数或其他 goroutine 将数据传递到通道中。
  4. 在通道上侦听的 goroutine 可以接受数据并对其进行处理。

使用通道的优点是多个 goroutine 可以在同一通道上等待并并发执行任务。

用 goroutines 解决出纳问题

在我们试图解决问题之前,让我们先制定我们想要实现的目标:

  1. 创建一个接受所有订单的通道orderChannel
  2. 启动所需数量的出纳 goroutines,接受来自orderChannel的有限数量的订单。
  3. 开始将所有订单放入orderChannel

让我们看一个可能的解决方案,它尝试使用前面的步骤解决出纳问题:

// wichan.go 
package main 

import ( 
    "fmt" 
    "sync" 
) 

func cashier(cashierID int, orderChannel <-chan int, wg *sync.WaitGroup) { 
    // Process orders upto limit. 
    for ordersProcessed := 0; ordersProcessed < 10; ordersProcessed++ { 
        // Retrieve order from orderChannel 
        orderNum := <-orderChannel 

        // Cashier is ready to serve! 
        fmt.Println("Cashier ", cashierID, "Processing order", orderNum, "Orders Processed", ordersProcessed) 
        wg.Done() 
    } 
} 

func main() { 
    var wg sync.WaitGroup 
    wg.Add(30) 
    ordersChannel := make(chan int) 

    for i := 0; i < 3; i++ { 
        // Start the three cashiers 
        func(i int) { 
            go cashier(i, ordersChannel, &wg) 
        }(i) 
    } 

    // Start adding orders to be processed. 
    for i := 0; i < 30; i++ { 
        ordersChannel <- i 
    } 
    wg.Wait() 
} 

在使用-race标志运行前面的代码时,我们可以看到代码运行时没有任何数据竞争:

$ go run -race wichan.go 
Cashier 2 Processing order 2 Orders Processed 0
Cashier 2 Processing order 3 Orders Processed 1
Cashier 0 Processing order 0 Orders Processed 0
Cashier 1 Processing order 1 Orders Processed 0
...
Cashier 0 Processing order 27 Orders Processed 9

代码非常简单,易于并行化,并且工作良好,不会引起任何数据争用。

信道和数据通信

Go 是一种静态类型语言,这意味着给定通道只能发送或接收单一数据类型的数据。在 Go 的术语中,这被称为通道的元素类型。Go 通道将接受任何有效的 Go 数据类型,包括函数。下面是一个接受和调用函数的简单程序示例:

// elems.go 
package main 

import "fmt" 

func main() { 
    // Let's create three simple functions that take an int argument 
    fcn1 := func(i int) { 
        fmt.Println("fcn1", i) 
    } 
    fcn2 := func(i int) { 
        fmt.Println("fcn2", i*2) 
    } 
    fcn3 := func(i int) { 
        fmt.Println("fcn3", i*3) 
    } 

    ch := make(chan func(int)) // Channel that sends & receives functions that take an int argument 
    done := make(chan bool)    // A Channel whose element type is a boolean value. 

    // Launch a goroutine to work with the channels ch & done. 
    go func() { 
        // We accept all incoming functions on Channel ch and call the functions with value 10\. 
        for fcn := range ch { 
            fcn(10) 
        } 
        // Once the loop terminates, we print Exiting and send true to done Channel. 
        fmt.Println("Exiting") 
        done <- true 
    }() 

    // Sending functions to channel ch 
    ch <- fcn1 
    ch <- fcn2 
    ch <- fcn3 

    // Close the channel once we are done sending it data. 
    close(ch) 

    // Wait on the launched goroutine to end. 
    <-done 
} 

上述代码的输出如下所示:

fcn1 10
fcn2 20
fcn3 30
Exiting

在前面的代码示例中,我们说信道ch的元素类型为func(int),信道done的元素类型为bool。代码中有许多更有趣的细节,但我们将在下面的章节中讨论它们。

信息和事件

到目前为止,我们一直使用术语数据来表示从通道发送和接收的值。虽然到目前为止这可能很容易理解,但 Go 使用两个特定术语来描述通过通道进行通信的数据类型。它们被称为消息事件。就代码而言,它们是相同的,但这些术语用于帮助我们理解正在发送的数据的类型。简言之:

  • 消息通常是我们希望 goroutine 处理并在需要时对其执行操作的值。
  • 事件用于表示某个事件已经发生。收到的实际价值可能不如收到价值的行为重要。请注意,尽管我们使用了术语事件,但它们仍然是消息的一种类型。

在前面的代码示例中,发送到ch的值是消息,而发送到done的值是事件。需要注意的一点是,事件通道的元素类型往往是struct{}{}boolint

现在我们了解了什么是通道元素类型、消息和事件,让我们看看不同类型的通道。

通道类型

Go 为我们提供了三种主要的频道类型。它们大致可分为:

  • 无缓冲
  • 缓冲
  • 单向(仅发送和仅接收类型通道)

无缓冲通道

这是 Go 中可用的基本通道类型。我们将数据发送到通道中,然后在另一端接收数据,这非常简单。有趣的是,任何在无缓冲信道上运行的 goroutine 都将被阻塞,直到发送方和接收方 goroutine 都可用为止。例如,考虑下面的代码片段:

ch := make(chan int) 
go func() {ch <- 100}     // Send 100 into channel.                
                             Channel: send100          
go func() {val := <- ch}  // Goroutine waiting on channel.        
                             Channel: recv1         
go func() {val := <- ch}  // Another goroutine waiting on channel.
                             Channel: recv2

我们有一个元素类型为int的通道ch。我们开始三次狂欢;一个向通道(send100发送100消息,另外两个 goroutine(recv1recv2在通道上等待。在recv1recv2中的任何一个开始监听通道以接收消息之前,send100被阻止。如果我们假设recv2接收到send100在通道上发送的消息,那么recv1将等待,直到另一条消息在通道上发送。如果前面四行是通道上的唯一通信,则recv1将等待程序结束,然后会被 Go 运行时突然终止。

缓冲通道

考虑这样一种情况,即我们能够将更多的消息发送到信道,而不是接收到消息的 GORUTIN 可以处理它们。如果我们使用无缓冲通道,它将显著降低程序的速度,因为我们必须等待每条消息被处理,然后才能放入另一条消息。如果通道可以存储这些额外的消息或“缓冲”消息,这将是理想的。这正是缓冲通道所做的。它维护一个消息队列,goroutine 将以自己的速度使用这些消息。然而,即使缓冲信道也具有有限的容量;我们需要在创建通道时定义队列的容量。

那么,我们如何使用缓冲通道呢?在语法方面,它与使用无缓冲通道相同。缓冲通道的行为可以解释如下:

  • 如果缓冲通道为空:在通道上接收消息被阻止,直到消息通过通道发送
  • 如果缓冲通道已满:在通道上发送消息将被阻止,直到至少从通道接收到一条消息,从而为新消息留出空间放置在通道的缓冲区或队列上
  • 如果缓冲通道被部分填满,即既不满也不空:在一个通道上发送或接收消息都是未阻塞的,通信是即时的

缓冲信道上的通信

单向缓冲器

可以从通道发送和接收消息。然而,当 goroutine 使用通道进行通信时,它们通常只用于一个目的:从通道发送或接收。Go 允许我们指定 goroutine 使用的通道是用于发送还是接收消息。它通过单向通道实现这一点。一旦通道被识别为单向通道,我们就不能对其执行其他操作。这意味着单向发送通道不能用于接收消息,单向接收通道不能用于发送消息。任何这样做的尝试都会被 Go 编译器捕获为编译时错误。

以下是正确使用单向通道的示例:

// unichans.go 
package main 

import ( 
    "fmt" 
    "sync" 
) 

func recv(ch <-chan int, wg *sync.WaitGroup) { 
    fmt.Println("Receiving", <-ch) 
    wg.Done() 
} 

func send(ch chan<- int, wg *sync.WaitGroup) { 
    fmt.Println("Sending...") 
    ch <- 100 
    fmt.Println("Sent") 
    wg.Done() 
} 

func main() { 
    var wg sync.WaitGroup 
    wg.Add(2) 

    ch := make(chan int) 
    go recv(ch, &wg) 
    go send(ch, &wg) 

    wg.Wait() 
} 

预期产出如下:

Sending...
Receiving 100 # (or) Sent
Sent # (or) Receiving 100  

现在,让我们尝试通过接收通道发送,看看会发生什么。我们将仅在前面的示例中看到已更改的函数:

// unichans2.go 
// ... 
// Changed function 
func recv(ch <-chan int, wg *sync.WaitGroup) { 
    fmt.Println("Receiving", <-ch) 
    fmt.Println("Trying to send") // signalling that we are going to send over channel. 
    ch <- 13                      // Sending over channel 
    wg.Done() 
} 

现在,如果我们尝试运行或构建更新的程序,将出现以下错误:

$ go run unichans.go 
# command-line-arguments
unichans.go:11: invalid operation: ch <- 13 (send to receive-only type <-chan int)  

那么,如果我们使用缓冲通道,程序将如何运行?由于在未填充的通道上没有阻塞,sendgoroutine 向通道发送消息,然后继续执行。recvgoroutine 在开始执行时从通道读取,然后打印:

// buffchan.go 
package main 

import ( 
    "fmt" 
    "sync" 
) 

func recv(ch <-chan int, wg *sync.WaitGroup) { 
    fmt.Println("Receiving", <-ch) 
    wg.Done() 
} 

func send(ch chan<- int, wg *sync.WaitGroup) { 
    fmt.Println("Sending...") 
    ch <- 100 
    fmt.Println("Sent") 
    wg.Done() 
} 

func main() { 
    var wg sync.WaitGroup 
    wg.Add(2) 

    // Using a buffered channel. 
    ch := make(chan int, 10) 
    go recv(ch, &wg) 
    go send(ch, &wg) 

    wg.Wait() 
} 

产出如下:

Sending...
Sent
Receiving 100

关闭通道

在前面的部分中,我们已经介绍了三种类型的频道以及如何创建它们。在本节中,让我们看看如何关闭通道,以及这可能会如何影响这些通道上的发送和接收。当我们不再希望在所述频道上发送任何消息时,我们关闭该频道。对于每种类型的通道,通道关闭后的行为是不同的。让我们深入了解一下:

  • 无缓冲封闭通道:发送消息将导致恐慌,通过该通道接收消息将立即产生通道元素类型的零值。
  • 缓冲封闭通道:发送消息将导致恐慌,但通过它接收将首先产生通道队列中的所有值。队列耗尽后,通道将开始产生通道元素类型的零值。

以下是对前面两点的说明:

// closed.go 
package main 

import "fmt" 

type msg struct { 
    ID    int 
    value string 
} 

func handleIntChan(intChan <-chan int, done chan<- int) { 
    // Even though there are only 4 elements being sent via channel, we retrieve 6 values. 
    for i := 0; i < 6; i++ { 
        fmt.Println(<-intChan) 
    } 
    done <- 0 
} 

func handleMsgChan(msgChan <-chan msg, done chan<- int) { 
    // We retrieve 6 values of element type struct 'msg'. 
    // Given that there are only 4 values in the buffered channel, 
    // the rest should be zero value of struct 'msg'. 
    for i := 0; i < 6; i++ { 
        fmt.Println(fmt.Sprintf("%#v", <-msgChan)) 
    } 
    done <- 0 
} 

func main() { 
    intChan := make(chan int) 
    done := make(chan int) 

    go func() { 
        intChan <- 9 
        intChan <- 2 
        intChan <- 3 
        intChan <- 7 
        close(intChan) 
    }() 
    go handleIntChan(intChan, done) 

    msgChan := make(chan msg, 5) 
    go func() { 
        for i := 1; i < 5; i++ { 
            msgChan <- msg{ 
                ID:    i, 
                value: fmt.Sprintf("VALUE-%v", i), 
            } 
        } 
        close(msgChan) 
    }() 
    go handleMsgChan(msgChan, done) 

    // We wait on the two channel handler goroutines to complete. 
    <-done 
    <-done 

    // Since intChan is closed, this will cause a panic to occur. 
    intChan <- 100 
} 

以下是程序的一种可能输出:

9
2
3
7
0
0
main.msg{ID:1, value:"VALUE-1"}
main.msg{ID:2, value:"VALUE-2"}
main.msg{ID:3, value:"VALUE-3"}
main.msg{ID:4, value:"VALUE-4"}
main.msg{ID:0, value:""}
main.msg{ID:0, value:""}
panic: send on closed channel

goroutine 1 [running]:
main.main()
     closed.go:58 +0x194

    Process finished with exit code 2

最后,以下是关于关闭通道和关闭通道的一些进一步有用的要点:

  • 无法确定通道是否已关闭。我们所能做的最好的事情就是检查我们是否能够成功地从通道中检索到消息。我们知道在通道上检索的默认语法是msg := <- ch。但是,此检索有一个变体:msg, ok := <-ch。第二个参数告诉我们检索是否成功。如果通道关闭,ok将为false。这可用于告知通道何时关闭。
  • msg, ok := <-ch是迭代通道时的常见模式。因此,Go 允许我们通过一个通道range。当通道关闭时,range循环结束。
  • 关闭闭合通道、零通道或仅接收通道将导致死机。只能关闭双向通道或仅发送通道。
  • 不强制关闭通道,与垃圾收集器GC无关。如果 GC 确定某个通道不可访问,不管它是打开的还是关闭的,该通道都将被垃圾收集。

多路复用信道

多路复用描述了一种方法,即我们使用单个资源对多个信号或动作进行操作。这种方法广泛应用于电信和计算机网络。我们可能会发现自己处于这样一种情况:我们有多种类型的任务要执行。但是,它们只能在相互排斥的情况下执行,或者需要在共享资源上工作。为此,我们在 Go 中使用了一种称为通道多路复用的模式。在我们深入研究如何实际多路复用通道之前,让我们试着自己实现它。

假设我们有一组通道,我们希望在通过通道发送数据时立即对它们采取行动。下面是一个关于我们如何做到这一点的幼稚方法:

// naiveMultiplexing.go 
package main 

import "fmt" 

func main() { 
    channels := [5](chan int){ 
        make(chan int), 
        make(chan int), 
        make(chan int), 
        make(chan int), 
        make(chan int), 
    } 

    go func() { 
        // Starting to wait on channels 
        for _, chX := range channels { 
            fmt.Println("Receiving from", <- chX) 
        } 
    }() 

    for i := 1; i < 6; i++ { 
        fmt.Println("Sending on channel:", i) 
        channels[i] <- 1 
    } 
} 

上述程序的输出如下:

Sending on channel: 1
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:
main.main()
 /home/entux/Documents/Code/GO-WORKSPACE/src/distributed-go/ch3/naiveSwitch.go:23 +0x2b1

goroutine 5 [chan receive]:
main.main.func1(0xc4200160c0, 0xc420016120, 0xc420016180, 0xc4200161e0, 0xc420016240)
 GO-WORKSPACE/src/distributed-go/ch3/naiveSwitch.go:17 +0xba
created by main.main
 GO-WORKSPACE/src/distributed-go/ch3/naiveSwitch.go:19 +0x18b

在 goroutine 中的循环中,第一个通道从不等待,这会导致 goroutine 中的死锁。多路复用有助于我们等待多个通道,而不会阻塞任何通道,同时在通道上可用的消息上执行操作。

以下是在通道上进行多路复用时需要记住的一些要点:

  • 语法
      select { 
      case <- ch1: 
        // Statements to execute if ch1 receives a message 
      case val := <- ch2: 
        // Save message received from ch2 into a variable and
        execute statements for ch2 
      }
  • 在执行select时,可能有多个案例准备好了一条消息。在这种情况下,select不会执行所有案例,而是随机选取一个案例,执行它,然后退出select语句。
  • 但是,如果我们希望在select情况下对发送到所有通道的消息做出反应,那么前面的一点可能会受到限制。然后我们可以将select语句放入for循环中,它将确保处理所有消息。
  • 即使for循环将处理所有通道上发送的消息,循环仍将被阻止,直到有消息可用。在某些情况下,我们可能不希望阻止循环迭代,而是执行一些“默认”操作。这可以通过使用select语句中的default案例来实现。
  • 基于上述两点更新的语法为:
      for { 
        select { 
            case <- ch1: 
            // Statements to execute if ch1 receives a message 
            case val := <- ch2: 
            // Save message received from ch2 into a variable and
            execute statements for ch2 
            default: 
            // Statements to execute if none of the channels has yet
            received a message. 
        } 
      } 
  • 在缓冲通道的情况下,无法保证消息的接收顺序。

以下是在所有所需通道上进行多路复用的正确方法,不会在任何通道上被阻塞,也不会继续处理所有发送的消息:

// multiplexing.go 

package main 

import ( 
    "fmt" 
) 

func main() { 
    ch1 := make(chan int) 
    ch2 := make(chan string) 
    ch3 := make(chan int, 3) 
    done := make(chan bool) 
    completed := make(chan bool) 

    ch3 <- 1 
    ch3 <- 2 
    ch3 <- 3 
    go func() { 
        for { 

            select { 
                case <-ch1: 
                      fmt.Println("Received data from ch1") 
                case val := <-ch2: 
                      fmt.Println(val) 
                case c := <-ch3: 
                      fmt.Println(c) 
                case <-done: 
                      fmt.Println("exiting...") 
                      completed <- true 
                      return 
            } 
        } 
    }() 

    ch1 <- 100 
    ch2 <- "ch2 msg" 
    // Uncomment us to avoid leaking the 'select' goroutine! 
    //close(done) 
    //<-completed 
} 

以下是前面程序的输出:

1
Received data from ch1
2
3

不幸的是,这个程序有一个缺陷:它泄漏了 goroutine 处理,select*。*在main函数末尾附近的注释中也指出了这一点。这通常发生在我们有一个正在运行的 goroutine,但我们无法直接到达它时。即使没有存储 goroutine 的引用,GC 也不会对其进行垃圾收集。因此,我们需要一个机制来停止和返回这样的 goroutines。通常,这可以通过创建一个专门用于从 goroutine 返回的通道来实现。

在前面的代码中,我们通过done通道发送信号。如果我们取消对行的注释,然后运行程序,则输出如下:

1
2
3
Received data from ch1
ch2 msg
exiting...

总结

在本章中,我们研究了控制并行性的原因,并对涉及共享状态时任务的复杂性进行了评估。我们使用了一个过度工作的出纳的例子作为编程问题来解决和试验频道,并进一步探索了不同类型的频道以及使用它们所涉及的细微差别。例如,我们看到,如果我们尝试在关闭的缓冲通道和未缓冲通道上发送消息,它们都会导致恐慌,并且根据通道是否缓冲以及通道是否为空或满,从它们接收消息会导致不同的结果。在select的帮助下,我们还了解了如何在不阻塞任何通道的情况下等待多个通道。

在后面的章节中,从第 5 章介绍 Goophr,到第 8 章部署 Goophr,我们将开发一个分布式 web 应用程序。这要求我们具备如何与 web 服务器交互的基本知识,使用 HTTP 协议,使用 web 浏览器以外的工具。这些知识不仅在与我们的应用程序交互时,而且在与作为开发人员的标准 web 交互时都会派上用场。这将是下一章第 4 章RESTful Web的主题,在这里我们将介绍我们将用于与 Web 应用程序交互的工具和协议。