Skip to content

Latest commit

 

History

History
972 lines (692 loc) · 36.7 KB

File metadata and controls

972 lines (692 loc) · 36.7 KB

五、锁、阻塞和更好的通道

现在,我们已经开始很好地掌握如何以安全和一致的方式使用 goroutines,现在是时候进一步了解是什么导致了代码阻塞和死锁。我们还将探索sync包,并深入分析一些评测和分析。

到目前为止,我们已经建立了一些相对基本的 goroutine 和补充通道,但是我们现在需要在 goroutine 之间利用一些更复杂的通信通道。为此,我们将实现更多自定义数据类型,并将它们直接应用于通道。

我们还没有研究过 Go 的一些用于同步和分析的低级工具,所以我们将探索sync.atomic,一个与sync.Mutex一起允许对状态进行更细粒度控制的包。

最后,我们将深入研究 pprof,这是一个由 Go 提供的极好的工具,它让我们可以分析二进制文件,以获得有关 goroutine、线程、总体堆和阻塞配置文件的详细信息。

有了一些新的工具和方法来测试和分析我们的代码,我们将准备生成一个健壮的、高度可扩展的 web 服务器,该服务器可用于安全、快速地处理向它抛出的任何流量。

了解Go中的拦网方法

到目前为止,通过我们的探索和示例,我们已经遇到了一些阻塞代码,有意的和无意的。在这一点上,谨慎地考虑一下我们可以引入(或无意中成为)阻塞代码的各种方式。

By looking at the various ways Go code can be blocked, we can also be better prepared to debug cases when concurrency is not operating as expected in our application.

阻塞方法 1–监听、等待通道

阻止代码的最同时关注的方式是留下一个串行通道监听一个或多个 goroutine。到目前为止,我们已经看到了几次,但基本概念是显示在下面的代码片段中:

func thinkAboutKeys() {
  for {
    fmt.Println("Still Thinking")
    time.Sleep(1 * time.Second)
  }
}

func main() {
  fmt.Println("Where did I leave my keys?")

  blockChannel := make(chan int)
  go thinkAboutKeys()

  <-blockChannel

  fmt.Println("OK I found them!")
}

尽管我们所有的循环代码都是并发的,但我们仍在等待blockChannel继续线性执行的信号。当然,我们可以通过沿通道发送来看到这一点,从而继续执行代码,如以下代码段所示:

func thinkAboutKeys(bC chan int) {
  i := 0
  max := 10
  for {
    if i >= max {
      bC <- 1
    }
    fmt.Println("Still Thinking")
    time.Sleep(1 * time.Second)
    i++
  }
}

在这里,我们修改了 goroutine 函数,以接受阻塞通道,并在达到最大值时向其发送结束消息。这些机制对于长时间运行的进程来说很重要,因为我们可能需要知道何时以及如何杀死它们。

通过通道发送更多数据类型

Go 使用通道(结构和函数)作为一等公民为我们提供了许多有趣的方式来执行,或者至少尝试通道间通信的新方法。

其中一个示例是创建一个通道,该通道通过函数本身处理转换,而不是通过标准语法直接通信,该通道执行其函数。您甚至可以在单个函数中迭代函数的片/数组上执行此操作。

创建功能通道

到目前为止,我们几乎只在单数据类型和单值通道中工作。那么,让我们尝试通过通道发送函数。对于一流的通道,我们不需要抽象来实现这一点;我们可以直接通过通道发送几乎任何内容,如以下代码段所示:

func abstractListener(fxChan chan func() string ) {

  fxChan <- func() string {

    return "Sent!"
  }
}

func main() {

  fxChan := make (chan func() string)
  defer close(fxChan)
  go abstractListener(fxChan)
  select {
    case rfx := <- fxChan:
    msg := rfx()
    fmt.Println(msg)      
    fmt.Println("Received!")

  }

}

This is like a callback function. However, it also is intrinsically different, as it is not just the method called after the execution of a function, but also serves as the mode of communication between functions.

Keep in mind that there are often alternatives to passing functions across channels, so this will likely be something very specific to a use case rather than a general practice.

由于您的频道类型实际上可以是任何可用的类型,因此此功能打开了一个可能的世界,可能会混淆抽象。作为通道类型的结构或接口是不言自明的,因为可以对其定义的任何属性做出与应用相关的决策。

在下一节中,我们来看一个以这种方式使用接口的示例。

使用接口通道

