Golang并发排序:使用Goroutines实现数字排序

Golang并发排序:使用Goroutines实现数字排序 我正在通过一些练习来学习Go语言,我遇到了一个练习,需要从输入中排序数字并使用Go协程。我们暂且不讨论练习的目的或我的算法,这只是探索性的尝试。

我将在下面解释我的问题。 这是我的代码,由于需要一些输入,你无法在Google Playground上使用它。

package main

import (
	"fmt"
	"sort"
)

func divideList(list []int, cut int) (tmp, s []int) {
	tmp = append(list[cut:])
	s = append(list[:cut])
	return tmp, s
}

func sortList(list []int, c chan []int) {
	sort.Ints(list)
	c <- list
}

func main() {
	// 定义要排序的数字数量
	size := 5

	// 将数字列表放入切片
	list := []int{-9, -11, 12, 13, 9}
	fmt.Println("Your digits input : ", list)

	// 将数字列表分成4个不同的切片
	cut := size / 4
	list, s1 := divideList(list, cut)
	cut = len(list) / 3
	list, s2 := divideList(list, cut)
	cut = len(list) / 2
	list, s3 := divideList(list, cut)
	s4 := list

	// 在不同的Go协程中排序
	c := make(chan []int, 4)
	go sortList(s1, c)
	go sortList(s2, c)
	go sortList(s3, c)
	go sortList(s4, c)

	sortedS1 := <-c
	sortedS2 := <-c
	sortedS3 := <-c
	sortedS4 := <-c

	fmt.Println(sortedS1)
	fmt.Println(sortedS2)
	fmt.Println(sortedS3)
	fmt.Println(sortedS4)
	fmt.Println("-----------------------")

	// 合并4个已排序的切片
	sortedList1 := append(sortedS1, sortedS2...)
	fmt.Println(sortedList1)
	sortedList2 := append(sortedS3, sortedS4...)
	fmt.Println(sortedList2)
	finalList := append(sortedList1, sortedList2...)
	fmt.Println(finalList)

	// 我们需要再次排序
	sort.Ints(finalList)
	fmt.Println("Here your digits sorted: ", finalList)
}

这是我的结果(你可能需要尝试多次才能发现问题)

How many integer do you want to sort? 5
Please type all your digits you want to sort and press Enter: -9 -11 12 13 9
Your digits input :  [-9 -11 12 13 9]
[-9]
[12]
[9 13]
[-11]
-----------------------
[-9 12]
[9 13 12]
[-9 12 9 13 12]
Here your digits sorted:  [-9 9 12 12 13]

Process finished with exit code 0

看这部分注释之后:// merge the 4 sorted slices 问题在于我正在合并4个切片,由于无法一次性合并它们,我分两次合并,但我不明白为什么我的结果会不同。

这部分结果是正常的 sortedList1 := append(sortedS1, sortedS2...) 将 [-9] 与 [12] 合并,很好,我得到了 [-9 12]

但是这部分 sortedList2 := append(sortedS3, sortedS4...) 我正在合并 [9 13] 与 [-11],却得到了 [9 13 12] 显然它与另一个切片合并了,但为什么? 我知道这不是最好的算法,但我需要理解这个问题。 我在Windows上使用Goland时遇到了这个问题,我更喜欢在Linux上工作,但我正在尝试GoLand IDE。

如果有人能解释这个问题,那就太好了。请记住,你需要多次运行代码才能发现问题,因为有时它能正确排序。


更多关于Golang并发排序:使用Goroutines实现数字排序的实战教程也可以访问 https://www.itying.com/category-94-b0.html

15 回复

哦,原来是这样!谢谢 🙂

更多关于Golang并发排序:使用Goroutines实现数字排序的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


但为什么行为会时不时地改变?哪一部分是未定义行为?

你好,cornos,你能告诉我这个程序的问题描述是什么吗?

当然,我怎么会没想到StackOverflow呢。 我会做一些测试,然后去StackOverflow上试试,谢谢你。

SO 指的是 Stackoverflow - https://stackoverflow.com 我不喜欢 SO,但那里有一些实力很强的 Go 开发者会回答问题。

如果你执行

sort.Ints(finalList)

为什么不

直接执行

sort.Ints(list)

并跳过中间的所有步骤呢?

Cornos:

这部分是将切片分割成4个切片

我的意思是你可以直接返回部分: return list[cut:], list[:cut] 你的追加操作没有起到任何作用。

抱歉回复晚了。 基本上问题在于,我覆盖了切片的底层数组。 由于底层数组发生了变化,导致结果切片中的数据出错。 我的代码非常糟糕,本可以简化这个问题。 这段代码存在很多问题。 那是我刚开始学习Go的时候。

