Skip to content

Latest commit

 

History

History
890 lines (604 loc) · 39.1 KB

File metadata and controls

890 lines (604 loc) · 39.1 KB

三、制定并行策略

在上一章中,我们研究了 Go 所依赖的并发模型,它使您作为开发人员的生活更加轻松。我们还看到了并行性和并发性的可视化表示。这些帮助我们理解序列化、并发和并行应用之间的差异和重叠。

然而,任何并发应用最关键的部分不是并发本身,而是并发进程之间的通信和协调。

在本章中,我们将介绍如何为一个应用创建一个计划,该应用严重影响流程之间的通信,以及缺乏协调如何导致一致性方面的重大问题。我们将研究如何在纸上可视化并行策略,以便更好地预测潜在问题。

复杂并发中的应用效率

当设计应用时,为了简单,我们通常避免复杂模式,因为我们假设简单的系统通常是最快和最有效的。似乎唯一合乎逻辑的是,运动部件较少的机器比运动部件较多的机器效率更高。

当它应用于并发时,这里的悖论是,增加冗余和明显更多的可移动部分通常会导致更高效的应用。如果我们认为并发方案,如 GOOTUTIN,是无限可扩展的资源,采用更多应该总是导致某种形式的效率效益。这不仅适用于并行并发,也适用于单核并发。

如果您发现自己正在设计一个以效率、速度和一致性为代价利用并发性的应用,那么您应该问问自己该应用是否真的需要并发性。

当我们谈论效率时,我们不仅仅是在讨论速度。效率还应权衡 CPU 和内存开销以及确保数据一致性的成本。

例如,如果一个应用稍微受益于并发性,但需要一个复杂和/或计算代价高昂的过程来保证数据的一致性,那么完全重新评估该策略是值得的。

保持数据的可靠性和最新应该是最重要的;虽然拥有不可靠的数据可能并不总是有毁灭性的影响,但它肯定会损害应用的可靠性。

用竞态检测识别竞态条件

如果您曾经编写过一个依赖于函数或方法的精确计时和顺序来创建所需输出的应用,那么您已经非常熟悉竞争条件。

在处理并发性的任何时候,这些都是非常常见的,在引入并行性时更是如此。在前几章中,我们实际遇到了其中的一些函数,特别是我们的递增数函数。

关于种族状况最常用的教育例子是银行账户。假设您从 1000 美元开始,尝试 200 个 5 美元的交易。每笔交易都需要查询账户的当前余额。如果通过,交易将被批准,并从余额中扣除 5 美元。如果失败,交易被拒绝,余额保持不变。

在并发事务(大多数情况下是在另一个线程中)的某个时刻发生查询之前,这一切都是好的。例如,当另一个线程正在删除 5 美元但尚未完成时,如果一个线程询问“您的帐户中有 5 美元吗?”,您可能会得到一笔本应被拒绝的已批准交易。

追踪种族状况的原因至少可以说是一个巨大的头痛。在 Go 的 1.1 版中,谷歌引入了一个种族检测工具,可以帮助你定位潜在的问题。

让我们举一个具有 RACE 条件的多线程应用的非常基本的例子,看看 Golang 如何帮助我们调试它。在本例中,我们将构建一个银行帐户,该帐户以 1000 美元开始,运行 100 笔交易,金额在 0 美元到 25 美元之间。

每个事务将在其自己的 goroutine 中运行,如下所示:

package main

import(
  "fmt"
  "time"
  "sync"
  "runtime"
  "math/rand"
)  

var balance int
var transactionNo int

func main() {
  rand.Seed(time.Now().Unix())
  runtime.GOMAXPROCS(2)
  var wg sync.WaitGroup

  tranChan := make(chan bool)

  balance = 1000
  transactionNo = 0
  fmt.Println("Starting balance: $",balance)

  wg.Add(1)
  for i := 0; i < 100; i++ {
    go func(ii int, trChan chan(bool)) {
      transactionAmount := rand.Intn(25)
      transaction(transactionAmount)
      if (ii == 99) {
        trChan <- true
      }

    }(i,tranChan)
  }

  go transaction(0)
  select {

    case <- tranChan:
      fmt.Println("Transactions finished")
      wg.Done()

  }

  wg.Wait()
  close(tranChan)
  fmt.Println("Final balance: $",balance)
}

func transaction(amt int) (bool) {

  approved := false  
  if (balance-amt) < 0 {
    approved = false
  }else {
    approved = true
    balance = balance - amt
  }

  approvedText := "declined"
  if (approved == true) {
    approvedText = "approved"
  }else {

  }
  transactionNo = transactionNo + 1
  fmt.Println(transactionNo,"Transaction for $",amt,approvedText)
  fmt.Println("\tRemaining balance $",balance)
  return approved
}

