如何在多个 goroutine 之间执行并发操作,共享同一个数据结构?

huangapple go评论77阅读模式
英文:

How to perform concurrent operation on a data structure that is shared by multiple goroutines

问题

我正在解决一个Go语言的谜题,通过将ASCII字节值旋转来匹配字符串,无论是按行(从左到右)还是按列(从上到下)查找2D字节数组中的字符串。我能够按顺序解决它,但当我尝试并发解决时,我尝试启动一个goroutine来处理特定输入组合,以查看可能的25个旋转中是否有任何匹配项。代码的摘要如下:

FindByConcurrentRot是一个方法,它接受一个2D字符数组输入,并尝试在可能的各种输入组合中查找字符串的匹配项。

问题是-下面使用的并发方法是否高效?如何改进它?将顺序程序“原样”转换为并发程序的方法是否错误?即是否应该重新编写整个程序以充分利用并发特性?

// searchResult定义了在并发搜索期间传递的用于确定是否已找到匹配项的信息
type searchResult struct {
	row   int
	col   int
	rot   int
	found bool
}

// processSearchResults运行goroutine来执行特定旋转的搜索
func processSearchResults(wg *sync.WaitGroup, iter int, resultChan chan searchResult, table [][]byte, word string) {
	// 在函数返回时调用goroutine结束
	defer wg.Done()
	if iter >= 1 {
		rotate(table, iter)
	}
	x, y, match := present(table, word)
	if match {
		resultChan <- searchResult{row: x, col: y, rot: iter, found: true}
		return
	}
	resultChan <- searchResult{found: false}
}

// doCopy为并发搜索的每次迭代创建原始表的副本
// 这是一个昂贵的操作,因为涉及内存复制操作
// 复制是为了让goroutine拥有自己的数据控制权,而不是将原始数据传递给它们,以避免数据竞争
func doCopy(table [][]byte) [][]byte {
	copyTable := make([][]byte, len(table))
	for i := range table {
		copyTable[i] = make([]byte, len(table[i]))
		copy(copyTable[i], table[i])
	}
	return copyTable
}

// FindByConcurrentRot通过使用并发原语在ASCII字符数组中搜索字符串
// 该函数通过旋转所有可能的输入表的组合来生成goroutine,并将它们的原始表的副本传递给它们,并在通道中处理结果
func FindByConcurrentRot(table [][]byte, word string) (int, int, int, bool) {
	iter := 0
	resultChan := make(chan searchResult, maxIteration)
	var wg sync.WaitGroup

	for iter <= maxIteration {
		wg.Add(1)
		go processSearchResults(&wg, iter, resultChan, doCopy(table), word)
		iter++
	}

	// 当我们等待所有goroutine完成时,停止遍历通道
	go func() {
		wg.Wait()
		close(resultChan)
	}()

	resultCount := 0
	// 处理通道中每个goroutine的结果
	for result := range resultChan {
		resultCount++
		if result.found {
			return result.row, result.col, result.rot, result.found
		}
	}

	return 0, 0, 0, false
}

完整的MVCE代码在此Go Playground链接中:https://go.dev/play/p/7YFAsAlFRUw

注意:基准测试的结果表明,顺序版本比并发版本性能更好。

go test -bench=.
goos: darwin
goarch: amd64
pkg: github.com/inianv/go-wordsearchpuzzle/wordsearch
cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
BenchmarkTestSequentialSearch-12    	11003130	       106.4 ns/op
BenchmarkTestConcurrentSearch-12    	   38865	     32031 ns/op
PASS
ok  	github.com/inianv/go-wordsearchpuzzle/wordsearch	3.339s
英文:

I was solving a puzzle in Go, that finds a string in 2D byte array by rotating the ASCII byte values to match the string either row wise (left->right) or column-wise (top->bottom). I was able to solve it sequentially and when it came to solving it concurrently I tried to launch one go-routine to work on a particular combination of input, to see if any of the possible 25 rotations could find a match. The abstract of the code as follows

The FindByConcurrentRot is the method that takes a 2D char array input and tries to find a match of the string in various combinations of input possible.

Question is - Is the below used concurrent method performant? How could it be improved? Is the approach to convert the sequential routine "as-is" as to a concurrent program wrong? i.e. should the whole program be re-written for making best use of the concurrent features?