该练习的目的是学习Go协程的工作原理,它要求我将切片分成4份,并使用Go协程对每个切片进行排序。我知道这有点多此一举,我们本可以直接排序整个切片就完事了。

我完全理解这个算法有点烦人,但正如我所说,让我们先把练习的目的放在一边,试着理解为什么我在这个append操作上遇到了问题。

我还在做一些测试,如果发现什么会及时告知。

func main() {
    fmt.Println("hello world")
}

您可以在代码中为切片设置值,以便在测试时无需每次都输入它们:

	// 定义要排序的数字大小
	size := 5

	// 将数字列表放入切片中
	list := []int{-9, -11, 12, 13, 9}

我没有在您的代码中发现任何错误,尽管这部分代码看起来没有意义:

	tmp = append(list[cut:])
	s = append(list[:cut])

也许您可以在 Stack Overflow 上提问。

由于我硬编码了输入,我在Go playground上尝试了代码,并且总是得到正确的结果,但在GoLand中,我有时仍然会得到错误的结果。

GreyShatter: 你可以在代码中为切片设置值,而不是每次都输入它们进行测试:

你说得完全正确,我会修改那部分。

GreyShatter: 也许你可以在SO上提问

SO是什么?

GreyShatter: 我没看出你的代码有什么问题,尽管这部分没有任何意义:

tmp = append(list[cut:])
s = append(list[:cut])

这部分是为了将切片分割成4个切片。

好的,没问题。以下是转换后的Markdown文档:

好的,好消息,Stack Overflow上有人找到了问题所在。

现在我100%确定,问题出在我为切片分配的长度/容量上,这导致了append操作出错。 是的,问题发生在最后一次append时。 关键信息在这里

如果切片s的容量不足以容纳新增的值,append会分配一个新的、足够大的底层数组来存放现有的切片元素和新增的值。否则,append会复用原有的底层数组。

切片和底层数组的工作原理在我脑海中仍然有些模糊,如果能想到一些练习或网站能让这个概念变得完全清晰,我会非常乐意去学习。

GreyShatter:

behaviour

好问题,结果不同是因为 Go 协程的竞态条件,我有 4 个协程,每次都是其中一个先进行追加操作。

我刚刚找到一篇关于这个问题的好文章 🙂

Medium – 12 Jun 18

Go 的 append 操作并非总是线程安全的

问题示例

阅读时间:3 分钟

我想补充一些相关信息,以防万一。 问题确实出在切片的分配上。我原本以为切片的工作原理与数组完全相同,但我忘记了切片更像是数组的一个“窗口”。 切片在同一个数组中拥有更大的容量,而我的 Go 协程每次运行代码时都会以不同的顺序向该数组追加数据。这就是为什么我得到了不同的结果。

以下是正确的代码,我在结果末尾添加了长度和容量,以便您可以看到它们已被正确分配。

请注意,这段代码并非最优,我们完全可以做得更好。

package main

import (
	"fmt"
	"sort"
	"strconv"
)

func send(list []int, c chan []int) {
	sort.Ints(list)
	c <- list
}

// 分割整数。
func split(list []int) (tmpS []int, splS []int) {
	halfSize := len(list)/2
	tmpSize := len(list) - halfSize
	tmpS = make([]int, tmpSize)
	splS = make([]int, halfSize)
	copy(tmpS, list[halfSize:])
	copy(splS, list[:halfSize])
	fmt.Println(tmpS, splS)
	return tmpS, splS
}

func convert(sStr []string) (sInt []int) {
	sInt = make([]int, 0, cap(sStr))
	for _, r := range sStr {
		digit, _ := strconv.Atoi(r)
		sInt = append(sInt, digit)
	}
	fmt.Println("Sorted sub array: ", sInt)
	return sInt
}

func main() {
	list := []string{"-9", "-11", "12", "13", "9"}
	fmt.Println("Your unsorted digits: ", list)

	// 转换整个切片
	sInt := convert(list)

	splS1, splS2 := split(sInt)
	splS11, splS12 := split(splS1)
	splS21, splS22 := split(splS2)

	// 在不同的 Go 协程中发送。
	c := make(chan []int)
	go send(splS11, c)
	go send(splS12, c)
	go send(splS21, c)
	go send(splS22, c)

	// 从通道接收。
	sortedS1 := <-c
	sortedS2 := <-c
	sortedS3 := <-c
	sortedS4 := <-c

	fmt.Printf("sortedS1 - Length: %d Capacity: %d\n", len(sortedS1), cap(sortedS1))
	fmt.Printf("sortedS2 - Length: %d Capacity: %d\n", len(sortedS2), cap(sortedS2))
	fmt.Printf("sortedS3 - Length: %d Capacity: %d\n", len(sortedS3), cap(sortedS3))
	fmt.Printf("sortedS4 - Length: %d Capacity: %d\n", len(sortedS4), cap(sortedS4))

	// 合并 4 个已排序的切片。
	sortedList1 := append(sortedS1, sortedS2...)
	sortedList2 := append(sortedS3, sortedS4...)
	finalList := append(sortedList1, sortedList2...)

	// 再次排序
	sort.Ints(finalList)
	fmt.Println("Here your digits sorted: ", finalList)
}

