【问题标题】:Goroutines 8kb and windows OS thread 1 mbGoroutines 8kb 和 windows 操作系统线程 1 mb
【发布时间】:2015-07-16 09:29:17
【问题描述】:

作为 Windows 用户,我知道操作系统线程由于 By default, Windows allocates 1 MB of memory for each thread’s user-mode stack. 而消耗约 1 Mb 的内存,如果操作系统线程更加贪吃,golang 如何为每个 goroutine 使用约 8kb 的内存。 goroutine 是一种虚拟线程吗?

【问题讨论】:

    标签: windows multithreading go threadpool goroutine


    【解决方案1】:

    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 核心,所以你不能一次执行多个线程,但原理还是一样的。

    【讨论】:

    • 是的,它看起来像 C# Tasks。任务像消息一样排队,并由线程池中的线程执行。当 10 个任务太短时,可能只需要一个操作系统线程即可全部执行
    • 什么方法在实践中更快?多少钱?
    • @Ark 每个人都有自己的优点和缺点 - 但我还没有真正看到你会看到很大差异的案例。到目前为止,最大的区别是每个的语法和用法。我不确定,但我认为 goroutines 总是合作的,而 .NET 的任务是先发制人和合作的混合体。此外,.NET 任务并没有像 goroutine(或线程)那样真正拥有堆栈 - 它们实际上只是最终 return 的方法调用;它们在执行时只有一个堆栈(它是线程的堆栈)。
    • @Ark 由于 goroutines 总是分配至少 2 kiB 的堆栈,实际上Tasks 在某些情况下可能更轻。但这是分析的问题,而不是猜测。如果您对自己想要做的事情有所了解,那么编写一些可以直接进行性能测试并找出更适合您的原型应该很容易。
    • 我喜欢这个答案 - 当你开始将 goroutines 与 .NET Tasks 进行比较时,我有点担心,但你设法不将它们一对一比较,这让我很高兴 :)
    【解决方案2】:

    goroutine 就是我们所说的green threads。它们不是操作系统线程,由 go 调度程序负责。这就是为什么它们可以拥有更小的内存占用。

    【讨论】:

      【解决方案3】:

      Goroutines 不是线程,它们是(来自spec):

      ...一个独立的并发控制线程,或 goroutine,在同一地址空间内。

      Effective Go 将它们定义为:

      它们被称为 goroutines 是因为现有的术语——线程、协程、进程等——传达了不准确的含义。 goroutine 有一个简单的模型:它是一个在同一地址空间中与其他 goroutine 并发执行的函数。它是轻量级的,成本仅比堆栈空间的分配多一点。堆栈开始时很小,因此它们很便宜,并通过根据需要分配(和释放)堆存储来增长。

      Goroutines 没有自己的线程。相反,多个 goroutine 被(可能)多路复用到同一个 OS 线程上,因此如果一个应该阻塞(例如等待 I/O 或阻塞通道操作),其他的会继续运行。

      同时执行 goroutine 的实际线程数可以通过runtime.GOMAXPROCS() 函数设置。引用runtime 包文档:

      GOMAXPROCS 变量限制了可以同时执行用户级 Go 代码的操作系统线程数。代表 Go 代码在系统调用中可以阻塞的线程数没有限制;这些不计入 GOMAXPROCS 限制。

      请注意,在当前的实现中,默认情况下只有 1 个线程用于执行 goroutine。

      【讨论】:

        猜你喜欢
        • 2020-02-20
        • 1970-01-01
        • 2014-08-27
        • 2011-05-24
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2020-10-08
        • 2014-02-18
        相关资源
        最近更新 更多