// searchResult defines the information needed to pass to identify if a match has been identified during concurrent search
type searchResult struct {
	row   int
	col   int
	rot   int
	found bool
}

// processSearchResults runs the gorotuine to perform the search for a particular rotation of input
func processSearchResults(wg *sync.WaitGroup, iter int, resultChan chan searchResult, table [][]byte, word string) {
	// call goroutine end when the function returns
	defer wg.Done()
	if iter &gt;= 1 {
		rotate(table, iter)
	}
	x, y, match := present(table, word)
	if match {
		resultChan &lt;- searchResult{row: x, col: y, rot: iter, found: true}
		return
	}
	resultChan &lt;- searchResult{found: false}
}

// doCopy creates a copy of the original table to passed for each iteration of the concurrent search
// This is an EXPENSIVE operation on a goroutine, because of memory copy operations
// The copy is needed for the goroutines to have their own control of data and not run into data
// races by passing the original data to each of them
func doCopy(table [][]byte) [][]byte {
	copyTable := make([][]byte, len(table))
	for i := range table {
		copyTable[i] = make([]byte, len(table[i]))
		copy(copyTable[i], table[i])
	}
	return copyTable
}

// FindByConcurrentRot searches for the string in the ASCII character array by using concurrency primitives
// The function spawns goroutines by rotating all combinations of the input table possible and passing them
// their own copy of original table and processes the results back in a channel
func FindByConcurrentRot(table [][]byte, word string) (int, int, int, bool) {
	iter := 0
	resultChan := make(chan searchResult, maxIteration)
	var wg sync.WaitGroup

	for iter &lt;= maxIteration {
		wg.Add(1)
		go processSearchResults(&amp;wg, iter, resultChan, doCopy(table), word)
		iter++
	}

	// Stop the range channel, when we finish waiting for all the goroutines
	go func() {
		wg.Wait()
		close(resultChan)
	}()

	resultCount := 0
	// process result from each goroutine on the channel
	for result := range resultChan {
		resultCount++
		if result.found {
			return result.row, result.col, result.rot, result.found
		}
	}

	return 0, 0, 0, false
}

Full MVCE at this Go playground link - https://go.dev/play/p/7YFAsAlFRUw

Note: The results from benchmarking indicated the sequential version outperfomed the concurrent one by a lot

go test -bench=.
goos: darwin
goarch: amd64
pkg: github.com/inianv/go-wordsearchpuzzle/wordsearch
cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
BenchmarkTestSequentialSearch-12    	11003130	       106.4 ns/op
BenchmarkTestConcurrentSearch-12    	   38865	     32031 ns/op
PASS
ok  	github.com/inianv/go-wordsearchpuzzle/wordsearch	3.339s

答案1

得分: 1

这种方法很可能在复制表格时花费了太多的循环。由于每个goroutine都在修改表格,所以每个goroutine都必须获得一个单独的副本。

另一种方法是在只读表格之上添加一个层,为每个goroutine提供修改后的视图。这并不能保证更好的性能,但很可能比使用多个goroutine进行复制要更好。

这种方法是创建一个表格视图:

type tableView struct {
   inc int
   table [][]byte
}

func (t tableView) get(row,col int) byte {
   v:=t.table[row][col]
   v+=t.inc
   if v>'z' {...}
   return v
}

然后,你初始化并传递一个tableView的实例。

再次强调:这可能不会像你期望的那样快,但很可能比多个表格副本要好。你需要进行测试并观察。

英文:

It is likely that this approach is suffering from too many cycles spent on copying tables. Since each goroutine is modifying the table, each goroutine has to get a separate copy.

Another approach to that would be to add a layer on top of a read-only table that gives a modified view for each goroutine. That does not guarantee better performance, but it is likely to perform better than copying with multiple goroutines.

The approach would be to have a table view:

type tableView struct {
inc int
table [][]byte
}
func (t tableView) get(row,col int) byte {
v:=t.table[row][col]
v+=t.inc
if v&gt;&#39;z&#39; {...}
return v
}

Then, you initialize and pass an instance of tableView around.

Again: this may not perform as fast as you expect, but it is likely to perform better than multiple copies of the table. You have to test and see.

huangapple
  • 本文由 发表于 2022年2月20日 02:09:30
  • 转载请务必保留本文链接:https://go.coder-hub.com/71187756.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定