题记
这是我参与「第五届青训营 」伴学笔记创作活动的第 2天,本文用于记录在青训营的学习笔记和一些心得。
day2 1月16日
协程:用户态,轻量级线程,栈MB级别。 线程:内核态,线程跑多个协程,栈KB级别。
goroutine
函数hello就不用多说了,fmt.Sprint(args…) 返回 args 参数组成的字符串
ManyGO函数,先创建一个sync.WaitGroup类型的变量,那么这个到底是什么呢? 官方文档对sync.WatiGroup的描述是:一个waitGroup对象可以等待一组协程结束,也就等待一组goroutine返回。有了sync.Waitgroup我们可以将原本顺序执行的代码在多个Goroutine中并发执行,加快程序处理的速度。
sync.WaitGroup提供了Add()方法增加一个计数器,Done()方法减掉一个计数器。
sync.Wait()方法会阻塞主Goroutine直到WaitGroup计数器变为0。
该函数执行循环,每一个循环计数器+1,然后创建一个goroutine,调用hello函数,然后goroutine结束前,计数器-1。
channel
协程之间的通信,go是倡通过通信共享内存而不是通过共享内存而实现通信
channel的语法:make(chan 类型,[缓冲大小])
channel分为无缓冲和有缓冲两种,下面是对这两种的解释:
- 无缓冲的通道(unbuffered channel)是指在接收前没有能力保存任何值的通道。 这种类型的通道要求发送 goroutine 和接收 goroutine 同时准备好,才能完成发送和接收操作。如果两个 goroutine 没有同时准备好,通道会导致先执行发送或接收操作的 goroutine 阻塞等待。这种对通道进行发送和接收的交互行为本身就是同步的。其中任意一个操作都无法离开另一个操作单独存在。
- 有缓冲的通道(buffered channel)是一种在被接收前能存储一个或者多个值的通道。 这种类型的通道并不强制要求 goroutine 之间必须同时完成发送和接收。通道会阻塞发送和接收动作的条件也会不同。只有在通道中没有要接收的值时,接收动作才会阻塞。只有在通道没有可用缓冲区容纳被发送的值时,发送动作才会阻塞。 这导致有缓冲的通道和无缓冲的通道之间的一个很大的不同:无缓冲的通道保证进行发送和接收的 goroutine 会在同一时间进行数据交换;有缓冲的通道没有这种保证。
两种的语法的格式:
- 无缓冲的 channel 创建格式:
make(chan Type)//等价于make(chan Type, 0) - 有缓冲的 channel 创建格式:
make(chan Type, capacity)
package main
import (
"fmt"
"time"
)
func main() {
start := time.Now()
src := make(chan int)
dest := make(chan int) // dest := make(chan int,3)
go func() {
defer close(src)
for i := 0; i < 10; i++ {
src <- i
}
}()
go func() {
defer close(dest)
for i := range src {
dest <- i * i
}
}()
for i := range dest {
println(i)
}
fmt.Println("time duration:", time.Now().Sub(start))
}
close(channel):关闭channel通道
channel<- 数据 :将数据传入channel
细心的人可能会发现我的程序进行了计时,以下是结果。
dest := make(chan int):
dest := make(chan int,3):
如果我们在扩大缓冲区又会怎么样呢?思考一下。但缓冲区不是越大越好,你要明白合适的缓冲区大小确实会使得程序执行得更快。
Lock
互斥锁sync.Mutex
并发访问中比如多个goroutine并发更新同一个资源,比如计时器、账户余额、秒杀系统、向同一个缓存中并发写入数据等等。如果没有互斥控制,很容易会出现异常,比如计时器计数不准确、用户账户可能出现透支、秒杀系统出现超卖、缓存出现数据缓存等等,后果会很严重。
互斥锁是并发控制的一种基本手段,是为了避免竞争而建立的一种并发控制机制。学习前首先需要弄清楚一个概念-临界区。在并发编程中,如果程序中的一部分会被并发访问或修改,为了避免并发访问导致的意想不到的结果,这部分程序需要被保护起来,这部分被保护起来的程序就叫做临界区。
临界区是一个被共享的资源,或者说是一个整体的共享资源,比如对数据库的访问,对某个共享数据结构的操作。对一个I/O设备的使用,对一个连接池中的连接的调用等等。
如果很多线程同步访问临界区就会造成访问或操作错误,这并不是我们希望看到的结果。所以,使用互斥锁,限定临界区只能同时由一个线程持有。当临界区由一个线程持有的时候,其他线程如果想进入临界区就会返回失败,或者是等待。直到持有的线程退出临界区,这些等待线程中的某一个才有机会接着持有这个临界区。
Go标准库提供了sync.Mutex互斥锁类型以及两个方法分别是Lock加锁和Unlock释放锁。可以通过在代码前调用Lock方法,在代码后调用Unlock方法来保证一段代码的互斥执行,也可以使用defer语句来保证互斥锁一定会被解锁。当一个goroutine调用Lock方法获得锁后,其它请求的goroutine都会阻塞在Lock方法直到锁被释放。
一个互斥锁只能同时被一个goroutine锁定,其它goroutine将阻塞直到互斥锁被解锁,也就是重新争抢对互斥锁的锁定。需要注意的是,对一个未锁定的互斥锁解锁时将会产生运行时错误。
sync.Mutex不区分读写锁,只有Lock()和Lock()之间才会导致阻塞的情况。若在一个地方Lock(),在另一个地方不Lock()而是直接修改或访问共享数据,对于sync.Mutext类型是允许的,因为mutex不会和goroutine进行关联。若要区分读锁和写锁,可使用sync.RWMutex类型。
在Lock()和Unlock()之间的代码段成为资源临界区(critical section),在这一区间内的代码是严格被Lock()保护的,是线程安全的,任何一个时间点都只能有一个goroutine执行这段区间的代码。
互斥即不能同时运行,使用互斥锁的两个代码片段相互排斥,只有其中一个代码片段执行完毕后,另一个才能执行。