Golang并发编程的高级技巧:Mutex、WaitGroup及其应用
在Go语言中,我们可以很方便地编写并发程序。但在面对一些复杂的场景时,我们需要使用一些高级的技巧。本文将介绍Golang中的两个重要工具:Mutex和WaitGroup,并且通过实际应用场景的演示,让读者更好地理解这些高级技巧的应用。
Mutex
在并发编程中,Mutex是我们最常用的同步工具之一。Mutex全称是Mutual Exclusion(互斥),它可以协调不同线程之间对共享资源的访问,避免不同线程之间对同一共享变量的干扰。
Mutex的使用十分简单。我们只需要调用Mutex类型的Lock()方法锁定共享资源,在临界区代码执行完成之后再调用Unlock()方法解锁。下面是一个简单的例子:
```go
import (
"fmt"
"sync"
)
var counter int
var mutex sync.Mutex // 定义锁
func increment() {
mutex.Lock() // 加锁
counter++
mutex.Unlock() // 解锁
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
increment()
}()
}
wg.Wait()
fmt.Println(counter)
}
```
在这个例子中,我们定义了一个全局变量counter,并用Mutex对其进行保护。increment函数是一个临界区,它会增加counter的值。在main函数中,我们启动了1000个goroutine,并且使用WaitGroup等待它们全部执行完毕。最后,我们输出了counter的值。
使用Mutex可以保证counter在并发访问时不会出现竞态条件(race condition),从而得到正确的结果。需要注意的是,如果我们没有使用Mutex,这个程序很可能会输出一个比1000小的值,因为在并发访问时,不同的goroutine可能会同时对counter进行操作。
WaitGroup
WaitGroup是另一个常用的同步工具,它可以等待一组goroutine全部完成之后再执行下一步操作。在之前的例子中,我们已经使用了WaitGroup来等待所有的goroutine执行完毕。
WaitGroup的使用也十分简单。我们只需要调用Add()方法来添加需要等待的goroutine数量,调用Done()方法来表示已完成一个goroutine,然后调用Wait()方法来等待所有goroutine完成。下面是一个简单的例子:
```go
import (
"fmt"
"sync"
)
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done()
fmt.Printf("Worker %d starting\n", id)
// 模拟工作
time.Sleep(time.Second)
fmt.Printf("Worker %d done\n", id)
}
func main() {
var wg sync.WaitGroup
for i := 1; i <= 5; i++ {
wg.Add(1)
go worker(i, &wg)
}
wg.Wait()
fmt.Println("All workers done")
}
```
在这个例子中,我们定义了一个worker函数,它模拟了一些工作,然后打印出worker的ID和完成信息。在main函数中,我们启动了5个worker goroutine,并使用WaitGroup等待它们全部执行完毕。最后,我们输出"All workers done"。
Mutex和WaitGroup的组合应用
我们已经了解了Mutex和WaitGroup的基本使用方法,下面我们看一个更加复杂的例子,来展示它们的组合应用。在这个例子中,我们需要爬取一些网页,并计算它们的大小和下载时间。这些操作会并行进行,但是为了避免对同一文件进行并发写入,我们需要使用Mutex对文件进行保护。同时,我们也需要使用WaitGroup来等待所有goroutine执行完毕,并计算全部网页的平均大小和平均下载时间。下面是代码:
```go
import (
"fmt"
"io/ioutil"
"net/http"
"os"
"sync"
"time"
)
var (
urls = []string{
"https://www.baidu.com",
"https://www.qq.com",
"https://www.taobao.com",
"https://www.weibo.com",
"https://www.zhihu.com",
}
mutex sync.Mutex // 定义锁
wg sync.WaitGroup
count int64 // 记录网页总大小
times int64 // 记录下载总时间
result []*item // 记录每个网页的大小和下载时间
)
type item struct {
url string
size int64
time int64
}
func download(url string) ([]byte, error) {
resp, err := http.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
return ioutil.ReadAll(resp.Body)
}
func writeToFile(filename string, data []byte) error {
mutex.Lock()
defer mutex.Unlock()
f, err := os.OpenFile(filename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return err
}
defer f.Close()
_, err = f.Write(data)
if err != nil {
return err
}
return nil
}
func worker(url string) {
defer wg.Done()
start := time.Now().UnixNano()
data, err := download(url)
if err != nil {
fmt.Printf("Error downloading %s: %s\n", url, err)
return
}
size := int64(len(data))
if err := writeToFile("result.txt", data); err != nil {
fmt.Printf("Error writing to file: %s\n", err)
return
}
end := time.Now().UnixNano()
elapsed := end - start
mutex.Lock()
count += size
times += elapsed
result = append(result, &item{url: url, size: size, time: elapsed})
mutex.Unlock()
}
func main() {
for _, url := range urls {
wg.Add(1)
go worker(url)
}
wg.Wait()
num := int64(len(urls))
avgSize := float64(count) / float64(num)
avgTime := float64(times) / float64(time.Millisecond) / float64(num)
fmt.Printf("Downloaded %d pages, average size: %.2f KB, average time: %.2f ms\n", num, avgSize/1024, avgTime)
for _, item := range result {
fmt.Printf("%s: size=%d bytes, time=%.2f ms\n", item.url, item.size, float64(item.time)/float64(time.Millisecond))
}
}
```
在这个例子中,我们定义了5个URL,并使用WaitGroup启动了5个worker goroutine。在每个worker goroutine中,我们首先使用download函数下载网页,然后使用writeToFile函数将网页写入文件(result.txt),并且使用Mutex对count、times、result等共享资源进行保护。在每个worker goroutine执行完毕后,我们将下载时间、网页大小等信息存储在result变量中。
在main函数中,我们使用WaitGroup等待所有的worker goroutine执行完毕,并计算所有网页的平均大小和平均下载时间。最后,我们输出结果,包括每个网页的下载时间和大小。
总结
在这篇文章中,我们学习了Golang中的两个重要工具:Mutex和WaitGroup。Mutex可以保证同一时刻只有一个goroutine对共享资源进行访问,避免竞态条件,而WaitGroup可以协调多个goroutine之间的执行顺序,并实现多个goroutine的协同工作。
通过一个更加复杂的例子,我们展示了Mutex和WaitGroup的组合应用。在实际应用中,我们可以将它们用于一些需要保护共享资源或需要协调多个goroutine之间的并发程序中。