Golang并发Map中的竞态问题 - 原因分析与解决

Golang并发Map中的竞态问题 - 原因分析与解决 我创建了一个由互斥锁保护的Go并发映射,但当两个goroutine同时修改该映射时,Go仍然会因并发映射访问和映射写入异常而引发恐慌。我不知道还能采取什么措施来阻止这种竞态条件的发生。

package main

import (
	"os"
	"fmt"
	"io/ioutil"
	"strings"
	"sync"
)

type concurrentMap struct{
	sync.Mutex
	urls map[string]int
}

func (c *concurrentMap) addToMap(url string) {
	c.Lock()
	if val , ok := c.urls[url];ok{
		c.urls[url] = val + 1
	}else{
		c.urls[url] = 1
	}
	c.Unlock()

}



func NewConcurrentMap() *concurrentMap {
	return &concurrentMap{
		urls:make(map[string]int),
	}
}


func (c *concurrentMap) getUrls() map[string]int {
	return c.urls
}


func main(){
	c := NewConcurrentMap()
	var waitGroup sync.WaitGroup
	file_path := os.Args[1]
	contents , err := ioutil.ReadFile(file_path)
	if err != nil {
		fmt.Println(err)
		return
	}
	fileContents := string(contents)
	lines := strings.Split(fileContents,"\n")
	half := len(lines) / 2

	go func(subset []string){
		waitGroup.Add(1)
		defer waitGroup.Done()
		for _,line := range subset {
			data := strings.Split(line,",")
			if len(data) < 2{
				continue
			}
			url := data[1]
			c.addToMap(url)
		}
	}(lines[0:half])

	go func(subset []string){
		waitGroup.Add(1)
		defer waitGroup.Done()
		for _,line := range subset {
			data := strings.Split(line,",")
			if len(data) < 2{
				continue
			}
			url := data[1]
			c.addToMap(url)
		}
	}(lines[half+1:])
	waitGroup.Wait()

	fmt.Println(c.getUrls())
}

更多关于Golang并发Map中的竞态问题 - 原因分析与解决的实战教程也可以访问 https://www.itying.com/category-94-b0.html

2 回复

我认为问题在于您在goroutine内部将goroutine添加到等待组中。然后

waitGroup.Add(1)
go func(subset []string){
	defer waitGroup.Done()
	for _,line := range subset {
		data := strings.Split(line,",")
		if len(data) < 2{
			continue
		}
		url := data[1]
		c.addToMap(url)
	}
}(lines[0:half])
waitGroup.Add(1)
go func(subset []string){
	defer waitGroup.Done()
	for _,line := range subset {
		data := strings.Split(line,",")
		if len(data) < 2{
			continue
		}
		url := data[1]
		c.addToMap(url)
	}
}(lines[half+1:])

waitGroup.Wait()

更多关于Golang并发Map中的竞态问题 - 原因分析与解决的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


在你的代码中,存在几个导致竞态条件的问题。主要问题在于WaitGroup的使用方式不正确,以及getUrls()方法没有进行同步保护。

问题分析:

  1. WaitGroup.Add(1)应该在goroutine外部调用,而不是在goroutine内部
  2. getUrls()方法返回了映射的引用,这可能导致在读取时发生并发访问
  3. 切片分割时可能存在索引越界问题

修正后的代码:

package main

import (
	"fmt"
	"io/ioutil"
	"os"
	"strings"
	"sync"
)

type concurrentMap struct {
	sync.RWMutex
	urls map[string]int
}

func (c *concurrentMap) addToMap(url string) {
	c.Lock()
	defer c.Unlock()
	if val, ok := c.urls[url]; ok {
		c.urls[url] = val + 1
	} else {
		c.urls[url] = 1
	}
}

func NewConcurrentMap() *concurrentMap {
	return &concurrentMap{
		urls: make(map[string]int),
	}
}

func (c *concurrentMap) getUrls() map[string]int {
	c.RLock()
	defer c.RUnlock()
	
	// 返回副本而不是原始映射的引用
	result := make(map[string]int)
	for k, v := range c.urls {
		result[k] = v
	}
	return result
}

func main() {
	if len(os.Args) < 2 {
		fmt.Println("请提供文件路径参数")
		return
	}
	
	c := NewConcurrentMap()
	var waitGroup sync.WaitGroup
	file_path := os.Args[1]
	contents, err := ioutil.ReadFile(file_path)
	if err != nil {
		fmt.Println(err)
		return
	}
	
	fileContents := string(contents)
	lines := strings.Split(fileContents, "\n")
	half := len(lines) / 2

	// 在goroutine外部调用Add
	waitGroup.Add(2)

	go func(subset []string) {
		defer waitGroup.Done()
		for _, line := range subset {
			data := strings.Split(line, ",")
			if len(data) < 2 {
				continue
			}
			url := data[1]
			c.addToMap(url)
		}
	}(lines[0:half])

	go func(subset []string) {
		defer waitGroup.Done()
		for _, line := range subset {
			data := strings.Split(line, ",")
			if len(data) < 2 {
				continue
			}
			url := data[1]
			c.addToMap(url)
		}
	}(lines[half:]) // 修正索引:使用half而不是half+1
	
	waitGroup.Wait()

	fmt.Println(c.getUrls())
}

主要改进:

  1. 使用defer c.Unlock()确保锁一定会被释放
  2. WaitGroup.Add(2)移到goroutine外部,确保在goroutine开始执行前计数器已增加
  3. sync.Mutex改为sync.RWMutex,在读取时使用读锁
  4. getUrls()方法中添加读锁保护,并返回映射的副本而不是引用
  5. 修正切片分割的索引问题
  6. 添加参数检查

这些修改应该能解决你遇到的竞态条件问题。

回到顶部