Go语言之并发编程

  • 协程

执行体是个抽象的概念,在操作系统层面有多个概念与之对应,比如操作系统自己掌管的
进程(process)、进程内的线程(thread)以及进程内的协程(coroutine,也叫轻量级线程)。与
传统的系统级线程和进程相比,协程的最大优势在于其“轻量级”,可以轻松创建上百万个而不
会导致系统资源衰竭,而线程和进程通常最多也不能超过1万个。这也是协程也叫轻量级线程的
原因。
多数语言在语法层面并不直接支持协程,而是通过库的方式支持,但用库的方式支持的功能
也并不完整,比如仅仅提供轻量级线程的创建、销毁与切换等能力。如果在这样的轻量级线程中
调用一个同步 IO 操作,比如网络通信、本地文件读写,都会阻塞其他的并发执行轻量级线程,
从而无法真正达到轻量级线程本身期望达到的目标。
Go 语言在语言级别支持轻量级线程,叫goroutine。Go 语言标准库提供的所有系统调用操作
(当然也包括所有同步 IO 操作),都会出让 CPU 给其他goroutine。这让事情变得非常简单,让轻
量级线程的切换管理不依赖于系统的线程和进程,也不依赖于CPU的核心数量。

 

  • goroutine

goroutine是Go语言中的轻量级线程实现,由Go运行时(runtime)管理。你将会发现,它的
使用出人意料得简单。
假设我们需要实现一个函数 Add() ,它把两个参数相加,并将结果打印到屏幕上,具体代码
如下:
func Add(x, y int) {
z := x + y
fmt.Println(z)
}
那么,如何让这个函数并发执行呢?具体代码如下:
go Add(1, 1)
是不是很简单?
你应该已经猜到,“go”这个单词是关键。与普通的函数调用相比,这也是唯一的区别。的
确, go 是Go语言中最重要的关键字,这一点从Go语言本身的命名即可看出。
在一个函数调用前加上 go 关键字,这次调用就会在一个新的goroutine中并发执行。当被调用
的函数返回时,这个goroutine也自动结束了。需要注意的是,如果这个函数有返回值,那么这个
返回值会被丢弃。

好了,现在让我们动手试一下吧,还是刚才 Add() 函数的例子,具体的代码如代码清单4-1
所示。
代码清单4-1 add.go
package main
import "fmt"
func Add(x, y int) {
z := x + y
fmt.Println(z)
}
func main() {
for i := 0; i < 10; i++ {
go Add(i, i)
}
}
在上面的代码里,我们在一个 for 循环中调用了10次 Add() 函数,它们是并发执行的。可是
当你编译执行了上面的代码,就会发现一些奇怪的现象:
“什么?!屏幕上什么都没有,程序没有正常工作!”
是什么原因呢?明明调用了10次 Add() ,应该有10次屏幕输出才对。要解释这个现象,就涉
及Go语言的程序执行机制了。
Go程序从初始化 main package 并执行 main() 函数开始,当 main() 函数返回时,程序退出,
且程序并不等待其他goroutine(非主goroutine)结束。
对于上面的例子,主函数启动了10个goroutine,然后返回,这时程序就退出了,而被启动的
执行 Add(i, i) 的goroutine没有来得及执行,所以程序没有任何输出。
OK,问题找到了,怎么解决呢?提到这一点,估计写过多线程程序的读者就已经恍然大悟,
并且摩拳擦掌地准备使用类似 WaitForSingleObject 之类的调用,或者写个自己很拿手的忙等
待或者稍微先进一些的 sleep 循环等待来等待所有线程执行完毕。
在Go语言中有自己推荐的方式,它要比这些方法都优雅得多。
要让主函数等待所有goroutine退出后再返回,如何知道goroutine都退出了呢?这就引出了多个
goroutine之间通信的问题。下一节我们将主要解决这个问题。
 

  • 并发通信

