并发通过利用多个处理核心来提高性能。Go 中的 API 支持帮助程序员以非常有效的方式实现并行算法。大多数主流编程语言都将并发支持作为附加功能,但 Go 内置了并发支持。本文介绍了 Go 中的并发编程。
Go 中的并发编程
并发编程充分利用了大多数现代计算机中存在的底层多处理内核。这个概念已经存在了很长一段时间,即使单个处理器中只有一个内核。使用多线程是在许多编程语言(包括 C/C++、Java 等)中实现某种并发性的常见做法。
单个线程基本上代表一小组计划独立执行的指令。您可以在概念上将其可视化为大型作业中的一项小任务。因此,多个执行线程被分组以执行一个复杂的进程并同时执行。多个任务之间的这种连贯性给人一种并发执行的感觉。但请注意,任何底层受限硬件(例如单核处理器)只能通过以分时方式调度作业来完成。
今天,处理硬件由多个内核驱动。因此,总是需要一种可以充分发挥其潜力的语言。主流编程语言逐渐意识到这一事实,并试图在其核心特性中包含并发的概念。然而,Go 的设计者认为为什么不在其核心特性中构建一种基于并发概念的语言呢?Go 就是这样一种语言,它提供了用于编写并发程序的高级 API。
多线程问题
多线程程序不仅难以编写和维护,而且难以调试。此外,并不总是可以使用多个线程来拆分任何算法以使其在性能方面像并发编程一样高效。多线程有它自己的开销。许多职责,例如进程间通信或共享内存访问,都由环境处理。开发人员只需专注于手头的业务,而不是纠缠于并行处理的细节。
牢记这些问题,另一种解决方案是完全依赖操作系统进行多处理。在这种情况下,开发人员负责处理进程间通信的复杂性或共享内存并发的开销。这种技术可以高度调整以提高性能,但也很容易造成混乱。
Go 对并发编程的好处
Go 在并发编程方面提供了三重解决方案。
- 高级别的支持不仅使实现并发更简单,而且更易于维护。
- 使用 goroutine。Goroutines 比线程轻得多。
- Go 的自动垃圾收集可以在没有开发人员干预的情况下处理内存管理的复杂性。
在 Go 中处理并发问题
goroutines使创建并发和形成基本原语变得容易。这里执行的活动称为goroutine. 考虑一个具有两个不相互调用的函数的程序。在顺序执行中,一个函数完成执行,然后调用另一个函数。但是,使用 Go,该功能可以同时处于活动状态和运行状态。如果功能不相关,这很容易实现,但是当功能相互交织并共享彼此的执行时间线时,可能会出现问题。即使使用 Go 对并发的高级支持,这些陷阱也无法完全避免,特别是如果 main 函数在依赖于它的函数之前完成它的执行。因此,我们必须小心让主 goroutine 等到所有工作完成。
另一个问题是死锁情况,其中多个 goroutine 锁定某个资源以保持独占性,而另一个 goroutine 尝试同时获取相同的锁。这种类型的风险在并发编程中很常见,但 Go 有一个解决方法,可以通过使用channels来避免使用锁。通常,当工作完成时,会创建一个表示执行完成的通道。另一种方法是使用sync.WaitGroup等待报告。但在任何一种情况下,仍然可能发生死锁,并且最好通过仔细设计来避免死锁。Go 只是提供了工具来规划正确的并发功能。
带有 WaitGroup 示例的 goroutine
我们可以通过在任何函数调用前加上关键字go来简单地创建一个 goroutine 。然后,该函数通过创建一个包含调用帧的 goroutine 来安排它像线程一样运行,从而像线程一样运行。与普通函数一样,它可以访问任何参数、全局变量以及在其范围内可访问的任何内容。
这是一个简单的代码来检查任何网站的状态,无论它是打开还是关闭。接下来,我们在相同的代码上应用 goroutine。观察当我们应用并发时执行如何变得更快。
package main
import (
"fmt"
"net/http"
"time"
)
func main() {
start := time.Now()
sitelist := []string{
"https://www.google.com//",
"https://www.duckduckgo.com/",
"https://www.developer.com/",
"https://www.codeguru.com/",
"https://www.nasa.gov/",
"https://golang.com/",
}
for _, site := range sitelist {
GetSiteStatus(site)
}
fmt.Printf("\n\nTime elapsed since %v\n\n", time.Since(start))
}
func GetSiteStatus(site string) {
if _, err := http.Get(site); err != nil {
fmt.Printf("%s is down\n", site)
} else {
fmt.Printf("%s is up\n", site)
}
}
上面的 Go 代码示例将产生以下输出:
https://www.google.com// is up
https://www.duckduckgo.com/ is up
https://www.developer.com/ is up
https://www.codeguru.com/ is up
https://www.nasa.gov/ is up
https://code8cn.com/ is up
https://golang.com/ is up
Time elapsed since 6.666198944s
现在,如果我们在上面的代码中使用WaitGroup同步机制应用并发,性能会得到多方面的提升。
package main
import (
"fmt"
"net/http"
"sync"
"time"
)
func main() {
var wg sync.WaitGroup
start := time.Now()
sitelist := []string{
"https://www.google.com//",
"https://www.duckduckgo.com/",
"https://www.developer.com/",
"https://www.codeguru.com/",
"https://www.nasa.gov/",
"https://golang.com/",
}
for _, site := range sitelist {
go GetSiteStatus(site, &wg)
wg.Add(1)
}
wg.Wait()
fmt.Printf("\n\nTime elapsed since %v\n\n", time.Since(start))
}
func GetSiteStatus(site string, wg *sync.WaitGroup) {
defer wg.Done()
if _, err := http.Get(site); err != nil {
fmt.Println("%s is down\n", site)
} else {
fmt.Printf("%s is up\n", site)
}
}
再一次,这是我们的 Go 程序的输出:
https://www.nasa.gov/ is up
https://www.google.com// is up
https://www.developer.com/ is up
https://www.duckduckgo.com/ is up
https://golang.com/ is up
https://www.codeguru.com/ is up
Time elapsed since 1.816887681s
WaitGroup是一种同步机制。请注意,对于创建的每个 goroutine,WaitGroup使用wg.Add(1)递增,并在例程完成时使用wg.Done()递减。wg.Wait ()是一个阻塞命令,让主 goroutine 等待所有 goroutine 任务完成。
在 Go 中与 Mutex 同步
除了WaitGroup之外,Go 还提供了其他共享资源的同步机制,例如Mutex。每当 goroutine 同时想要使用sync.Mutex.lock()访问共享资源时,它就会使用锁定机制。同样,有一种方法可以使用sync.Mutex.Unlock()解锁。
上述代码中的以下更改实现了与Mutex的同步机制。
//...
func main() {
var wg sync.WaitGroup
var mut sync.Mutex
//...
for _, site := range sitelist {
go GetSiteStatus(site, &wg, &mut)
wg.Add(1)
}
//...
}
func GetSiteStatus(site string, wg *sync.WaitGroup, mut *sync.WaitGroup) {
defer wg.Done()
if _, err := http.Get(site); err != nil {
fmt.Println("%s is down\n", site)
} else {
mut.Lock()
defer mut.Unlock()
fmt.Printf("%s is up\n", site)
}
}
在 Golang 中实现通道
通道是 goroutine 活动之间的连接。它们通过发送和接收值来充当一个 goroutine 与另一个 goroutine 之间的通信机制,就像 UNIX 管道一样,我们可以将数据放在一端并在另一端接收。因此,通道有两个主要操作,发送和接收。与具有特定类型关联值的管道通道不同,因此必须在创建过程中定义具有元素类型的通道。例如,元素类型为int的通道可写为:
cha := make(chan int)
元素类型确定将通过通道传递的值的类型。使用通道声明一个空的接口类型使我们能够传递任何类型的值并在接收端进行确定。可以将相同类型的通道与相等运算符“==”进行比较,并且可以将空通道与nil进行比较。通道支持具有可配置缓冲区大小的缓冲。
以下是如何在 Go 中实现通道的快速示例:
package main
import "fmt"
func main() {
odd := make(chan int)
oddsquared := make(chan int)
//odd
go func() {
for x := 1; x < 10; x++ {
if x%2 != 0 {
odd <- x
}
}
close(odd)
}()
//oddsquared
go func() {
for x := range odd {
oddsquared <- x * x
}
close(oddsquared)
}()
for x := range oddsquared {
fmt.Println(x)
}
}
上面的例子很简单,奇数goroutine 在 10 个元素后完成循环,它关闭奇数通道。这会导致oddsquared完成其循环并关闭oddsquared通道。最后,主 goroutine 完成它的循环并且程序退出。
关于 Go 并发编程的最终想法
除了高级并发支持之外,Go 还通过sync/atomic包提供了低级功能。它们通常不被普通程序员使用,主要用于支持高级的东西,例如线程安全同步和数据结构实现。对于并发编程,主要使用诸如 goroutines 和通道之类的高级工具。Go 中高级支持 API 的程度很少能与任何其他主流编程语言相提并论。这清楚地表明,Go 的设计者付出了坚实的努力,将并发的支持融入到其核心设施中。