3.8 KiB
14.1 并发,并行和协程
14.1.1 什么是协程
一个应用程序是运行在机器上的一个进程;进程是一个运行在自己内存地址空间里的独立执行体。一个进程由一个或多个操作系统线程组成,这些线程其实是共享同一个内存地址空间的一起工作的执行体。几乎所有'正式'的程序都是多线程的,以便让用户或计算机不必等待,或者能够同时服务多个请求(如Web服务器),或增加性能和吞吐量(例如,通过对不同的数据集并行执行代码)。一个并发程序可以在一个处理器或者内核上使用多个线程来执行任务,但是只有在同一个程序在某一个时间点在多个些处理内核或处理器上同时执行的任务才是真正的并行。
并行是一种通过使用多处理器以提高速度的能力。所以并发程序可以是并行的,也可以不是。
公认的,使用多线程的应用难以做到准确,最主要的问题是内存中的数据共享,它们会被多线程以无法预知的方式进行操作,导致一些无法重现或者随机的结果(称作竞态
)
!!不要使用全局变量或者共享内存,它们会给你的代码在并发运算的时候带来危险。
解决之道在于同步不同的线程,对数据加锁,这样同时就只有一个线程可以变更数据。在Go的标准库sync
中有一些工具用来在低级别的代码中实现加锁;我们在章节9.3中讨论过这个问题。不过过去的软件开发经验告诉我们这会带来更高的复杂度,更容易使代码出错以及更低的性能,所以这个经典的方法明显不再适合现代多核/多处理器编程:"thread-per-connection
"模型不够有效。
Go更倾向于其他的方式,在诸多比较合适的范式中,有个被称作Communicating Sequential Processes(顺序通信处理)
(CSP, C. Hoare发明的)还有一个叫做message passing-model(消息传递)
(已经运用在了其他语言中,比如Eralng)。
在Go中,应用程序并发处理的部分被称作goroutines(协程)
,它可以进行更有效的并发运算。在协程和操作系统线程之间并无一对一的关系:协程是根据一个或多个线程的可用性,映射(多路复用,执行于)在他们之上的;协程调度器在Go运行时很好的完成了这个工作。
协程工作在相同的地址空间中,所以共享内存的方式一定是同步的;这个可以使用sync
包来实现(参见章节9.3),不过我们很不鼓励这样做:Go使用channels
来同步协程(可以参见14.2等章节)
当系统调用(比如等待I/O)阻塞协程时,其他协程会继续在其他线程上工作。协程的设计隐藏了许多线程创建和管理方面的复杂工作。
协程是轻量的,比线程更轻。它们痕迹非常不明显(使用少量的内存和资源):在堆中使用4K的堆内存就可以创建它们。因为创建非常廉价,必要的时候可以轻松创建并运行大量的协程(在同一个一个地址空间中100,000个连续的协程)。并且它们对堆进行了分割,从而动态的增加(或缩减)内存的使用;堆的管理是自动的,但不是由垃圾回收器管理的,而是在协程退出后自动释放。
协程可以运行在多个操作系统线程之间,也可以运行在线程之内,让你可以很小的内存占用就可以处理大量的任务。由于操作系统线程上的协程时间片,你可以使用少量的操作系统线程就能拥有任意多个提供服务的协程,而且Go运行时可以聪明的意识到哪些协程被阻塞了,暂时搁置它们并处理其他携程。
链接
- 目录
- 上一节:性能调试:分析并优化 Go 程序
- 下一节:并发,并行和协程