从上面的例子中可以看到,关键字 go 的引入使得在Go语言中并发编程变得简单而优雅,但
我们同时也应该意识到并发编程的原生复杂性,并时刻对并发中容易出现的问题保持警惕。别忘
了,我们的例子还不能正常工作呢。
事实上,不管是什么平台,什么编程语言,不管在哪,并发都是一个大话题。话题大小通常
也直接对应于问题的大小。并发编程的难度在于协调,而协调就要通过交流。从这个角度看来,
图灵社区会员 soooldier(soooldier@live.com) 专享 尊重版权
92 第 4章 并发编程
并发单元间的通信是最大的问题。
在工程上,有两种最常见的并发通信模型:共享数据和消息。
共享数据是指多个并发单元分别保持对同一个数据的引用,实现对该数据的共享。被共享的
数据可能有多种形式,比如内存数据块、磁盘文件、网络数据等。在实际工程应用中最常见的无
疑是内存了,也就是常说的共享内存。
先看看我们在C语言中通常是怎么处理线程间数据共享的,如代码清单4-2所示。
代码清单4-2 thread.c
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
void *count();
pthread_mutex_t mutex1 = PTHREAD_MUTEX_INITIALIZER;
int counter = 0;
main()
{
int rc1, rc2;
pthread_t thread1, thread2;
/* 创建线程,每个线程独立执行函数functionC */
if((rc1 = pthread_create(&thread1, NULL, &add, NULL)))
{
printf("Thread creation failed: %d\n", rc1);
}
if((rc2 = pthread_create(&thread2, NULL, &add, NULL)))
{
printf("Thread creation failed: %d\n", rc2);
}
/* 等待所有线程执行完毕 */
pthread_join( thread1, NULL);
pthread_join( thread2, NULL);
exit(0);
}
void *count()
{
pthread_mutex_lock( &mutex1 );
counter++;
printf("Counter value: %d\n",counter);
pthread_mutex_unlock( &mutex1 );
}
现在我们尝试将这段C语言代码直接翻译为Go语言代码

package main
import "fmt"
import "sync"
import "runtime"
var counter int = 0
func Count(lock *sync.Mutex) {
lock.Lock()
counter++
fmt.Println(z)
lock.Unlock()
}
func main() {
lock := &sync.Mutex{}
for i := 0; i < 10; i++ {
go Count(lock)
}
for {
lock.Lock()
c := counter
lock.Unlock()
runtime.Gosched()
if c >= 10 {
break
}
}
}
此时这个例子终于可以正常工作了。
在上面的例子中,我们在10个goroutine中共享了变量 counter 。每个goroutine执行完成后,
将 counter 的值加1。因为10个goroutine是并发执行的,所以我们还引入了锁,也就是代码中的
lock 变量。每次对 n 的操作,都要先将锁锁住,操作完成后,再将锁打开。在主函数中,使用 for
循环来不断检查 counter 的值(同样需要加锁)。当其值达到10时,说明所有goroutine都执行完
毕了,这时主函数返回,程序退出。
事情好像开始变得糟糕了。实现一个如此简单的功能,却写出如此臃肿而且难以理解的代码。
想象一下,在一个大的系统中具有无数的锁、无数的共享变量、无数的业务逻辑与错误处理分
支,那将是一场噩梦。这噩梦就是众多C/C++开发者正在经历的,其实Java和C#开发者也好不到
哪里去。
Go语言既然以并发编程作为语言的最核心优势,当然不至于将这样的问题用这么无奈的方
 

并发编程
式来解决。Go语言提供的是另一种通信模型,即以消息机制而非共享内存作为通信方式。
消息机制认为每个并发单元是自包含的、独立的个体,并且都有自己的变量,但在不同并发
单元间这些变量不共享。每个并发单元的输入和输出只有一种,那就是消息。这有点类似于进程
的概念,每个进程不会被其他进程打扰,它只做好自己的工作就可以了。不同进程间靠消息来通
信,它们不会共享内存。
Go语言提供的消息通信机制被称为channel,接下来我们将详细介绍channel。现在,让我们
用Go语言社区的那句著名的口号来结束这一小节:
“不要通过共享内存来通信,而应该通过通信来共享内存。”

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值