从0开始go语言-14|Go主题月

224 阅读5分钟

并发编程

接着来学习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 int10) 
     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主题学习月」, 点击查看活动详情