Depending on your environment (and whether you enable multiple processors), you might have the previous goroutine operate successfully with a $0 or more final balance. You might, on the other hand, simply end up with transactions that exceed the balance at the time of transaction, resulting in a negative balance.

那么我们如何确定呢?

对于大多数应用和语言,此过程通常涉及大量的运行、重新运行和日志记录。比赛条件下出现令人生畏且费力的调试过程并不罕见。谷歌知道这一点,并为我们提供了一个种族状况检测工具。要进行测试,只需在测试、构建或运行应用时使用–race标志,如图所示:

go run -race race-test.go

在上一代码上运行时,Go 将执行应用,然后报告任何可能的竞态条件,如下所示:

>> Final balance: $0
>> Found 2 data race(s)

在这里,Go 告诉我们有两个潜在的数据竞争条件。它并没有告诉我们这些肯定会造成数据一致性问题,但如果您遇到这些问题,这可能会给您一些关于原因的线索。

如果您查看输出的顶部,您将得到关于导致竞争条件的更详细说明。在此示例中,详细信息如下所示:

==================
WARNING: DATA RACE
Write by goroutine 5: main.transaction()   /var/go/race.go:75 +0xbd 
 main.func┬╖001()   /var/go/race.go:31 +0x44

Previous write by goroutine 4: main.transaction() 
 /var/go/race.go:75 +0xbd main.func┬╖001()   /var/go/race.go:31 
 +0x44

Goroutine 5 (running) created at: main.main()   /var/go/race.go:36 
 +0x21c

Goroutine 4 (finished) created at: main.main()   /var/go/race.go:36 
 +0x21c

我们得到了一个详细的,完整的线索,我们潜在的种族条件存在的地方。很有帮助,是吗?

race检测器保证不会产生误报,因此您可以将结果作为代码中存在潜在问题的有力证据。这里强调了这种可能性,因为在正常情况下,竞争条件可能无法被检测到。通常,在竞争条件出现之前,应用可能会按预期工作数天、数月甚至数年。

提示

我们已经提到了日志记录,如果您对 Go 的核心语言不太熟悉,您的思维可能会转向 stdout、文件日志等等。到目前为止,我们仍然坚持使用标准输出,但您可以使用标准库来处理此日志记录。Go 的日志包允许您写入 io 或 stdout,如图所示:

  messageOutput := os.Stdout
  logOut := log.New(messageOutput,"Message: ",log.
  Ldate|log.Ltime|log.Llongfile);
  logOut.Println("This is a message from the 
  application!")

这将产生以下输出:

Message: 2014/01/21 20:59:11 /var/go/log.go:12: This is a message from the application!

那么,日志包与您自己的滚动相比有什么优势呢?除了标准化之外,这个包在输出方面也是同步的。

So what now? Well, there are a few options. You can utilize your channels to ensure data integrity with a buffered channel, or you can use the sync.Mutex struct to lock your data.

使用相互排除

通常情况下,互斥被认为是一种低级且最著名的同步方法在应用中,您应该能够解决通道间通信中的数据一致性问题。但是,在某些情况下,您需要在使用某个值时真正阻止该值的读/写。

在 CPU 级别,互斥表示在寄存器之间交换二进制整数值以获取和释放锁。当然,我们会在更高的层次上处理一些事情。

通过使用WaitGroup结构,我们已经熟悉了 sync 包,但是该包还包含条件变量struct CondOnce,它们只执行一次操作,互斥锁RWMutexMutex。顾名思义,RWMutex开放给多个读卡器和/或写卡器进行锁定和解锁;在本章后面的和第 5 章中有更多关于的内容,锁定、阻塞和更好的通道

正如包名所暗示的,所有这些都使您能够防止任何数量的 goroutine 和/或线程访问的数据上的争用条件。使用此包中的任何方法都不能确保数据和结构中的原子性,但它确实为您提供了有效管理原子性的工具。让我们看看在并发线程安全应用中巩固帐户余额的几种方法。

如前所述,我们可以在通道级别协调数据更改,无论该通道是缓冲的还是非缓冲的。让我们把逻辑和数据操作转移到通道上,看看–race标志显示了什么。

如果我们修改主循环(如以下代码所示),以利用通道接收的消息来管理平衡值,我们将避免竞争条件:

package main

import(
  "fmt"
  "time"
  "sync"
  "runtime"
  "math/rand"
)  

var balance int
var transactionNo int

func main() {
  rand.Seed(time.Now().Unix())
  runtime.GOMAXPROCS(2)
  var wg sync.WaitGroup
  balanceChan := make(chan int)
  tranChan := make(chan bool)

  balance = 1000
  transactionNo = 0
  fmt.Println("Starting balance: $",balance)

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

    go func(ii int) {

      transactionAmount := rand.Intn(25)
      balanceChan <- transactionAmount

      if ii == 99 {
        fmt.Println("Should be quittin time")
        tranChan <- true
        close(balanceChan)
        wg.Done()
      }

    }(i)

  }

  go transaction(0)

    breakPoint := false
    for {
      if breakPoint == true {
        break
      }
      select {
        case amt:= <- balanceChan:
          fmt.Println("Transaction for $",amt)
          if (balance - amt) < 0 {
            fmt.Println("Transaction failed!")
          }else {
            balance = balance - amt
            fmt.Println("Transaction succeeded")
          }
          fmt.Println("Balance now $",balance)

        case status := <- tranChan:
          if status == true {
            fmt.Println("Done")
            breakPoint = true
            close(tranChan)

          }
      }
    }

  wg.Wait()

  fmt.Println("Final balance: $",balance)
}

