go go的并发

研究下go的并发。

参考:http://www.cnblogs.com/yjf512/archive/2012/06/06/2537712.html

 

一、goroutine

1.是什么?

对于初学者,goroutine直接理解成为线程就可以了。当对一个函数调用go,启动一个goroutine的时候,就相当于起来一个线程,执行这个函数。

实际上,一个goroutine并不相当于一个线程,goroutine的出现正是为了替代原来的线程概念成为最小的调度单位。一旦运行goroutine时,先去当前线程查找,如果线程阻塞了,则被分配到空闲的线程,如果没有空闲的线程,那么就会新建一个线程。注意的是,当goroutine执行完毕后,线程不会回收退出,而是成为了空闲的线程。

2.为什么?

goroutine和线程并不是一回事。为什么要使用goroutine?

借用网友LazyTiger和洪瑞琦的回答:

https://groups.google.com/forum/#!topic/golang-china/Dp1oPKdm7AA

传统多线程程序:

在Thread1 block之后,如果还想要干其他事情,

那就只有再开一个thread来做事了。

这样一来,当线程足够多时,性能就会下降,这是显然的。

于是就有了非阻塞式/异步 比如epoll:

也即将原来可能阻塞的地方的代码以回调,事件等方式分解,在别的时候当条件满足时再执行,这样就可以利用为数不多的线程来高效的完成任务。

但是显然这种方式,写起来很费劲,读起来很费劲,调试起来也很费劲。

而go语言利用goroutine来解决了这一点,goroutine替代原来的thread成了最小调度单元:当前线程阻塞时,其它的goroutine并不是在当前线程执行,而是被分配到空闲的线程(阻塞不是空闲),如果没有空闲线程就新建一个。新建线程中的goroutine执行完毕后,线程不会退出,成为空闲线程(一个动态增加的线程池)。

我的理解:

我感觉有点像是java中的线程池的概念。并不是每次要执行什么方法都去重新开一个线程,而是从线程池中寻找空闲的线程,避免过多线程引起性能下降。

写java线程池,是可以使用提供的api对线程池进行管理的。现在我最大的问题是不知道goroutine有没有这样的管理方法。

3.怎么使用?

(1)在函数前增加一个go,即可使用goroutine。

go ready(“Xie”, 2):使用goroutine运行ready方法。

(2)这里有个问题,对比一下:

如果在ready方法中使用time.Sleep(sec * 1e9),编译时就会报出如下错误:

但是在main方法中使用time.Sleep(5 * 1e9),则没有这个问题。

尝试把time.Sleep(time.Duration(sec * 1e9))替换成time.Sleep(5 * 1e9),在方法中也可以正确编译了。

感觉差别只在于有没有sec这个参数而已。

查了一下,发现stackoverflow上有这样的解释(大概是这个意思):

在main方法中,5 * 1e9可以自动转换。但是方法中的sec则需要手动进行转换。所以需要time.Duration()进行一次转换才行。

(3)假如把时间改成这样:

那么结果就会变成这样:

可以发现Pjl就不能ready了。意思就是:我根本不关心你们goroutine执行得怎么样了,只要我主线程(main())到时间了,我就直接结束进程。

那么问题就来了:主线程要等待goroutine结束再处理怎么办?这个场景在平时开发中很常见。比如说开十个线程对数据分别进行运算,最后才在主线程进行汇总。在这种情况下,主线程当然要等待所有子线程都执行完毕了,才能开始进行汇总,不然就会出问题。

这里就出来了一个需求:一个goroutine结束后必须要向主线程传输数据,告诉主线程这个goroutine已经结束了。

也就是说,单单goroutine不能实现对线程的控制,需要其他的方法。

二、channel

1.是什么?

主线程告诉大家你开goroutine可以,但是我在我的主线程开了一个管道,你做完了你要做的事情之后,往管道里面塞个东西告诉我你已经完成了。

2.为什么?

刚才提到了,要对goroutine的执行顺序进行管理。

3.怎么使用?

(1)声明一个channel

变量为chan

(2)实例化这个channel

通过make来进行实例化。管道要声明类型名。int说明这个管道能传输int类型的数据。

(3)channel输入输出数据

(4)为什么channel要输出两次?

如果我们去掉一个<-channel,结果变成:

因为开启了两个goroutine,每个goroutine都往管道输出一个1,因此主线程要接收两次才能说明两个goroutine都结束了。

否则主线程要接收不到channel了,就认为goroutine已经结束了,就会继续执行。

三、带缓冲和不带缓冲的channel

现在的go环境,好像带缓冲和不带缓冲都没有什么区别了…

带缓冲和不带缓冲的channel:

1.先放再取

无论是带缓冲和不带缓冲的channel,结果都是正常的。

输出均为:

2.先取再放

(1)不带缓冲的channel

输出也是:

(2)带缓冲的channel

这次的输出变成了:

因为这次的channel是带缓冲的,所以要把channel的缓冲区填满,才能从channel中获取数据。因为只输入了一个c <- 0,所以channel的缓冲区没有填满,主线程没有收到channel,就不知道goroutine执行完毕了,所以var a string依然是空字符串。

但是这样输入一堆数据,强行把缓冲区填满,就可以获取了:

(3)总结一下

借用网友黑斯免疫缺陷综合征网友的总结:

c1:=make(chan int) 无缓冲channel。

c2:=make(chan int,1) 有缓冲channel。

c1<-1

无缓冲的,c1<-1这句阻塞了。不仅仅是向c1 channel放1,而是一直要有别的线程<-c1接手了这个参数,那么c1<-1才会继续下去,要不然就一直阻塞着(先放再取的例子中,如果不<-c去去channel中的值,那么goroutine就会被阻塞)。

而c2<-1 则不会阻塞,因为缓冲大小是1 (其实是缓冲大小为0)只有当放第二个值的时候,第一个值还没被人拿走,才会阻塞。

四、总结

因为有goroutine和channel,go极大地简化了多线程编程。我个人感觉,不但比java更省心,效果也更好。

发表评论

电子邮件地址不会被公开。 必填项已用*标注