并发编程
接着来学习Go语言并发编程的知识。
并发通信
Go语言的并发编程优雅的引入了go关键字,但是我们也意识到并发没有想象中那么简单,并发一直是各种语言解决起来很复杂的问题,并发涉及到原子性和竞争关系,数据通信和共享,并发从来就没有简单的(面试java挂并发的还少吗?一把泪!!)所以需要时刻需要对并发编程保持高度的敬畏和警惕。
并发编程的难度在于协调,而协调就需要交流,而交流少不了通信,那么归根结底并发最大的问题是底层的通信问题。
在工程上有两种常见的并发通信模型,共享数据和消息。
共享数据:是指多个并发单元分别保持对同一数据的引用,实现对该数据的共享,被共享的数据可以是多种形式,比如:内存数据块,磁盘文件内容等,在实际工作中我们常见的是在内存中操作数据,那么共享的就是共性内存。
先了解下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
}
}
}
这个代码能看懂,在每次执行++操作的时候,先上锁Lock()
然后执行业务逻辑++运算,然后再打印,最后释放锁Unlock()
,主函数一直循环去调用,如果发现是锁着的就不进入函数,如果发现没有锁就持有该锁,上锁执行业务逻辑++然后再释放锁。一直等到循环10遍的话,程序退出。执行完毕打印了10次!
上述的做法其实就是程序员自己手动加锁和释放锁,手动的处理,这和c++对锁的操作做法几乎一致,那么怎么体现Go语言多线程高并发下并发编程的优雅呢?思考脸?那么Go语言引入了另一种通信模型:消息机制。
经典的解释来了,消息机制认为每个并发单元是自包含的、独立的个体,并且都有自己的变量(私有),但在不同并发单元间这些变量不共享。每个并发单元的输入和输出只有一种,那就是消息(改变了我告诉你我改了啥)。这有点类似于进程的概念,每个进程不会被其他进程打扰,它只做好自己的工作就可以了。不同进程间靠消息来通信,它们不会共享内存。那么Go语言引入的消息通信为channel(自我理解这个单词是通道或者管道),还有Go语言并发编程有句响亮的口号不要通过共享内存来通信,而应该通过通信来共享内存前一句听懂了,后一句....
channel
channel是Go语言在语言级别提供的goroutine间的通信方式。多个goroutine之间通过channel传递消息。channel是进程内的通信方式,因此通过channel传递对象的过程和调用函数时的参数传递行为比较一致,比如也可以传递指针等。如果需要跨进程通信,需要其他方法来解决,比如使用Socket或者HTTP等通信协议来解决。
那么channel实现上文的循环打印怎么写,代码如下:
package main
import "fmt"
func Count(ch chan int) {
ch <- 1
fmt.Println("Counting")
}
func main() {
chs := make([]chan int, 10)
for i := 0; i < 10; i++ {
chs[i] = make(chan int)
go Count(chs[i])
}
for _, ch := range(chs) {
<-ch
}
}
解释:
- 主函数先定义一个数组,数组内容是10个channel(名为chs)。
- 把数组中的每个channel分配给10个不同的goroutine。
- 在每个goroutine的Add()函数完成后,通过ch <- 1语句向对应的channel中写入一个数据。
- 在这个channel被读取前,这个操作是阻塞的。在所有的goroutine启动完成后,通过<-ch语句从10个channel中依次读取数据。在对应的channel写入数据前,这个操作也是阻塞的。
- 我们就用channel实现了类似锁的功能,进而保证了所有goroutine完成后主函数才返回。 自我理解这种做法有点像令牌桶的做法,提前放10个令牌在桶里面,放入和读取都是阻塞的。执行10次操作的时候去令牌桶拿依次拿令牌,拿完一个执行++,然后再拿下一个这种做法。水平有限先这样理解....
备注
今天周五,公司没人了,我一个人在工位上水文章,突然办公室领导准备走,出来见整个公司就我一个人,冲我笑笑说我发现你就是我们公司最能加班的加班狂人!!!哎!!我也不想呀!水完这篇我就走还不好吗?最后祝掘金的小伙伴周末愉快!!!
本文正在参与「掘金Golang主题学习月」, 点击查看活动详情。