Golang中goroutines使用遇到的问题

Golang中goroutines使用遇到的问题 大家好, 我是新手,仍在学习Go语言。 我有一个用于下载多个git仓库的脚本。为了加速这个过程,我使用了goroutine。我创建了另一个简短的脚本来模拟我的问题。

以下脚本在等待每个goroutine中的随机时间的同时创建多个文件夹和子文件夹。

package main

import (
	"fmt"
	"math/rand"
	"os"
	"strconv"
	"sync"
	"time"
)

const (
	xthreads    = 5  // 要使用的线程总数,不包括 main() 线程
	folderCount = 50 // 需要创建的最大文件夹数
)

func doSomething1(a int) {
	fmt.Println("doSomething1:", a)

	if err := os.Mkdir("testDir"+strconv.Itoa(a), 0755); err != nil {
		fmt.Println(err)
	}
}

func doSomething2(a int) {
	fmt.Println("doSomething2:", a)

	if err := os.Chdir("testDir" + strconv.Itoa(a)); err != nil {
		fmt.Println(err)
	}

	if err := os.Mkdir("anotherDir", 0755); err != nil {
		fmt.Println(err)
	}

	if err := os.Chdir(".."); err != nil {
		fmt.Println(err)
	}
}

func main() {
	var ch = make(chan int)
	var wg sync.WaitGroup
	var dir string
	var err error

	if dir, err = os.MkdirTemp(os.TempDir(), "test-"); err != nil {
		fmt.Println(err)
	}
	if err = os.Chdir(dir); err != nil {
		fmt.Println(err)
	}

	// 这启动了 xthreads 个 goroutine,等待执行任务
	wg.Add(xthreads)
	for i := 0; i < xthreads; i++ {
		go func() {
			for {
				a, ok := <-ch
				if !ok { // 如果无事可做且通道已关闭,则结束 goroutine
					wg.Done()
					return
				}

				doSomething1(a)
				doSomething2(a)
				rand.Seed(time.Now().UnixNano())
				randomSleep := time.Duration(rand.Intn(10))
				time.Sleep(randomSleep * time.Second)
				fmt.Printf("Sleep for %d second...\n", randomSleep)
			}
		}()
	}

	// 现在可以将任务添加到通道中,该通道用作队列
	for i := 0; i < folderCount; i++ {
		ch <- i // 将 i 添加到队列
	}

	close(ch) // 这告诉 goroutines 没有其他事情可做
	wg.Wait() // 等待线程完成
}

我想要实现的目标是:

  1. 例如,同一时间只应执行5个goroutine xthreads
  2. 当其中一个完成后,另一个必须启动,这样我就可以一直有5个goroutine同时运行,直到达到 folderCount
  3. 文件夹结构应如下所示:
/tmp/test-2325124430
├── testDir0
│   └── anotherDir
├── testDir1
│   └── anotherDir
├── testDir2
│   └── anotherDir
├── testDir3
│   └── anotherDir
└── testDir4
    └── anotherDir
...................
...................
└── testDir50
    └── anotherDir

等等…

但问题是,当我启动脚本时,它创建的文件夹如下:

/tmp/test-2325124430
├── anotherDir
├── testDir0
│   └── anotherDir
├── testDir1
├── testDir2
│   └── anotherDir
├── testDir3
│   └── anotherDir
└── testDir4
    └── anotherDir
...................
/tmp
├── testDir16
├── anotherDir
│   └── testDir17
...................
/
├── testDir18
├── testDir19
│   └── testDir20
...................

控制台输出是:

go run -race test.go
doSomething1: 3
doSomething1: 1
doSomething2: 3
doSomething2: 1
doSomething1: 4
doSomething1: 0
doSomething2: 0
doSomething1: 2
doSomething2: 2
doSomething2: 4
chdir testDir1: no such file or directory
Sleep for 1 second...
doSomething1: 5
doSomething2: 5
Sleep for 0 second...
doSomething1: 6
doSomething2: 6
Sleep for 4 second...
doSomething1: 7
doSomething2: 7
Sleep for 5 second...
doSomething1: 8
doSomething2: 8
Sleep for 7 second...
doSomething1: 9
doSomething2: 9
Sleep for 3 second...
doSomething1: 10
doSomething2: 10
Sleep for 1 second...
doSomething1: 11
doSomething2: 11
Sleep for 9 second...
doSomething1: 12
doSomething2: 12
Sleep for 9 second...
doSomething1: 13
doSomething2: 13
Sleep for 9 second...
doSomething1: 14
doSomething2: 14
Sleep for 7 second...
doSomething1: 15
doSomething2: 15
Sleep for 7 second...
doSomething1: 16
Sleep for 6 second...
doSomething2: 16
doSomething1: 17
doSomething2: 17
chdir testDir17: no such file or directory
Sleep for 8 second...
doSomething1: 18
doSomething2: 18
Sleep for 6 second...
doSomething1: 19
doSomething2: 19
Sleep for 6 second...
Sleep for 7 second...
Sleep for 6 second...
Sleep for 7 second...
Sleep for 8 second...

更多关于Golang中goroutines使用遇到的问题的实战教程也可以访问 https://www.itying.com/category-94-b0.html

3 回复

