Go并发编程 1| 青训营笔记

82 阅读5分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 2 天

进程/线程/协程

对操作系统来说,进程是资源分配的最小单位,程序启动时,操作系统就会给这个程序分配一块内存空间,对于程序本身而言它认为这是一整块连续的内存空间,称为虚拟内存空间,而实际上落实到操作系统内核时通常是一块块的内存碎片。一个进程大小可能是几个G,进程之间切换开销较大,进程可以实现操作系统的并发。这片虚拟内存空间,可以划分为内核空间和用户空间,它们相互隔离,程序即使崩溃了,内核空间也不会受到影响。进程运行在内核空间时称为内核态,运行在用户空间称之为用户态。内核空间用于执行内核代码,用户空间只用于执行用户程序,若要执行各种IO操作,就需要通过系统调用等方式从用户态切换到内核态进入内核空间进行操作。多进程并发有两个缺点:一是内核的管理成本高,二是无法简单通过内存同步数据,进程间通信较困难

对操作系统来说,线程是资源调度的最小单位,线程是进程的一个执行单元,一个进程至少需要包含一个线程(可以包含多个),只有拥有了线程的进程才会被CPU执行。一个线程大小约是几M。线程可以实现进程内部的并发。总结起来就是:进程主要面向内存的分配管理,线程主要面向CPU的调度。 一个进程下的多个线程是共享这个进程的内存空间的,即线程没有自己独立的内存空间。正因如此,所以一个线程可以读、写甚至清除另一个线程的堆栈。相当于线程之间是没有保护的。但每个线程都有自己的堆栈、程序计数器、寄存器等信息,这些都不是共享的。线程也被称为轻量级进程,与进程调度类似,CPU在线程之间快速切换,就有了线程并行运行的假象。线程间的切换开销要比进程间切换小的多,因为不需要切换页表,虚拟地址空间等等一些东西。 共享地址空间可以方便的共享对象,但也有一个问题,就是任何一个线程崩溃,进程中所有线程会一起崩溃。 同时,多线程虽然进一步提高了并发,但在当今互联网高并发场景下,为每个任务都创建一个线程甚至是创建上万个线程来工作是不现实的,因为会消耗大量的内存,而且多线程开发要考虑很多同步竞争等问题,如锁、竞争冲突等。此外,不管是进程还是线程,它们的切换都是由内核控制的,所以线程的切换涉及到用户空间和内核空间的切换(特权模式的切换),然后需要操作系统的调度模块完成线程调度。

协程它不像线程和进程那样需要进行系统内核上的上下文切换(协程切换不涉及特权模式的切换),协程切换只涉及基本的CPU上下文切换,完全在用户空间完成,做的事要比进程线程少,因此切换开销要更小。 协程的优点:一是可以提高CPU利用率,避免系统内核级的线程间频繁切换造成的资源浪费;二是可以节约内存,一个进程几G、一个线程几M、一个协程几KB;三是稳定性好一些,线程可以通过内存共享数据,但是一个线程挂了,进程中所有线程会一起崩溃。 协程缺点:协程本质是个单线程,它不能同时使用单个 CPU 的多个核;一旦协程出现阻塞,将会阻塞整个线程。

Goroutine

一个 goroutine 本质上是一个协程

在一个函数前面加上go关键字,就创建了一个goroutine实现协程 在golang中main函数启动时会开启一个goroutine,当main函数结束时,这个goroutine结束,同时用户自己新开的goroutine协程也会结束,所以通过time.sleep等方法不让main函数结束即可防止goroutine来不及执行就被停止。 相应代码如下:

func hello() {
    fmt.Println("New Goroutine!")
}
func main() {
    go hello()
    fmt.Println("main goroutine done!")
    time.Sleep(time.Second)  //等待1秒
}

当启动多个 goroutine 并同时操作同一个资源时会发生竞态问题(数据竞态)。

何为竞态:当多个线程竞争同一个资源时,如果对资源的访问顺序敏感,就称存在竞态条件。导致竞态条件发生的代码区称作临界区。数据竞态问题会导致计算结果不可预期。

如何解决:加锁。共享资源在同一时间只能由一个线程访问(加锁),其他线程想要访问必须等当前线程访问完释放锁。

总共有以下几个常用的锁:

互斥锁mutex

它能够保证同时只有一个 goroutine 可以访问共享资源。Golang 中可以使用 sync 包的 Mutex 来实现互斥锁

读写锁

不同于互斥锁的完全互斥,读写锁比较适用于读多写少的场景,当并发的去读取一个资源无需对资源修改时是没有必要加锁的,此时读写锁是更好的选择。