func transaction(amt int) (bool) {

  approved := false  
  if (balance-amt) < 0 {
    approved = false
  }else {
    approved = true
    balance = balance - amt
  }

  approvedText := "declined"
  if (approved == true) {
    approvedText = "approved"
  }else {

  }
  transactionNo = transactionNo + 1
  fmt.Println(transactionNo,"Transaction for $",amt,approvedText)
  fmt.Println("\tRemaining balance $",balance)
  return approved
}

这一次,我们让通道完全管理数据。让我们看看我们在做什么:

transactionAmount := rand.Intn(25)
balanceChan <- transactionAmount

这个仍然会生成一个介于 0 和 25 之间的随机整数,但我们不是将其传递给函数,而是沿着通道传递数据。通道允许您灵活地控制数据的所有权。然后我们会看到 select/listener,它在很大程度上反映了我们在本章前面定义的transaction()函数:

case amt:= <- balanceChan:
fmt.Println("Transaction for $",amt)
if (balance - amt) < 0 {
  fmt.Println("Transaction failed!")
}else {
  balance = balance - amt
  fmt.Println("Transaction succeeded")
}
fmt.Println("Balance now $",balance)

为了测试我们是否避免了比赛条件,我们可以再次使用-race标志运行go run,并且看不到任何警告。

频道可以被看作是被认可的同步处理方式dataUse Sync.Mutex()

如前所述,拥有内置的种族检测器是大多数语言的开发人员所不具备的奢侈品,拥有它可以让我们测试方法并获得每个方法的实时反馈。

我们注意到,不鼓励使用显式互斥来支持 goroutine 通道。这并不总是完全正确的,因为每件事都有一个合适的时间和地点,互斥体也不例外。值得注意的是,互斥体是通过 Go-for 通道在内部实现的。如前所述,您可以使用显式通道来处理读写操作,并在它们之间处理数据。

然而,这并不意味着显式锁没有用处。一个有很多读操作和很少写操作的应用可能会受益于写操作的显式锁;这并不一定意味着读取将是脏读取,但它可能会导致更快和/或更多的并发执行。

For the sake of demonstration, let's remove our race condition using an explicit lock. Our -race flag tells us where it encounters read/write race conditions, as shown:

Read by goroutine 5: main.transaction()   /var/go/race.go:62 +0x46

前一行只是我们将从种族检测报告中获得的其他几行中的一行。如果我们查看代码中的第 62 行,我们会找到对balance的引用。我们还将找到对transactionNo的引用,这是我们的第二个比赛条件。解决这两个问题的最简单方法是在transaction函数的内容周围放置互斥锁,因为这是修改balancetransactionNo变量的函数。transaction功能如下:

func transaction(amt int) (bool) {
  mutex.Lock()

  approved := false
  if (balance-amt) < 0 {
    approved = false
  }else {
    approved = true
    balance = balance - amt
  }

  approvedText := "declined"
  if (approved == true) {
    approvedText = "approved"
  }else {

  }
  transactionNo = transactionNo + 1
  fmt.Println(transactionNo,"Transaction for $",amt,approvedText)
  fmt.Println("\tRemaining balance $",balance)

  mutex.Unlock()
  return approved
}

我们还需要在应用顶部将mutex定义为一个全局变量,如下所示:

var mutex sync.Mutex

如果现在使用-race标志运行应用,则不会收到任何警告。

出于的实际目的,mutex变量是WaitGroup结构的替代品,该结构起到条件同步机制的作用。这也是通道操作的方式,在 goroutine 之间包含并隔离沿通道移动的数据。通过将 goroutine 状态绑定到WaitGroup,通道可以以这种方式有效地作为先进先出工具工作;然后,可以通过较低级别的互斥锁安全地提供通过通道访问的数据。

