第二节课之Go语言并发
-
并发与并行 并发指的是多线程程序在一个核的CPU上交替运行,而并行则指多线程程序在多个核的CPU上分别运行,其目的都是为了表现得像同时运行程序的不同线程。
-
协程与线程 在Go语言开发中,主要使用协程Goroutine来实现并发。其中,协程是一种用户态的轻量级线程,调度完全由用户控制,可以无需加锁访问,因而上下文切换更快。 正如一个线程可以有多个协程,一个进程也可以单独拥有多个协程;线程进程都是同步机制,而协程则是异步机制。协程能保留上一次调用的状态,每次重入时,就重新进入上一次调用的状态,以保证上下文切换的前后一致。
-
通道 Go语言的并发,通过通信共享内存而非通过共享内存实现通信。在Go语言中,即利用channel通道,实现在两个Goroutine之间传递一个指定类型的值来同步运行和通讯。 默认情况下,通道是不带缓冲区的。发送端发送数据,同时必须有接收端相应的接收数据,此时发送方会被阻塞直到某个接收方获取到一个值。而在创建通道时,可以增加一个参数指定缓冲区大小,对于带缓冲区的通道,发送方则会阻塞直到发送的值被拷贝到缓冲区内;若缓冲区已满,则发送方需等待有接收方获取到缓冲区内的值释放空间。
-
共享锁与
sync包 在并发实现中,也有一种锁机制,即在访问某一数据时,不能同时有多个线程访问,否则会造成数据更改丢失或数据不一致的问题,这就需要在访问数据时加锁以免被额外访问。sync.Mutex实现了Lock()与Unlock()方法,保证数据同时只能有一个锁存在。sync.RWMutex则是一个读写互斥锁,除了Lock()与Unlock()方法,还具备RLock()与RUnlock()方法,可以实现并发读取,但一般仅在频繁读取和不频繁写入的场景里运用。sync.WaitGroup则用于同步,可以让程序等待一组协程全部执行完成后再执行后续代码,避免协程执行完成的先后顺序影响程序运行效果。 -
示例代码1 此代码为协程的使用示例,此函数会分别创建两个通道。之后再运行两个协程,一个负责向
src通道里传递数据,另一个则负责从src数据里遍历取出数据并向dest通道传递。最后一层循环是遍历dest通道的数据将其打印。func calSquare() { src := make(chan int) dest := make(chan int, 3) go func() { defer close(src) for i := 0; i < 10; i++ { src <- 1 } }() go func() { defer close(dest) for i := range src { dest <- i * i } }() for i := range dest { println(i) } } -
示例代码2 本示例代码演示了锁的运用。在不启用锁时,因为有2000个协程都要对数据执行加1操作,极可能存在并发操作的时刻,最后的结果会小于2000不符预期。而有了加锁放锁操作的保障,最后的结果则是稳定在2000,符合预期。
var ( x int64 lock sync.Mutex ) func addWithLock() { for i := 0; i < 2000; i++ { lock.Lock() x += 1 lock.Unlock() } } func addWithoutLock() { for i := 0; i < 2000; i++ { x += 1 } } func Add() { x = 0 for i := 0; i < 5; i++ { go addWithoutLock() } time.Sleep(time.Second) println("WithoutLock:", x) x = 0 for i := 0; i < 5; i++ { go addWithLock() } time.Sleep(time.Second) println("WithLock", x) }