与我们的功能通道一样,能够跨通道传递接口(这是一种补充数据类型)非常有用。让我们看一个通过接口发送的示例:

type Messenger interface {
  Relay() string
}

type Message struct {
  status string
}

func (m Message) Relay() string {
  return m.status
}

func alertMessages(v chan Messenger, i int) {
  m := new(Message)
  m.status = "Done with " + strconv.FormatInt(int64(i),10)
  v <- m
}

func main () {

  msg := make(chan Messenger)

  for i:= 0; i < 10; i++ {
    go alertMessages(msg,i)
  }

  select {
    case message := <-msg:
      fmt.Println (message.Relay())
  }
  <- msg
}

这是如何将接口用作通道的一个非常基本的示例;在前面的示例中,接口本身在很大程度上是装饰性的。实际上,我们通过接口的通道传递新创建的消息类型,而不是直接与接口交互的。

使用结构、接口和更复杂的通道

为我们的频道创建自定义类型,允许我们在仍在放手的情况下,指令频道内通信的工作方式和幕后调度。

最终,这主要是一个设计考虑。在前面的示例中,我们使用单独的通道来进行特定的通信,而不是使用传递大量数据的“一刀切”通道。但是,您也可能会发现使用单个通道处理 goroutine 和其他通道之间的大量通信是有利的。

在决定是否将信道分离为单个通信比特或通信包时,主要考虑的因素取决于每个比特的总体可变性。

例如,如果您总是希望将计数器与函数或字符串一起发送,并且它们在数据一致性方面总是成对的,那么这种方法可能是有意义的。如果这些组件中的任何一个在传输过程中会失去同步性,那么保持每个组件的独立性就更符合逻辑。

Note

Go中的地图

正如提到的,Go 中的映射类似于其他地方的哈希表,并且与切片或数组直接相关。

在前面的示例中,我们检查用户名/密钥是否已经存在;为此,Go 提供了一种简单的方法。尝试检索不存在密钥的哈希时,返回零值,如以下代码行所示:

if Users[user.name] {
  fmt.Fprintln(conn, "Unfortunately, that username is in use!");
}

这使得针对地图及其键进行测试在语法上变得简单和干净。

Go 中地图的一个最佳特性是能够从任何可比较的类型中生成键,其中包括字符串、整数、布尔值以及专门由这些类型组成的任何地图、结构、切片或通道。

这个一对多频道可以作为主从或广播用户模式使用。我们将有一个侦听消息并将其路由到适当用户的频道,还有一个侦听广播消息并将其排队给所有用户的频道。

为了更好地演示这一点,我们将创建一个简单的多用户聊天系统,它允许与单个用户进行推特式的@user交流,并且能够向所有用户广播标准消息,创建一个可供所有用户阅读的通用广播聊天笔记。两者都是简单的自定义类型结构通道,因此我们可以描绘各种通信片段。

Go 中的结构

作为一种一流的、匿名的、可扩展的类型,struct 是最通用的、最有用的数据构造之一。创建类似于其他数据结构(如数据库和数据存储)的很简单,虽然我们不太愿意称它们为对象,但它们肯定可以被视为对象。

在函数中使用结构的经验法则是,如果结构特别复杂,则按引用传递而不是按值传递。澄清的两点如下:

  • 引用出现在 Quotes 中是因为(Go 的 FAQ 验证了这一点)从技术上讲,Go 中的所有内容都是按值传递的。我们的意思是,尽管对指针的引用仍然存在,但在过程的某个步骤中,会复制该值。
  • "Particularly complex" is, understandably, tough to quantify, so personal judgment might come into play. However, we can consider a simple struct one with no more than five methods or properties.

您可以从帮助台系统的角度来考虑这一点,虽然在今天我们不太可能为这样的事情创建命令行界面,但是避开 web 部分可以让我们忽略所有与 Go 不一定相关的客户端代码。

您当然可以举这样一个例子,并利用一些前端库(如backbone.jssocket.io)将其推广到 Web。

To accomplish this, we'll need to create both a client and a server application, and we'll try to keep each as bare bone as possible. You can clearly and simply augment this to include any functionality you see fit such as making Git comments and updating a website.

我们将从服务器开始,这将是最复杂的部分。客户端应用将大部分通过套接字接收回的消息,因此流程的客户端将看不到很多的读取和路由逻辑。

网络包–一个带有接口通道的聊天服务器

