放假第一天,也是最后一天组队学习。最后一天的任务难度还是挺大的,毕竟连官群大佬都发话了
话不多说,开始今天的总结任务。
今天学习的是Go语言并发编程,说到并发,我想这是Go语言最具优势的地方了,随便在网上搜索Go编程的优势,必有一点即Go是一种高效的语言,高度支持并发性,也可以说Go是为大数据、微服务、并发而生的一种编程语言。那么我们来仔细讨论下Go并发。
01
并发与并行
首先我们来搞清楚两个概念:并发和并行,在计算机操作系统课程中有讲过,并发(Concurrent)指的是两个或多个事件在同一时间间隔内发生,并行(Parallel)指的是两个或者多个事件在同一时刻发生。我们借用Erlang 之父 Joe Armstrong曾经解释并发与并行时用到的图示。
并发在图中的解释是两队人排队接咖啡,两队切换。
并行是两个咖啡机,两队人同时接咖啡。
02
一个现象
为什么操作系统经常可以运行多个程序但是用户却感觉不出来呢?这是因为无论是单CPU还是多CPU,我们的操作系统营造了一种可以同时运行多个程序的假象,实际上是通过操作系统对进程的调度以及CPU的快速上下文切换来实现的,这个过程即每个进程被执行一段时间后会被停下,然后CPU切换到下个被操作系统调度的进程上继续执行,由于切换的很快,所以用户就会误以为操作系统一直在运行服务着自己的程序。
通过这个现象我们再来解释下并发与并行的区别,这两者虽然都是说"多个进程同时执行",但是两者的"同时"却不是一个概念,并行的"同时"是同一时刻可以有多个进程在运行,并发的"同时"是指经过上下文切换,使得看上去多个进程同时都在运行的现象,是一种操作系统欺骗用户的现象。
03
并发的原因
原因有很多,其中比较重要的原因如下:
不阻塞等待其他任务的执行,从而浪费时间,影响系统性能。
并行可以使系统变得简单些,将复杂的大任务切换成许多小任务执行,单独测试。
在开发中,经常会遇到为什么某些进程通常会相互等待呢?为什么有些运行慢,有些快呢?
通常受限来源于进程I/O或CPU。
进程I/O限制
如:等待网络或磁盘访问
CPU限制
如:大量计算
04
协程goroutine
Go语言中协程的概念提出一定程度上是为了解决在像Java/C++这类编程语言在实现并发编程的时候,需要程序员自己去定义并维护上下文切换任务所耗费的时间和精力问题。Go语言提供这样的一个机制,程序员只需要去定义任务,而让系统去帮助我们把这些任务分配给CPU执行。goroutine类似于线程,Go程序会自动的将goroutine中的任务合理的分配给CPU。
我们来介绍一下如何使用goroutine。Go程序中使用go关键字为一个函数创建一个goroutine,一个函数可以被创建多个goroutine,其中一个goroutine必须对应一个函数。举个例子,先来写个日常的代码:
func hello() { fmt.Println("Hello Goroutine!")}func main() { hello() fmt.Println("main goroutine done!")}
上面这个程序是串行执行的(串行的概念很容易理解,大家自行学习),执行的结果是打印完成Hello Goroutine!后打印main goroutine done!
接下来我们在调用hello()函数前面加上关键字go,也就是启动一个goroutine执行hello()这个函数。
func main() { go hello() // 启动另外一个goroutine去执行hello函数 fmt.Println("main goroutine done!")}
这次执行结果只有main goroutine done!,并没有Hello Goroutine,这是什么原因呢?在程序启动时,Go程序就会为main()函数创建一个默认的goroutine。当main()函数返回的时候该goroutine就结束了,所有在main()函数中启动的goroutine会一同结束。我们如果要main()函数等一下hello()函数,最直接的方式就是使用time包里的Sleep方法。
func main() { go hello() // 启动另外一个goroutine去执行hello函数 fmt.Println("main goroutine done!") time.Sleep(time.Second)}
这样,在打印main goroutine后就紧接着打印输出Hello Goroutine!
但是!假设主线程要等待其余的goroutine都运行完毕,不得不在末尾添加time.Sleep(),但是这样会引发两个问题:
等待多长时间?
时间太长,影响性能?
在go的sync库中的WaitGroup可以帮助我们完成此项工作,Add(n)把计数器设置为n,Done()会将计数器每次减1,Wait()函数会阻塞代码运行,直到计数器减0。
// 这是我们将在每个goroutine中运行的函数。// 注意,等待组必须通过指针传递给函数。func worker(id int, wg *sync.WaitGroup) { defer wg.Done() fmt.Printf("Worker %d starting\n", id) time.Sleep(time.Second) fmt.Printf("Worker %d done\n", id)}func main() { var wg sync.WaitGroup for i := 1; i <= 5; i++ { wg.Add(1) go worker(i, &wg) } wg.Wait()}
需要注意sync.WaitGroup是一个结构体,传递的时候要传递指针。
这里首先把wg 计数设置为1, 每个for循环运行完毕都把计数器减一,主函数中使用Wait() 一直阻塞,直到wg为1——也就是所有的5个for循环都运行完毕。
使用注意点:
计数器不能为负值
WaitGroup对象不是引用类型
启动多个goroutiine
var wg sync.WaitGroupfunc hello(i int) { defer wg.Done() fmt.Println("Hello Goroutine!", i)}func main() { for i := 0; i < 10; i++ { wg.Add(1) go hello(i) } wg.Wait()}
多次执行上面的代码,会发现每次打印的数字的顺序都不一致。这是因为10个goroutine是并发执行的,而goroutine的调度是随机的。
05
互斥锁Mutex
单纯地将函数并发执行是没有意义的。函数与函数间需要交换数据才能体现并发执行函数的意义。为了保证数据交换的正确性,必须使用互斥量对内存进行加锁,这种做法势必造成性能问题。
互斥锁是并发程序对共享资源进行访问控制的主要手段,在go中的sync中提供了Mutex的支持。
// SafeCounter 的并发使用是安全的。type SafeCounter struct { v map[string]int mux sync.Mutex}// Inc 增加给定 key 的计数器的值。func (c *SafeCounter) Inc(key string) { c.mux.Lock() defer c.mux.Unlock() // Lock 之后同一时刻只有一个 goroutine 能访问 c.v c.v[key]++}// Value 返回给定 key 的计数器的当前值。func (c *SafeCounter) Value(key string) int { c.mux.Lock() // Lock 之后同一时刻只有一个 goroutine 能访问 c.v defer c.mux.Unlock() return c.v[key]}func main() { c := SafeCounter{v: make(map[string]int)} for i := 0; i < 1000; i++ { go c.Inc("somekey") } time.Sleep(time.Second) fmt.Println(c.Value("somekey"))}
在这个例子中,我们使用了sync.Mutex的Lock与Unlock方法。
在前面例子中我们使用了sync.Mutex,读操作与写操作都会被阻塞。其实读操作的时候我们是不需要进行阻塞的,因此sync中还有另一个锁:读写锁RWMutex,这是一个单写多读模型。
sync.RWMutex分为:读、写锁。在读锁占用下,会阻止写,但不会阻止读,多个goroutine可以同时获取读锁,调用RLock()函数即可,RUnlock()函数释放。写锁会阻止任何goroutine进来,整个锁被当前goroutine,此时等价于Mutex,写锁调用Lock启用,通过UnLock()释放。
例如:我们对上述例子进行改写,读的时候用读锁,写的时候用写锁。
// SafeCounter 的并发使用是安全的。type SafeCounter struct { v map[string]int rwmux sync.RWMutex}// Inc 增加给定 key 的计数器的值。func (c *SafeCounter) Inc(key string) { // 写操作使用写锁 c.rwmux.Lock() defer c.rwmux.Unlock() // Lock 之后同一时刻只有一个 goroutine 能访问 c.v c.v[key]++}// Value 返回给定 key 的计数器的当前值。func (c *SafeCounter) Value(key string) int { // 读的时候加读锁 c.rwmux.RLock() // Lock 之后同一时刻只有一个 goroutine 能访问 c.v defer c.rwmux.RUnlock() return c.v[key]}func main() { c := SafeCounter{v: make(map[string]int)} for i := 0; i < 1000; i++ { go c.Inc("somekey") } time.Sleep(time.Second) for i := 0; i < 10; i++ { fmt.Println(c.Value("somekey")) }}
电脑电量不足,最后还有一个非常重要的地方即管道Channel和select,由于明天这一期的组队学习就结束了,所以明天就单独详细讲述下管道相关知识
参考资料:
并发与并行
http://tutorials.jenkov.com/java-concurrency/concurrency-vs-parallelism.html