是的,我确信如果我将 os.Chdir 改为某种能跟踪整个路径的方法,就一定能正常工作。我之前不知道 os.Chdir 有这种行为,文档里没有提到。

谢谢

更多关于Golang中goroutines使用遇到的问题的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


os.Chdir 会改变整个进程的工作目录;它并非针对特定的 goroutine(或“线程”)。

与其在你的 goroutine 中改变目录,不如记录下你希望每个 goroutine 使用的路径,并直接使用这些路径;不要改变目录。

你的代码存在两个主要问题:目录切换的竞态条件和随机数种子重复初始化。以下是修正后的版本:

package main

import (
	"fmt"
	"math/rand"
	"os"
	"path/filepath"
	"strconv"
	"sync"
	"time"
)

const (
	xthreads    = 5  // 并发goroutine数量
	folderCount = 50 // 需要创建的文件夹总数
)

func doSomething1(baseDir string, a int) error {
	dirName := "testDir" + strconv.Itoa(a)
	dirPath := filepath.Join(baseDir, dirName)
	
	fmt.Println("doSomething1:", a)
	
	if err := os.Mkdir(dirPath, 0755); err != nil {
		return fmt.Errorf("创建目录 %s 失败: %v", dirPath, err)
	}
	return nil
}

func doSomething2(baseDir string, a int) error {
	dirName := "testDir" + strconv.Itoa(a)
	dirPath := filepath.Join(baseDir, dirName)
	
	fmt.Println("doSomething2:", a)
	
	anotherDir := filepath.Join(dirPath, "anotherDir")
	if err := os.Mkdir(anotherDir, 0755); err != nil {
		return fmt.Errorf("创建子目录 %s 失败: %v", anotherDir, err)
	}
	return nil
}

func worker(id int, ch <-chan int, wg *sync.WaitGroup, baseDir string) {
	defer wg.Done()
	
	// 每个worker有自己的随机数生成器
	r := rand.New(rand.NewSource(time.Now().UnixNano() + int64(id)))
	
	for a := range ch {
		// 执行任务
		if err := doSomething1(baseDir, a); err != nil {
			fmt.Println(err)
			continue
		}
		
		if err := doSomething2(baseDir, a); err != nil {
			fmt.Println(err)
			continue
		}
		
		// 随机休眠
		randomSleep := time.Duration(r.Intn(10))
		time.Sleep(randomSleep * time.Second)
		fmt.Printf("Worker %d: 任务 %d 休眠 %d 秒\n", id, a, randomSleep)
	}
}

func main() {
	// 创建临时目录
	baseDir, err := os.MkdirTemp(os.TempDir(), "test-")
	if err != nil {
		fmt.Println("创建临时目录失败:", err)
		return
	}
	defer os.RemoveAll(baseDir) // 清理临时目录
	
	fmt.Printf("工作目录: %s\n", baseDir)
	
	// 创建任务通道
	ch := make(chan int, folderCount)
	var wg sync.WaitGroup
	
	// 启动worker goroutines
	wg.Add(xthreads)
	for i := 0; i < xthreads; i++ {
		go worker(i, ch, &wg, baseDir)
	}
	
	// 发送任务到通道
	for i := 0; i < folderCount; i++ {
		ch <- i
	}
	
	// 关闭通道,通知worker没有更多任务
	close(ch)
	
	// 等待所有worker完成
	wg.Wait()
	
	fmt.Println("\n所有任务完成!")
	fmt.Println("生成的目录结构:")
	
	// 验证生成的目录结构
	for i := 0; i < folderCount; i++ {
		dirPath := filepath.Join(baseDir, "testDir"+strconv.Itoa(i))
		subDirPath := filepath.Join(dirPath, "anotherDir")
		
		if _, err := os.Stat(dirPath); os.IsNotExist(err) {
			fmt.Printf("  ❌ 目录不存在: %s\n", dirPath)
		} else if _, err := os.Stat(subDirPath); os.IsNotExist(err) {
			fmt.Printf("  ❌ 子目录不存在: %s\n", subDirPath)
		} else {
			fmt.Printf("  ✓ testDir%d/anotherDir\n", i)
		}
	}
}

关键修改:

  1. 移除全局目录切换:使用filepath.Join()构建完整路径,避免os.Chdir()导致的竞态条件
  2. 修复随机数生成:每个worker使用独立的随机数生成器,避免并发调用rand.Seed()的问题
  3. 改进错误处理:函数返回错误而不是直接打印
  4. 添加worker ID:便于调试和跟踪
  5. 添加结果验证:最后检查所有目录是否正确创建

运行示例输出:

工作目录: /tmp/test-1234567890
doSomething1: 0
doSomething2: 0
Worker 0: 任务 0 休眠 3 秒
doSomething1: 1
doSomething2: 1
Worker 1: 任务 1 休眠 7 秒
doSomething1: 2
doSomething2: 2
Worker 2: 任务 2 休眠 2 秒
...
所有任务完成!
生成的目录结构:
  ✓ testDir0/anotherDir
  ✓ testDir1/anotherDir
  ✓ testDir2/anotherDir
  ...
  ✓ testDir49/anotherDir

这个版本确保了:

  • 最多5个goroutine并发执行
  • 目录结构正确创建
  • 没有竞态条件
  • 正确的错误处理
回到顶部