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之间的并发程序中。