解决并行调度问题的几个思路

本文转载自微信公众号:labuladong

举个具体的例子,最近再往 B 站上搬运 YouTube 的优质算法视频,但是手动下载太麻烦,干脆直接写个并发程序给我打工算了。以下是非并发版本的 main 函数中的主要部分:

func main() {
    urls := exec.GetUrls()
    for i := 0; i < len(urls); i++ {
        // 下载操作,较耗时
        exec.DownloadVideos(urls[i])
    }
}

这段代码应该很好理解,一个一个下载。但是我想最大化利用网速,改成并发。你可能想这样写:

func main() {
    urls := exec.GetUrls()
    for i := 0; i < len(urls); i++ {
        go exec.DownloadVideos(urls[i])
    }
}

不行,回想刚才 sleep 和 study 的例子,程序会瞬间结束,go 出来的分身(goroutine)什么都来不及做。对于这种场景,教给你三种方法进行并发完成任务。

第一种方法:

func main() {
    // 这是什么类型的 channel 其实并不重要
    // 只是想利用 channel 来进行阻塞等待
    done := make(chan bool)
    urls := exec.GetUrls()
    for i := 0; i < len(urls); i++ {
        go func () {
            exec.DownloadVideos(urls[i])
            done <- true
        }()
    }
    // main 在这里阻塞,等着他们做完
    for i := 0; i < len(urls); i++ {
        <-done
    }
}

想象这个场景帮助理解:main 函数扫了一眼任务列表,给每个任务都召唤一个分身去做,然后自己坐在指挥部等着分身们传回捷报;大家都做完了,main 函数就去交差。

但是这有个问题,当任务量很大(urls 中元素很多)时,main 函数可能要召唤上千个分身,可能会过度并行,反而会极大降低效率。因为计算机的资源毕竟是有限的,比如 CPU 个数限制计算负载,硬盘决定你的 IO 操作频率,网络状况决定网速上限等等。所以过度并行是需要避免的,最好限制 goroutine 的数量,不要让 main 函数无限制地召唤分身。

第二种方法:

func main() {
    // 这是什么类型的 channel 仍然不重要
    // 新建一个缓存为 10 的 channel
    tokens := make(chan bool, 10)
    // 用来和 main 通信,报告完成任务
    done := make(chan bool)
    urls := exec.GetUrls()
    for i := 0; i < len(urls); i++ {
        // 如果缓存满,就会在这阻塞
        tokens <- true
        go func () {
            exec.DownloadVideos(urls[i])
            // 完成任务,释放缓存
            <-tokens
            // 通知 main 函数
            done <- true
        }()
    }
    // main 在这里等着大家执行完
    for i := 0; i < len(urls); i++ {
        <-done
    }
}

这里新建了一个带 10 个缓冲容量的 channel。就是说往这个 channel 里发送消息时,如果还有缓冲位,即便没有人正在另一头接收消息,也可以发送,暂存在缓冲中,除非缓冲也被占满,才会发生阻塞;接收同理,如果缓存非空,可以从缓存里取消息,如果缓存也是空的,才会发生阻塞。类似排队。

可以想象这个场景:工地里有很多任务要做,但是只有 10 个柜子可以存放个人物品,每个分身去干活前都得存放物品,干完活后再把自己的东西取走。只有存在空柜子的时候,才能有新的分身进入干活。这就保证了同时最多有 10 个分身在工作。

方法三:

func main() {
    // 通过这个 channel 分发任务
    tasks := make(chan string)
    // 通过这个 channel 汇报完成任务
    done := make(chan bool)
    urls := exec.GetUrls()
    // 造 10 个长活的 goroutine
	for i := 0; i < 10; i++ {
		go func() {
            // 阻塞在这里等着接收
			for url := range tasks {
				exec.DownloadVideos(url)
			}
		}()
	}
    // 再派遣一个分身专门发任务,等着人接收
    go func() {
        for i := 0; i < len(urls); i++ {
            tasks <- urls[i]
            done <- true
        }
    }()
    // main 在这等着大家把活干完
    for i := 0; i < len(urls); i++ {
        <-done
    }
}

这里用到了 range 关键字,相当于不断地企图从 channel 里抢任务,抢到了就去做,做完了回来继续抢;如果没有抢到任务就阻塞在那等着下一波机会。

这个方法通过创建 10 个长活的 goroutine,实现了对并发的限制,即同时最多有 10 个分身在干活。

可以类比这个场景:main 函数召唤了 10 个分身,把大家拉到一个微信群里,然后 main 函数又召唤了一个分身来发红包,其他分身们时刻准备着抢红包。抢到红包的分身去干活,干完活回来继续参与抢红包。另外,只有当上一个红包被抢走才会发下一个红包。

解决并行调度问题的几个思路_第1张图片

你可能感兴趣的:(Go,语言)