在这里,我们需要引入一个相关的包来处理应用的大部分通信。我们在SVG 输出生成示例中稍微提到了net包,以展示并发性-net/http只是更广泛、更复杂、功能更完整的包的一小部分。

我们将使用的基本组件是 TCP 侦听器(服务器)和 TCP 拨号器(客户端)。让我们看看这些的基本设置。

服务器

在 TCP 端口上进行侦听非常简单。只需启动net.Listen()方法并处理错误,如下行代码所示:

  listener, err := net.Listen("tcp", ":9000")
  if err != nil {
    fmt.Println ("Could not start server!")
  }

如果启动服务器时出错,请检查防火墙或修改端口。可能是系统上有什么东西正在使用端口 9000。

虽然很简单,但在我们的客户端/拨号端也同样简单。

客户

在本例中,我们让所有东西都在本地主机上运行,如下面的代码行所示。但是,在实际应用中,我们可能会在此处使用内部网地址:

  conn, err := net.Dial("tcp","127.0.0.1:9000")
  if err != nil {
    fmt.Println("Could not connect to server!")
  }

在这个应用中,我们演示了两种不同的方法来处理Read()上长度未知的字节缓冲区。第一种是使用strings.TrimRight()修剪字符串的一种相当粗糙的方法。此方法允许您定义您不感兴趣的字符计数作为输入的一部分,如以下代码行所示。大多数情况下,我们可以假设空白字符是缓冲区长度中未使用的部分。

sendMessage := []byte(cM.name + ": " + 
  strings.TrimRight(string(buf)," \t\r\n"))

用这种方式处理字符串通常既不雅观又不可靠。如果我们在这里得到了我们不期望的东西会怎么样?字符串将是缓冲区的长度,在本例中为 140 字节。

我们处理这个问题的另一种方法是直接使用缓冲区的末尾。在这种情况下,我们将n变量分配给conn.Read()函数,然后可以将其作为字符串到缓冲区转换中的缓冲区长度,如下代码行所示:

messBuff := make([]byte,1024)
n, err := conn.Read(messBuff)
if err != nil {

}
message := string(messBuff[:n])

这里我们取消息缓冲区接收值的第一个n字节。

这更可靠、更高效,但您肯定会遇到文本接收的情况,您需要删除某些字符以创建更清晰的输入。

此应用中的每个连接都是一个结构,每个用户也是。我们通过在用户加入时将他们推到Users部分来跟踪他们。

The selected username is a command-line argument as follows:

./chat-client nathan
chat-client.exe nathan

我们不会检查以确保只有一个用户具有该名称,因此可能需要逻辑,特别是如果直接消息聊天包含敏感信息。

处理直接消息

在大多数情况下,这个聊天客户端是一个简单的 echo服务器,但正如前面提到的,我们还包括通过调用 Twitter 风格的@语法来进行非全局广播消息的功能。

我们主要通过正则表达式来处理这个问题,如果消息与@user匹配,那么只有该用户才能看到消息;否则,它将向所有人广播。这有点不雅观,因为如果直接消息的发件人的用户名与预期的用户名不匹配,他们将看不到自己的直接消息。

为此,我们在广播之前通过evalMessageRecipient()功能引导每条消息。由于这依赖于用户输入来创建正则表达式(以用户名的形式),请注意,我们应该使用regexp.QuoteMeta()方法来避免正则表达式失败。

我们先来检查一下我们的聊天服务器,它负责维护所有连接并将其传递给 goroutines 进行监听和接收,如下代码所示:

chat-server.go
package main

import
(
  "fmt"
  "strings"
  "net"
  "strconv"
  "regexp"
)

var connectionCount int
var messagePool chan(string)

const (
  INPUT_BUFFER_LENGTH = 140
)

我们使用最大字符缓冲区。这将我们的聊天信息限制为不超过 140 个字符。让我们看看我们的User结构,看看我们可能保留的关于加入的用户的信息,如下所示:

type User struct {
  Name string
  ID int
  Initiated bool

initiated 变量告诉我们,User在连接和通告之后被连接。让我们检查以下代码,以了解登录用户在频道上的监听方式:

  UChannel chan []byte
  Connection *net.Conn
}
The User struct contains all of the information we will maintain 
  for each connection. Keep in mind here we don't do any sanity 
  checking to make sure a user doesn't existthis doesn't 
  necessarily pose a problem in an example, but a real chat client 
  would benefit from a response should a user name already be 
  in use.

