1 MiB 是 默认,正如您正确指出的那样。您可以轻松选择自己的堆栈大小(但是,最小值仍远高于 ~8 kiB)。
也就是说,goroutines 不是线程。它们只是具有协作式多任务处理的任务,类似于 Python 的。 goroutine 本身只是做你想做的事情所需的代码和数据;还有一个单独的调度程序(在多个操作系统线程上运行),它实际执行该代码。
在伪代码中:
loop forever
take job from queue
execute job
end loop
当然,execute job 部分可以很简单,也可以很复杂。您可以做的最简单的事情就是执行给定的委托(如果您的语言支持类似的东西)。实际上,这只是一个方法调用。例如,在更复杂的场景中,还可能存在诸如恢复某种上下文、处理延续和协作任务产出之类的东西。
这是一种非常轻量级的方法,在进行异步编程时非常有用(这几乎是当今的一切 :))。许多语言现在都支持类似的东西——Python 是我第一个看到这个(“tasklets”)的语言,很久以前。当然,在没有抢占式多线程的环境中,这几乎是默认设置。
例如,在 C# 中,有 Tasks。它们与 goroutine 并不完全相同,但在实践中,它们非常接近 - 主要区别在于 Tasks 使用线程池中的线程(通常),而不是单独的专用“调度程序”线程。这意味着如果您启动 1000 个任务,它们可能由 1000 个单独的线程运行;在实践中,这将需要您编写非常糟糕的Task 代码(例如,仅使用阻塞 I/O、休眠线程、等待等待句柄等)。如果您将Tasks 用于异步非阻塞 I/O 和 CPU 工作,它们在实际实践中非常接近 goroutines。理论是有点不同:)
编辑:
为了消除一些混淆,下面是典型的 C# 异步方法的样子:
async Task<string> GetData()
{
var html = await HttpClient.GetAsync("http://www.google.com");
var parsedStructure = Parse(html);
var dbData = await DataLayer.GetSomeStuffAsync(parsedStructure.ElementId);
return dbData.First().Description;
}
从GetData 方法的角度来看,整个处理是同步的——就好像你根本没有使用异步方法一样。关键的区别是你在“等待”时没有用完线程;但忽略这一点,它几乎与编写同步阻塞代码完全相同。当然,这也适用于共享状态的任何问题 - await 中的多线程问题和阻塞多线程 I/O 之间没有太大区别。 Tasks 更容易避免,但这只是因为你拥有的工具,而不是因为 Tasks 所做的任何“魔法”。
在这方面与 goroutine 的主要区别在于 Go 并没有通常意义上的阻塞方法。他们没有阻塞,而是将其特定的异步请求排队,然后屈服。当操作系统(以及 Go 中的任何其他层——我对内部工作没有深入了解)收到响应时,它会将其发布到 goroutine 调度程序,而后者又知道“等待”响应的 goroutine 是现在准备恢复执行;当它实际获得一个插槽时,它将从“阻塞”调用继续,就好像它真的被阻塞了一样——但实际上,它与 C# 的await 所做的非常相似。没有根本的区别 - C# 的方法和 Go 的方法之间有很多区别,但它们并没有那么巨大。
还要注意,这与旧 Windows 系统上使用的方法基本相同,没有抢先式多任务处理 - 任何“阻塞”方法都会简单地将线程的执行返回给调度程序。当然,在那些系统上,你只有一个 CPU 核心,所以你不能一次执行多个线程,但原理还是一样的。