另一个值得注意的是信道的多功能性我们可以在一系列 goroutine 之间共享一个信道来接收和/或发送数据,作为一级公民,我们可以在函数中传递它们。

探索超时

另一个值得注意的是,我们可以在指定的时间后显式地杀死通道。如果您决定手动处理相互排除,那么这是一个更复杂的操作。

通过通道终止长时间运行的例程的能力非常有用;考虑一个依赖于网络的操作,不仅应该被限制在短时间内,而且不允许长时间运行。换言之,您希望在几秒钟内完成该过程;但是,如果它运行超过一分钟,我们的应用应该知道出现了错误,足以停止尝试在该频道上侦听或发送。以下代码演示如何在select调用中使用超时通道:

func main() {

  ourCh := make(chan string,1)

  go func() {

  }()

  select {
    case <-time.After(10 * time.Second):
      fmt.Println("Enough's enough")
      close(ourCh)
  }

}

如果我们运行上一个简单的应用,我们将看到 goroutine 将被允许在整整 10 秒钟内什么也不做,之后我们实现了一个超时保护,它将帮助我们摆脱困境。

您可以看到这在网络应用中特别有用;即使在阻塞和依赖线程的服务器时代,也会实现类似的超时,以防止单个行为不当的请求或进程阻塞整个服务器。这是一个经典的web 服务器问题的基础,我们将在后面更详细地讨论这个问题。

一致性的重要性

在我们的示例中,我们将构建一个事件调度器。如果我们可以参加一个会议,并且我们同时收到两个会议邀请请求,那么如果存在竞争条件,我们将获得双重预订。或者,跨两个 goroutine 锁定的数据可能会导致两个请求都被拒绝或导致实际的死锁。

我们希望保证任何可用性请求都是一致的,既不应重复预订,也不应错误地阻止事件请求(因为两个并发或并行例程同时锁定数据)。

同步我们的并发操作

“同步”一词字面上是指在同一时间发生的事物的暂时存在。那么看来,最恰当的同步性证明将是与时间本身有关的东西。