感谢大家所做的一切。

你的代码存在数据竞争问题。divideList 函数中的 append 使用方式不正确,导致切片共享底层数组,当多个 goroutine 同时修改时会产生不可预测的结果。

主要问题在于:

  1. divideList 函数没有创建新的底层数组
  2. 切片 s1, s2, s3, s4 可能共享内存
  3. 并发排序时数据相互覆盖

这是修复后的代码:

package main

import (
	"fmt"
	"sort"
)

// 创建切片的副本,避免共享底层数组
func divideList(list []int, cut int) ([]int, []int) {
	tmp := make([]int, len(list)-cut)
	copy(tmp, list[cut:])
	s := make([]int, cut)
	copy(s, list[:cut])
	return tmp, s
}

func sortList(list []int, c chan []int) {
	// 创建副本进行排序
	sorted := make([]int, len(list))
	copy(sorted, list)
	sort.Ints(sorted)
	c <- sorted
}

func main() {
	size := 5
	list := []int{-9, -11, 12, 13, 9}
	fmt.Println("Your digits input : ", list)

	// 创建独立的切片
	cut := size / 4
	remaining, s1 := divideList(list, cut)
	
	cut = len(remaining) / 3
	remaining, s2 := divideList(remaining, cut)
	
	cut = len(remaining) / 2
	remaining, s3 := divideList(remaining, cut)
	
	// s4 需要创建副本
	s4 := make([]int, len(remaining))
	copy(s4, remaining)

	// 在不同的Go协程中排序
	c := make(chan []int, 4)
	go sortList(s1, c)
	go sortList(s2, c)
	go sortList(s3, c)
	go sortList(s4, c)

	sortedS1 := <-c
	sortedS2 := <-c
	sortedS3 := <-c
	sortedS4 := <-c

	fmt.Println("Sorted slices:")
	fmt.Println(sortedS1)
	fmt.Println(sortedS2)
	fmt.Println(sortedS3)
	fmt.Println(sortedS4)
	fmt.Println("-----------------------")

	// 合并4个已排序的切片
	sortedList1 := append(sortedS1, sortedS2...)
	fmt.Println("First merge:", sortedList1)
	sortedList2 := append(sortedS3, sortedS4...)
	fmt.Println("Second merge:", sortedList2)
	finalList := append(sortedList1, sortedList2...)
	fmt.Println("Final merge:", finalList)

	// 最终排序
	sort.Ints(finalList)
	fmt.Println("Here your digits sorted: ", finalList)
}

更简洁的并发排序实现:

package main

import (
	"fmt"
	"sort"
	"sync"
)

func concurrentSort(numbers []int) []int {
	if len(numbers) <= 1 {
		return numbers
	}

	// 将切片分成4部分
	partSize := len(numbers) / 4
	var wg sync.WaitGroup
	results := make([][]int, 4)
	
	for i := 0; i < 4; i++ {
		wg.Add(1)
		go func(idx int) {
			defer wg.Done()
			start := idx * partSize
			end := start + partSize
			if idx == 3 {
				end = len(numbers)
			}
			
			// 创建副本进行排序
			part := make([]int, end-start)
			copy(part, numbers[start:end])
			sort.Ints(part)
			results[idx] = part
		}(i)
	}
	
	wg.Wait()
	
	// 合并结果
	var merged []int
	for _, part := range results {
		merged = append(merged, part...)
	}
	
	sort.Ints(merged)
	return merged
}

func main() {
	numbers := []int{-9, -11, 12, 13, 9, 5, -3, 0, 7, 8}
	fmt.Println("Original:", numbers)
	
	sorted := concurrentSort(numbers)
	fmt.Println("Sorted:", sorted)
}

关键点:

  1. Go 切片是引用类型,共享底层数组
  2. 并发操作时必须创建数据副本
  3. append 在不扩容时会修改原切片
  4. 使用 makecopy 创建独立的数据副本
回到顶部