func (u *User) Listen() {
  fmt.Println("Listening for",u.Name)
  for {
    select {
      case msg := <- u.UChannel:
        fmt.Println("Sending new message to",u.Name)
        fmt.Fprintln(*u.Connection,string(msg))

    }
  }
}

This is the core of our server: each User gets its own Listen() method, which maintains the User struct's channel and sends and receives messages across it. Put simply, each user gets a concurrent channel of his or her own. Let's take a look at the ConnectionManager struct and the Initiate() function that creates our server in the following code:

type ConnectionManager struct {
  name      string
  initiated bool
}

func Initiate() *ConnectionManager {
  cM := &ConnectionManager{
    name:      "Chat Server 1.0",
    initiated: false,
  }

  return cM
}

我们的ConnectionManager结构只启动一次。这设置了一些相对装饰性的属性,其中一些可以在请求或聊天登录时返回。我们将检查函数,该函数尝试大致识别任何发送消息的预期收件人,如下所示:

func evalMessageRecipient(msg []byte, uName string) bool {
  eval := true
  expression := "@"
  re, err := regexp.MatchString(expression, string(msg))
  if err != nil {
    fmt.Println("Error:", err)
  }
  if re == true {
    eval = false
    pmExpression := "@" + uName
    pmRe, pmErr := regexp.MatchString(pmExpression, string(msg))
    if pmErr != nil {
      fmt.Println("Regex error", err)
    }
    if pmRe == true {
      eval = true
    }
  }
  return eval
}

这是我们的路由器,它获取字符串的@部分,并使用它来检测一个想要隐藏的接收者,以防公众消费。如果用户不存在或已离开聊天室,我们不会返回错误。

使用regexp包的正则表达式的格式依赖于re2语法,在中描述 https://code.google.com/p/re2/wiki/Syntax

Let's take a look at the code for the Listen() method of the ConnectionManager struct:

func (cM *ConnectionManager) Listen(listener net.Listener) {
  fmt.Println(cM.name, "Started")
  for {

    conn, err := listener.Accept()
    if err != nil {
      fmt.Println("Connection error", err)
    }
    connectionCount++
    fmt.Println(conn.RemoteAddr(), "connected")
    user := User{Name: "anonymous", ID: 0, Initiated: false}
    Users = append(Users, &user)
    for _, u := range Users {
      fmt.Println("User online", u.Name)
    }
    fmt.Println(connectionCount, "connections active")
    go cM.messageReady(conn, &user)
  }
}