When we think about the ways time impacts us, it's generally a matter of scheduling, due dates, and coordination. Going back to our preliminary example from the Preface, if one wishes to plan their grandmother's birthday party, the following types of scheduled tasks can take several forms:

  • 必须在一定时间内完成的事情(实际参与方)
  • Things that cannot be done until another task is completed (putting up decorations before they're purchased)
  • 在不影响结果的情况下可以按任何特定顺序完成的事情(打扫房间)
  • 可以按任何顺序完成但可能会影响结果的事情(在找出祖母最喜欢的蛋糕之前先买一块蛋糕)

考虑到这些,我们将尝试通过设计一个约会日历来处理一些基本的人工日程安排,该日历可以处理上午 9 点到下午 5 点之间有一小时时间间隔的任何人数。

项目-多用户预约日历

当你决定写一个程序时,你会做什么?

如果你和很多人一样,你会考虑这个项目;也许您和一个团队将编写一个规范或需求文档,然后您将开始编码。有时,会有一张代表应用工作方式的传真图。

通常,确定应用的体系结构和内部工作的最佳方法是将铅笔放在纸上,直观地表示程序的工作方式。对于许多线性或串行应用,这通常是不必要的步骤,因为事情将以可预测的方式工作,不需要在应用逻辑本身内进行任何特定的协调(尽管协调第三方软件可能从规范中受益)。

您可能熟悉一些类似下图的逻辑:

The project – multiuser appointment calendar

这里的逻辑是有道理的。如果您还记得我们的前言,当人类绘制流程时,我们倾向于序列化它们。从视觉上看,使用有限数量的过程从第一步到第二步很容易理解。

然而,在设计并发应用时,我们必须至少考虑无数并发请求、进程和逻辑,以确保我们的应用在我们想要的地方结束,并提供我们期望的数据和结果。

在前面的示例中,我们完全忽略了“用户可用”可能失败或报告旧数据或错误数据的可能性。当我们发现这些问题时,解决这些问题是否更有意义,或者我们是否应该将它们作为控制流的一部分进行预测?增加模型的复杂性可以帮助我们减少未来数据完整性问题的可能性。

让我们再次设想一下这一点,考虑到可用性轮询器,该轮询器将通过对时间/用户对的任何给定请求为用户请求可用性。

可视化并发模式

正如我们已经讨论过的,我们希望创建一个基本蓝图,说明我们的应用应该如何作为一个起点。在这里,我们将实现一些与用户活动相关的控制流,以帮助我们决定需要包括哪些功能。下图说明了控制流的外观:

Visualizing a concurrent pattern

在前面的图中,我们预测了在哪里可以使用并发和并行进程来共享数据,以定位故障点。如果我们以这种图形化的方式设计并发应用,我们以后就不太可能发现竞争条件。

当我们谈到Go 如何帮助您在应用完成运行后定位这些问题时,我们理想的开发工作流程是尝试从一开始就解决这些问题。

开发我们的服务器需求

现在我们已经了解了调度过程应该如何工作,我们需要确定我们的应用将需要的组件。在这种情况下,组件如下所示:

  • A web server handler
  • 用于输出的模板
  • 确定日期和时间的系统

Web server

在上一章中的可视化并发示例中,我们使用了 Go 的内置http包,我们在这里也将这样做。有很多好的框架可以实现这一点,但它们主要是扩展核心 Go 功能,而不是重新发明轮子。以下是其中一些功能,从最轻到最重列出:

  • Web.go: http://webgo.io/

    Web.go非常轻量级和精简,它提供了net/http包中无法提供的一些路由功能。

  • Gorilla: http://www.gorillatoolkit.org/

    Gorilla 是一把瑞士军刀,用于扩充net/http套餐。它不是特别重,而且速度快,实用性强,而且非常干净。

  • Revel: http://robfig.github.io/revel/

    Revel is the heaviest of the three, but it focuses on a lot of intuitive code, caching, and performance. Look for it if you need something mature that will face a lot of traffic.

第 6 章C10K–Go中的非阻塞 Web 服务器中,我们将推出我们自己的 Web 服务器和框架,其唯一目标是实现极高的性能。

大猩猩工具包

对于这个应用,我们将部分使用 Gorilla web 工具包。Gorilla 是一个相当成熟的web 服务平台,能够满足我们在本地的一些需求,即在 URL 路由中包含正则表达式的能力。(注意:Web.Go 还扩展了一些功能。)Go 的内部 HTTP 路由处理程序相当简单;当然,您可以扩展此功能,但我们将在这里沿着一条老旧且可靠的路径走捷径。

我们使用这个包只是为了简化 URL 路由,但是 Gorilla web toolkit 还包括处理 cookie、会话和请求变量的包。我们将在第 6 章C10K——Go中的一个非阻塞 Web 服务器中进一步研究这个包。

使用模板

As Go is intended as a system language, and as system languages often deal with the creation of servers with clients, some care was put into making it a well-featured alternative to create web servers.

任何与“web 语言”打过交道的人都会知道,除此之外,你还需要一个框架,最好是一个处理 web 表示层的框架。如果你承担这样一个项目,你很可能会寻找或构建自己的框架,这是事实,Go 使模板化变得非常容易。

模板包有两种类型:texthttp。尽管它们都服务于不同的端点,但提供动态性和灵活性的相同属性应用于表示层,而不是严格意义上的应用层。

提示

The text template package is intended for general plaintext documents, while the http template package handles the generation of HTML and related documents.

这些模板范例现在太普遍了;如果你看一下http/template软件包,你会发现它与胡子有很多相似之处,胡子是一种更流行的变体。虽然 Go 中有一个 Mustache 端口,但模板包中没有默认情况下不处理的内容。

有关胡子的更多信息,请访问http://mustache.github.io/

胡子的一个潜在优势是它可以在其他语言中使用。如果您曾经觉得需要将一些应用逻辑移植到另一种语言(或将现有模板移植到 Go 中),那么利用 Mustache 可能是有利的。也就是说,您牺牲了 Go 模板的许多扩展功能,即从编译包中取出 Go 代码并将其直接移动到模板控制结构中的能力。虽然 Mustache(及其变体)具有控制流,但它们可能不会镜像 Go 的模板系统。以以下为例:

<ul>
{{range .Users}}
<li>A User </li>
{{end}}
</ul>

鉴于对 Go 的逻辑结构的熟悉,在我们的模板语言中保持它们的一致性也是有意义的。

We won't show all the specific templates in this thread, but we will show the output. If you wish to peruse them, they're available at mastergoco.com/chapters/3/templates.

时间

We're not doing a whole lot of math here; time will be broken into hour blocks and each will be set to either occupied or available. At this time, there aren't a lot of external date/time packages for Go. We're not doing any heavy-date math, but it doesn't really matter because Go's time package should suffice even if we were.

事实上,由于我们有从上午 9 点到下午 5 点的文字时间段,我们只需将其设置为 9-17 的 24 小时时间值,并调用函数将其转换为语言日期。

终点

我们希望识别 REST 端点(通过GET请求),并简要描述它们的工作方式。您可以将这些视为模型-视图-控制器体系结构中的模块或方法。以下是我们将使用的端点模式列表:

  • entrypoint/register/{name}:这是我们将在用户列表中添加一个名称。如果用户存在,它将失败。
  • entrypoint/viewusers:在这里,我们将展示一个用户列表及其时间段,既有可用的时间段,也有已占用的时间段。
  • entrypoint/schedule/{name}/{time}:此将初始化安排约会的尝试。

每个都有一个附带的模板,该模板将报告预期操作的状态。

自定义结构

我们将处理用户和响应(网页),因此我们需要两个结构来分别表示。一个结构如下所示:

type User struct {
  Name string
  email string
  times[int] bool
}

另一个结构如下所示:

type Page struct {
  Title string
  Body string
}

我们将保持页面尽可能简单。我们将在大部分代码中生成 HTML,而不是进行大量迭代循环。

Our endpoints for requests will relate to our previous architecture, using the following code:

func users(w http.ResponseWriter, r *http.Request) {
}
func register(w http.ResponseWriter, r *http.Request) {
}
func schedule(w http.ResponseWriter, r *http.Request) {
}

多用户预约日历

在部分中,我们将快速查看我们的示例约会日历应用,它试图控制特定元素的一致性,以避免明显的竞争条件。以下是完整的代码,包括路由和模板:

package main

import(
  "net/http"
  "html/template"
  "fmt"
  "github.com/gorilla/mux"
  "sync"
  "strconv"
)

type User struct {
  Name string
  Times map[int] bool
  DateHTML template.HTML
}

type Page struct {
  Title string
  Body template.HTML
  Users map[string] User
}

var usersInit map[string] bool
var userIndex int
var validTimes []int
var mutex sync.Mutex
var Users map[string]User
var templates = template.Must(template.New("template").ParseFiles("view_users.html", "register.html"))

func register(w http.ResponseWriter, r *http.Request){
  fmt.Println("Request to /register")
  params := mux.Vars(r)
  name := params["name"]

  if _,ok := Users[name]; ok {
    t,_ := template.ParseFiles("generic.txt")
    page := &Page{ Title: "User already exists", Body: 
      template.HTML("User " + name + " already exists")}
    t.Execute(w, page)
  }  else {
          newUser := User { Name: name }
          initUser(&newUser)
          Users[name] = newUser
          t,_ := template.ParseFiles("generic.txt")
          page := &Page{ Title: "User created!", Body: 
            template.HTML("You have created user "+name)}
          t.Execute(w, page)
    }

}

func dismissData(st1 int, st2 bool) {

// Does nothing in particular for now other than avoid Go compiler 
  errors
}

func formatTime(hour int) string {
  hourText := hour
  ampm := "am"
  if (hour > 11) {
    ampm = "pm"
  }
  if (hour > 12) {
    hourText = hour - 12;
  }
fmt.Println(ampm)
  outputString := strconv.FormatInt(int64(hourText),10) + ampm

  return outputString
}

func (u User) FormatAvailableTimes() template.HTML { HTML := "" 
  HTML += "<b>"+u.Name+"</b> - "

  for k,v := range u.Times { dismissData(k,v)

    if (u.Times[k] == true) { formattedTime := formatTime(k) HTML 
      += "<a href='/schedule/"+u.Name+"/"+strconv.FormatInt(int64(k),10)+"' class='button'>"+formattedTime+"</a> "

    } else {

    }

 } return template.HTML(HTML)
}

func users(w http.ResponseWriter, r *http.Request) {
  fmt.Println("Request to /users")

  t,_ := template.ParseFiles("users.txt")
  page := &Page{ Title: "View Users", Users: Users}
  t.Execute(w, page)
}

func schedule(w http.ResponseWriter, r *http.Request) {
  fmt.Println("Request to /schedule")
  params := mux.Vars(r)
  name := params["name"]
  time := params["hour"]
  timeVal,_ := strconv.ParseInt( time, 10, 0 )
  intTimeVal := int(timeVal)

  createURL := "/register/"+name

  if _,ok := Users[name]; ok {
    if Users[name].Times[intTimeVal] == true {
      mutex.Lock()
      Users[name].Times[intTimeVal] = false
      mutex.Unlock()
      fmt.Println("User exists, variable should be modified")
      t,_ := template.ParseFiles("generic.txt")
      page := &Page{ Title: "Successfully Scheduled!", Body: 
        template.HTML("This appointment has been scheduled. <a 
          href='/users'>Back to users</a>")}

      t.Execute(w, page)

    }  else {
            fmt.Println("User exists, spot is taken!")
            t,_ := template.ParseFiles("generic.txt")
            page := &Page{ Title: "Booked!", Body: 
              template.HTML("Sorry, "+name+" is booked for 
              "+time+" <a href='/users'>Back to users</a>")}
      t.Execute(w, page)

    }

  }  else {
          fmt.Println("User does not exist")
          t,_ := template.ParseFiles("generic.txt")
          page := &Page{ Title: "User Does Not Exist!", Body: 
            template.HTML( "Sorry, that user does not exist. Click 
              <a href='"+createURL+"'>here</a> to create it. <a 
                href='/users'>Back to users</a>")}
    t.Execute(w, page)
  }
  fmt.Println(name,time)
}

func defaultPage(w http.ResponseWriter, r *http.Request) {

}

func initUser(user *User) {

  user.Times = make(map[int] bool)
  for i := 9; i < 18; i ++ {
    user.Times[i] = true
  }

}

func main() {
  Users = make(map[string] User)
  userIndex = 0
  bill := User {Name: "Bill"  }
  initUser(&bill)
  Users["Bill"] = bill
  userIndex++

  r := mux.NewRouter()  r.HandleFunc("/", defaultPage)
    r.HandleFunc("/users", users)  
      r.HandleFunc("/register/{name:[A-Za-z]+}", register)
        r.HandleFunc("/schedule/{name:[A-Za-z]+}/{hour:[0-9]+}", 
          schedule)     http.Handle("/", r)

  err := http.ListenAndServe(":1900", nil)  if err != nil {    // 
    log.Fatal("ListenAndServe:", err)    }

}

请注意,我们将应用的种子设定为用户 Bill。如果您尝试点击/register/bill|bill@example.com,应用将报告用户存在。

由于我们通过通道控制最敏感的数据,因此我们避免了任何竞争条件。我们可以用两种方法来测试这一点。第一种也是最简单的方法是记录注册了多少次成功的约会,并将 Bill 作为默认用户运行此日志。

然后,我们可以针对该操作运行并发负载测试程序。有很多这样的测试人员可用,包括 Apache 的 ab 和 Seake。出于我们的目的,我们将使用 JMeter,主要是因为它允许我们同时测试多个 URL。

提示

虽然我们不一定要使用 JMeter 进行负载测试(相反,我们使用它来运行并发测试),但负载测试程序可以非常有价值地在尚未存在的规模上发现应用中的瓶颈。

For example, if you built a web application that had a blocking element and had 5,000-10,000 requests per day, you may not notice it. But at 5 million-10 million requests per day, it might result in the application crashing.

In the dawn of network servers, this is what happened; servers scaled until one day, suddenly, they couldn't scale further. Load/stress testers allow you to simulate traffic in order to better detect these issues and inefficiencies.

考虑到我们每天有一个用户和八个小时,我们应该以总共不超过八次的成功约会来结束我们的脚本。当然,如果您点击/register端点,您将看到八倍于您添加的用户。以下屏幕截图显示了我们在 JMeter 中的基准测试计划:

A multiuser Appointments Calendar

When you run your application, keep an eye on your console; at the end of our load test, we should see the following message:

Total registered appointments: 8

如果我们按照本章中的初始图形模型表示(带有种族条件)设计我们的应用,那么这是合理的,事实上,我们注册的约会可能远远多于实际存在的约会。

通过隔离潜在的竞争条件,我们保证了数据的一致性,并确保没有人在等待与其他与会者的预约。以下屏幕截图是我们提供的所有用户及其可用预约时间的列表:

A multiuser Appointments Calendar

上一个屏幕截图是我们的初始视图,显示了可用用户及其可用时间段。通过为用户选择时间段,我们将尝试为该特定时间预订时间段。我们下午 5 点从内森开始。

以下屏幕截图显示了当我们尝试与可用用户进行计划时发生的情况:

A multiuser Appointments Calendar

但是,如果我们再次尝试预订(即使是同时预订),我们会收到一条悲伤的消息,Nathan 在下午 5 点无法看到我们,如以下屏幕截图所示:

A multiuser Appointments Calendar

有了这个功能,我们就有了一个多用户日历应用,可以创建新用户、安排日程和阻止双重预订。

让我们看看这个应用中的几个有趣的新点。

首先,您会注意到,我们对应用的大部分部分使用了一个名为generic.txt的模板。这里没有太多内容,只有每个处理程序填写的页面标题和正文。然而,在/users端点上,我们使用users.txt如下:

<html>
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-
    8"> 
  <title>{{.Title}}</title>
</head>
<body>

<h1>{{.Title}}</h1>

{{range .Users}}
<div class="user-row">

  {{.FormatAvailableTimes}}

</div>
{{end}}

</body>
</html>

我们在模板中提到了基于范围的功能,但是{{.FormatAvailableTimes}}是如何工作的呢?在任何给定的上下文中,我们都可以使用特定于类型的函数,这些函数以比模板 lexer 中严格可用的更复杂的方式处理数据。

In this case, the User struct is passed to the following line of code:

func (u User) FormatAvailableTimes() template.HTML {

这行代码然后执行一些条件分析,并返回一个带有时间转换的字符串。

在这个示例中,您可以使用一个通道来控制User.times的流,也可以使用一个显式互斥体,就像我们在这里看到的那样。我们不想限制所有锁,除非绝对必要,因此,如果我们确定请求已通过修改任何给定用户/时间对状态所需的测试,我们只调用Lock()函数。以下代码显示了我们在互斥中设置用户可用性的位置:

if _,ok := Users[name]; ok {
  if Users[name].Times[intTimeVal] == true {
    mutex.Lock()
    Users[name].Times[intTimeVal] = false
    mutex.Unlock()

外部评估检查该名称(键)的用户是否存在。第二次评估检查时间可用性是否存在(true)。如果有,我们锁定变量,将其设置为false,然后进入输出渲染。

如果没有Lock()功能,许多并发连接可能会破坏数据的一致性,并导致用户在给定的一小时内有多个约会。

关于风格的注释

您会注意到尽管我们的大多数变量更喜欢 camelCase,但在结构中有一些大写变量。这是一个重要的 Go 约定值得一提:任何以大写字母开头的结构变量都是公共。任何以小写字母开头的变量都是私有

如果试图在模板文件中输出私有(或不存在)变量,则模板呈现将失败。

关于不变性的注记

Note that whenever possible, we'll avoid using the string type for comparative operations, especially in multithreaded environments. In the previous example, we use integers and Booleans to decide availability for any given user. In some languages, you may feel empowered to assign the time values to a string for ease of use. For the most part, this is fine, even in Go; but assuming that we have an infinitely scalable, shared calendar application, we run the risk of introducing memory issues if we utilize strings in this way.

字符串类型是 Go 中唯一的不可变类型;如果最终为字符串赋值或重新赋值,这一点值得注意。假设在字符串转换为副本后产生内存,这不是问题。然而,在 Go(以及其他一些语言)中,完全可以将原始值保留在内存中。我们可以使用以下示例对此进行测试:

func main() {

  testString := "Watch your top / resource monitor"
  for i:= 0; i < 1000; i++ {

    testString = string(i)

  }
  doNothing(testString)  

  time.Sleep(10 * time.Second)

}

在 Ubuntu 中运行时,大约需要 1.0MB 的内存;其中一些无疑是开销,但却是一个有用的参考点。尽管使用以下代码行,1000 个相对较小的指针不会产生太大影响,但让我们提高一点赌注:

for i:= 0; i < 100000000; i++ {

现在,在经历了 1 亿次内存分配之后,您可以看到对内存的影响(字符串本身在这一点上比初始值长并没有帮助,但它不能解释全部影响)。垃圾收集也发生在这里,这会影响 CPU。在我们这里的初始测试中,CPU 和内存都出现峰值。如果我们用它来代替整数或布尔赋值,我们得到的足迹会小得多。

这并不完全是一个真实的场景,但值得注意的是,在并发环境中,必须进行垃圾收集,以便我们可以评估逻辑的属性和类型。

这也是完全可能的,这取决于您当前的 Go 版本、您的机器等等,在任何一种情况下都可以高效运行。虽然这看起来不错,但我们并行策略规划的一部分应该包括应用在输入、输出、物理资源或所有这些方面进行扩展的可能性。仅仅因为某些东西现在运行良好并不意味着它不值得实施能够防止它以 100 倍规模造成性能问题的效率。

如果你遇到一个字符串是逻辑的地方,但是你想要或者可以从一个可变类型中获益,那么考虑一个字节片。

当然,常量也是不可变的,但如果这是常量变量的隐含用途,那么您应该已经知道这一点。毕竟,可变常量变量是一种矛盾修饰法。

Summary

在深入研究之前,本章希望引导您探索规划和绘制并发应用的方法。通过简要介绍比赛条件和数据一致性,我们试图强调预期设计的重要性。同时,我们使用了一些工具来识别这些问题(如果出现)。

Creating a robust script flowchart with concurrent processes will help you locate possible pitfalls before you create them, and it will give you a better sense of how (and when) your application should be making decisions with logic and data.

在下一章中,我们将研究数据一致性问题,并研究高级通道通信选项,以避免不必要且通常昂贵的缓解功能、互斥和外部进程。