英文:
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 >= 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 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 <= maxIteration {
wg.Add(1)
go processSearchResults(&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>'z' {...}
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.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论