func (cM *ConnectionManager) messageReady(conn net.Conn, user 
  *User) {
  uChan := make(chan []byte)

  for {

    buf := make([]byte, INPUT_BUFFER_LENGTH)
    n, err := conn.Read(buf)
    if err != nil {
      conn.Close()
      conn = nil
    }
    if n == 0 {
      conn.Close()
      conn = nil
    }
    fmt.Println(n, "character message from user", user.Name)
    if user.Initiated == false {
      fmt.Println("New User is", string(buf))
      user.Initiated = true
      user.UChannel = uChan
      user.Name = string(buf[:n])
      user.Connection = &conn
      go user.Listen()

      minusYouCount := strconv.FormatInt(int64(connectionCount-1), 
        10)
      conn.Write([]byte("Welcome to the chat, " + user.Name + ", 
        there are " + minusYouCount + " other users"))

    } else {

      sendMessage := []byte(user.Name + ": " + 
        strings.TrimRight(string(buf), " \t\r\n"))

      for _, u := range Users {
        if evalMessageRecipient(sendMessage, u.Name) == true {
          u.UChannel <- sendMessage
        }

      }

    }

  }
}geReady (per connectionManager) function instantiates new 
  connections into a User struct, utilizing first sent message as 
  the user's name.

var Users []*User
This is our unbuffered array (or slice) of user structs.
func main() {
  connectionCount = 0
  serverClosed := make(chan bool)

  listener, err := net.Listen("tcp", ":9000")
  if err != nil {
    fmt.Println ("Could not start server!",err)
  }

  connManage := Initiate()  
  go connManage.Listen(listener)

  <-serverClosed
}

正如所料,main()主要是处理连接和错误,并通过serverClosed通道保持服务器的开放和非阻塞。

我们可以采用许多方法来改进发送消息的方式。第一种方法是调用绑定到用户名的映射(或哈希表)。如果映射的键存在,如果用户已经存在,我们可以返回一些错误功能,如以下代码段所示:

type User struct {
  name string
}
var Users map[string] *User

func main() {
  Users := make(map[string] *User)
}

检查我们的客户

我们的客户端应用稍微简单一点,主要是因为我们不太关心阻塞代码。

虽然我们确实有两个并发操作(等待消息和等待用户输入来发送消息),但这比我们的服务器要简单得多,它需要分别并发地侦听每个创建的用户和分发发送的消息。

现在让我们将聊天客户端与聊天服务器进行比较。显然,客户端对连接和用户的总体维护较少,因此我们不需要使用几乎同样多的通道。让我们看看我们的聊天客户端的代码:

chat-client.go
package main

import
(
  "fmt"
  "net"
  "os"
  "bufio"
  "strings"
)
type Message struct {
  message string
  user string
}

var recvBuffer [140]byte

func listen(conn net.Conn) {
  for {

      messBuff := make([]byte,1024)
      n, err := conn.Read(messBuff)
      if err != nil {
        fmt.Println("Read error",err)
      }
      message := string(messBuff[:n])
      message = message[0:]

      fmt.Println(strings.TrimSpace(message))
      fmt.Print("> ")
  }

}

func talk(conn net.Conn, mS chan Message) {

      for {
      command := bufio.NewReader(os.Stdin)
        fmt.Print("> ")        
                line, err := command.ReadString('\n')

                line = strings.TrimRight(line, " \t\r\n")
        _, err = conn.Write([]byte(line))                       
                if err != nil {
                        conn.Close()
                        break

                }
      doNothing(command)  
        }  

}

func doNothing(bf *bufio.Reader) {
  // A temporary placeholder to address io reader usage

}
func main() {

  messageServer := make(chan Message)

  userName := os.Args[1]

  fmt.Println("Connecting to host as",userName)

  clientClosed := make(chan bool)

  conn, err := net.Dial("tcp","127.0.0.1:9000")
  if err != nil {
    fmt.Println("Could not connect to server!")
  }
  conn.Write([]byte(userName))
  introBuff := make([]byte,1024)    
  n, err := conn.Read(introBuff)
  if err != nil {

  }
  message := string(introBuff[:n])  
  fmt.Println(message)

  go talk(conn,messageServer)
  go listen(conn)

  <- clientClosed
}

阻塞方法 2–循环中的 select 语句

Have you noticed yet that the select statement itself blocks? Fundamentally, the select statement is not different from an open listening channel; it's just wrapped in conditional code.

<- myChannel通道的操作方式与以下代码段相同:

select {
  case mc := <- myChannel:
    // do something
}

一个开放的监听通道不是死锁,只要没有 goroutines 睡眠。你会在正在收听但永远不会收到任何东西的频道上发现这一点,这是另一种基本上等待的方法。

These are useful shortcuts for long-running applications you wish to keep alive but you may not necessarily need to send anything along that channel.

清理 goroutines

Any channel that is left waiting and/or left receiving will result in a deadlock. Luckily, Go is pretty adept at recognizing these and you will almost without fail end up in a panic when running or building the application.

到目前为止,我们的许多示例都使用了延迟的close()方法,即立即并将应该在不同点执行的类似代码片段清晰地组合在一起。

虽然垃圾收集处理了大量的清理工作,但我们主要负责开放通道,以确保我们不会有一个进程等待接收和/或一些东西等待发送,这两个进程同时等待对方。幸运的是,我们无法编译具有可检测死锁条件的任何此类程序,但我们还需要管理等待的关闭通道。

到目前为止,相当多的示例都以一个通用的整数或布尔通道结束,该通道只需等待,这几乎完全用于通道的阻塞效果,并允许我们在应用仍在运行时演示并发代码的效果和输出。在许多情况下,这个通用通道是一个不必要的语法错误,如以下代码行所示:

<-youMayNotNeedToDoThis
close(youmayNotNeedToDoThis)

没有任务发生的事实是一个很好的指标,这就是这样一个例子。如果我们将其修改为包含赋值,则之前的代码将改为以下代码:

v := <-youMayNotNeedToDoThis

它可能表明该值是有用的,而不仅仅是任意的块代码。

阻塞方法 3–网络连接和读取

如果您在没有启动服务器的情况下运行我们早期聊天服务器客户端的代码,您会注意到功能会阻止任何后续 goroutine。我们可以通过在连接上施加比正常时间更长的超时来测试这一点,或者在登录后简单地关闭客户端应用,因为我们没有实现关闭 TCP 连接的方法。

由于我们用于连接的网络读卡器是缓冲的,所以在通过 TCP 等待数据时,我们总是有一个阻塞机制。

创建渠道中的渠道

The preferred and sanctioned way of managing concurrency and state is exclusively through channels.

We've demonstrated a few more complex types of channels, but we haven't looked at what can become a daunting but powerful implementation: channels of channels. This might at first sound like some unmanageable wormhole, but in some situations we want a concurrent action to generate more concurrent actions; thus, our goroutines should be capable of spawning their own.

和往常一样,您管理这一点的方式是通过设计,而实际的代码可能只是一个美学上的副产品。以这种方式构建应用应该使您的代码在大多数情况下更加简洁和干净。

让我们回顾一下之前的 RSS 提要阅读器示例,以演示如何管理它,如以下代码所示:

package main

import (
 "fmt"
)

type master chan Item

var feedChannel chan master
var done chan bool

type Item struct {
 Url  string
 Data []byte
}
type Feed struct {
 Url   string
 Name  string
 Items []Item
}

var Feeds []Feed

func process(feedChannel *chan master, done *chan bool) {
 for _, i := range Feeds {
  fmt.Println("feed", i)
  item := Item{}
  item.Url = i.Url
  itemChannel := make(chan Item)
  *feedChannel <- itemChannel
  itemChannel <- item
 }
 *done <- true
}
func processItem(url string) {
 // deal with individual feed items here
 fmt.Println("Got url", url)
}

func main() {
 done := make(chan bool)
 Feeds = []Feed{Feed{Name: "New York Times", Url: "http://rss.nytimes.com/services/xml/rss/nyt/HomePage.xml"},
  Feed{Name: "Wall Street Journal", Url: "http://feeds.wsjonline.com/wsj/xml/rss/3_7011.xml"}}
 feedChannel := make(chan master)
 go func(done chan bool, feedChannel chan master) {
  for {
   select {
   case fc := <-feedChannel:
    select {
    case item := <-fc:
     processItem(item.Url)
    }
   default:
   }
  }
 }(done, feedChannel)
 go process(&feedChannel, &done)
 <-done
 fmt.Println("Done!")
}

在这里,我们将feedChannel作为自定义结构进行管理,该结构本身就是Item类型的通道。这允许我们完全依赖于通过信号量式构造处理的同步通道。

如果我们想看看处理低级同步的另一种方法,sync.atomic提供了一些简单的迭代模式,允许您直接在内存中管理同步。

根据 Go 的文档,这些操作需要非常小心,并且容易出现数据一致性错误,但是如果您需要直接触摸内存,这是一种方法。当我们谈论高级并发特性时,我们将直接使用这个包。

Pprof–又一个很棒的工具

就在你认为你已经看到了 Go 令人惊叹的工具集的整个范围时,总会有一个更实用的工具,一旦你意识到它的存在,你就会想知道没有它你是如何生存的。

Go 格式非常适合清理代码;-race标志对于检测可能的竞态条件至关重要,但存在一个更为强大的手插式污垢工具,用于分析您的最终应用,即 pprof。

谷歌最初创建 PPROF 来分析 C++应用的循环结构和内存分配(以及相关类型)。

如果您认为您有 Go 运行时中提供的测试工具没有发现的性能问题,那么它特别有用。这也是在任何应用中生成数据结构的可视化表示的一种非常好的方法。

Some of this functionality also exists as part of the Go testing package and its benchmarking tools—we'll explore that more in Chapter 7, Performance and Scalability.

要使 pprof 的运行时版本正常工作,首先需要进行几项设置。我们需要包括runtime.pprof包和flag包,它们允许命令行解析(在本例中,用于 pprof 的输出)。

如果我们使用聊天服务器代码,我们可以添加几行代码,让应用为性能分析做好准备。

让我们确保将这两个包与其他包一起包括在内。我们可以使用下划线语法向编译器表明,我们只对包的副作用感兴趣(意味着我们得到了包的初始化函数和全局变量),如以下代码行所示:

import
(
  "fmt"
...
  _ "runtime/pprof"
)

接下来,在我们的main()函数中,我们包含一个标志解析器,它将解析和解释 pprof 生成的数据,并在 CPU 配置文件不存在时创建 CPU 配置文件本身(如果无法创建,则创建 bailing),如以下代码段所示:

var profile = flag.String("cpuprofile", "", "output pprof data to 
  file")

func main() {
  flag.Parse()
  if *profile != "" {
    flag,err := os.Create(*profile)
    if err != nil {
      fmt.Println("Could not create profile",err)
    }
    pprof.StartCPUProfile(flag)
    defer pprof.StopCPUProfile()

  }
}

This tells our application to generate a CPU profiler if it does not exist, start the profiling at the beginning of the execution, and defer the end of the profiling until the application exits successfully.

创建了该文件后,我们可以使用cpuprofile标志运行二进制文件,该标志告诉程序生成一个配置文件,如下所示:

./chat-server -cpuprofile=chat.prof

为了的多样性(以及任意开发更多的资源),我们将暂时放弃聊天服务器,并在退出之前创建一个生成数十个 goroutines 的循环。这将为我们提供一个比简单且长寿的聊天服务器更令人兴奋的评测数据演示,尽管我们将简要地回到这一点:

下面是生成更详细和有趣的分析数据的示例代码:

package main

import (
  "flag"
  "fmt"
  "math/rand"
  "os"
  "runtime"
  "runtime/pprof"
)

const ITERATIONS = 99999
const STRINGLENGTH = 300

var profile = flag.String("cpuprofile", "", "output pprof data to 
  file")

func generateString(length int, seed *rand.Rand, chHater chan 
  string) string {
  bytes := make([]byte, length)
  for i := 0; i < length; i++ {
    bytes[i] = byte(rand.Int())
  }
  chHater <- string(bytes[:length])
  return string(bytes[:length])
}

func generateChannel() <-chan int {
  ch := make(chan int)
  return ch
}

func main() {

  goodbye := make(chan bool, ITERATIONS)
  channelThatHatesLetters := make(chan string)

  runtime.GOMAXPROCS(2)
  flag.Parse()
  if *profile != "" {
    flag, err := os.Create(*profile)
    if err != nil {
      fmt.Println("Could not create profile", err)
    }
    pprof.StartCPUProfile(flag)
    defer pprof.StopCPUProfile()

  }
  seed := rand.New(rand.NewSource(19))

  initString := ""

  for i := 0; i < ITERATIONS; i++ {
    go func() {
      initString = generateString(STRINGLENGTH, seed, 
        channelThatHatesLetters)
      goodbye <- true
    }()

  }
  select {
  case <-channelThatHatesLetters:

  }
  <-goodbye

  fmt.Println(initString)

}

当我们从中生成概要文件时,可以运行以下命令:

go tool pprof chat-server chat-server.prof 

这将启动应用本身的 PPR。这为我们提供了几个命令来报告生成的静态文件,如下所示:

  • topN:此显示了概要文件中顶部的N样本,其中N表示您想要查看的显式数字。

  • web: This creates a visualization of data, exports it to SVG, and opens it in a web browser. To get the SVG output, you'll need to install Graphviz as well (http://www.graphviz.org/).

    您还可以使用某些标志直接运行 pprof,以多种格式输出,或按如下方式启动浏览器:

    • --text:此生成文本报告
    • --web:此生成 SVG 并在浏览器中打开
    • --gv:生成鬼影视图附言
    • --pdf:此生成要输出的 PDF
    • --SVG: This generates the SVG to output
    • --gif: This generates the GIF to output

命令行结果足以说明问题,但特别有趣的是,可以看到应用的阻塞配置文件以描述性的、可视化的方式显示,如下图所示。当您使用 pprof 工具时,只需键入web,浏览器就会生成以 SVG 形式详细描述的CPU 配置文件。

Pprof – yet another awesome tool

这里的想法不是关于文本,而是关于复杂性

瞧,我们突然洞察到我们的程序是如何利用 CPU 时间消耗的,以及我们的应用是如何执行、循环和退出的。

在典型的 Go 方式中,pprof 工具也存在于net/http包中,尽管它更以数据为中心而非视觉。这意味着,您可以直接将结果输出到 Web 进行分析,而不是只处理命令行工具。

与命令行工具一样,您将直接通过 localhost 看到 block、goroutine、heap 和 thread 配置文件以及完整的堆栈大纲,如以下屏幕截图所示:

Pprof – yet another awesome tool

要生成这个服务器,您只需要在应用中包含几行关键代码,构建它,然后运行它。对于本例,我们在聊天服务器应用中包含了代码,它允许我们获取一个仅使用命令行的应用的 Web 视图。

确保您已包含net/httplog套餐。您还需要http/pprof套餐。代码片段如下所示:

import(_(_ 
  "net/http/pprof"
  "log"
  "net/http"
)

然后,只需在应用中的某个位置(理想情况下,main()函数顶部附近)包含此代码,如下所示:

  go func() {
    log.Println(http.ListenAndServe("localhost:6060", nil))
  }()

一如往常,港口完全是一个偏好问题。

然后您可以在localhost:6060找到数量的分析工具,包括:

  • All tools can be found at http://localhost:6060/debug/pprof/
  • http://localhost:6060/debug/pprof/block?debug=1处可以找到阻塞轮廓
  • 所有 goroutine 的配置文件可在http://localhost:6060/debug/pprof/goroutine?debug=1中找到
  • 堆的详细配置文件见http://localhost:6060/debug/pprof/heap?debug=1
  • 创建的线程配置文件可在http://localhost:6060/debug/pprof/threadcreate?debug=1中找到

In addition to the blocking profile, you may find a utility to track down inefficiency in your concurrent strategy through the thread creation profile. If you find a seemingly abnormal amount of threads created, you can toy with the synchronization structure as well as runtime parameters to streamline this.

请记住,以这种方式使用 pprof 还将包括一些可归因于httppprof包的分析和概要文件,而不是您的核心代码。您会发现某些行显然不是应用的一部分;例如,聊天服务器的线程创建分析包括以下几行内容:

#       0x7765e         net/http.HandlerFunc.ServeHTTP+0x3e     /usr/local/go/src/pkg/net/http/server.go:1149
#       0x7896d         net/http.(*ServeMux).ServeHTTP+0x11d /usr/local/go/src/pkg/net/http/server.go:1416

鉴于我们在本次迭代中明确避免了通过 HTTP 或 web 套接字交付聊天应用,这一点应该相当明显。

除此之外,还有更明显的冒烟枪,如下所示:

#       0x139541        runtime/pprof.writeHeap+0x731           /usr/local/go/src/pkg/runtime/pprof/pprof.go:447
#       0x137aa2        runtime/pprof.(*Profile).WriteTo+0xb2   /usr/local/go/src/pkg/runtime/pprof/pprof.go:229
#       0x9f55f         net/http/pprof.handler.ServeHTTP+0x23f  /usr/local/go/src/pkg/net/http/pprof/pprof.go:165
#       0x9f6a5         net/http/pprof.Index+0x135              /usr/local/go/src/pkg/net/http/pprof/pprof.go:177

一些我们永远无法从最终编译的二进制文件中减少的系统和 Go 核心机制如下:

#       0x18d96 runtime.starttheworld+0x126 
  /usr/local/go/src/pkg/runtime/proc.c:451

十六进制值表示运行时函数内存中的地址。

提示

给 Windows 用户的一个提示:pprof 在*nix 环境中很容易使用,但在 Windows 下可能需要进行一些更为艰巨的调整。具体来说,您可能需要一个 bash替代品,比如 Cygwin。您还可能会发现一些必要的调整,以确保其自身(实际上是一个 Perl 脚本)符合要求。对于 64 位 Windows 用户,请确保安装 ActivePerl 并直接使用 64 位版本的 Perl 执行 Perl 脚本。

在发布时,在 64 位 OSX 上运行此功能也存在一些问题。

处理死锁和错误

在编译代码时,每当您遇到死锁错误时,您都会看到常见的半神秘错误字符串,可以说,解释了是哪个 goroutine 留下来负责。

但是,请记住,您始终能够使用 Go 的内置 panic 调用您自己的 panic,这对于构建您自己的错误捕获保护措施非常有用,以确保数据一致性和理想的操作。代码如下:

package main

import
(
  "os"
)

func main() {
  panic("Oh No, we forgot to write a program!")
  os.Exit(1)
}

这可以在您希望向开发人员或最终用户提供详细退出信息的任何地方使用。

总结

在探索了一些新的方法来检查 Go 代码阻塞和死锁的方式之后,我们现在还可以使用一些工具来检查 CPU 配置文件和资源使用情况。

希望到目前为止,您可以使用简单的 goroutine 和通道构建一些复杂的并发系统,一直到结构、接口和其他通道的多路复用通道。

到目前为止,我们已经构建了一些功能性应用,但接下来我们将利用我们所做的一切来构建一个可用的 web 服务器,它解决了一个经典问题,并可用于设计内部网、文件存储系统等。

In the next chapter, we'll take what we've done in this chapter with regard to extensible channels and apply it to solving one of the oldest challenges the Internet has to offer: concurrently serving 10,000 (or more) connections.