【Go语言】【16】GO语言的并发

原创作品,允许转载,转载时请务必以超链接形式标明文章 原始出处 、作者信息和本声明。否则将追究法律责任。http://qingkechina.blog.51cto.com/5552198/1678483

       在写该文之前一直犹豫,是把Go的并发写的面面俱到显得高大尚一些,还是简洁易懂一些?今天看到一个新员工在学习Java,突然间想起第一次接触Java的并发时,被作者搞了一个云里雾里,直到现在还有阴影,所以决定本文从简。哈哈,说笑了,言归正传。

       Go的并发真的很简单,所以本文不罗嗦进程、线程、协程、信号量、锁、调度、时间片等乱七八糟的东西,因为这些不影响您理解Go的并发。先看一个小例子:

package main

import "fmt"

func Add(i, j int) {

        sum := i + j

        fmt.Println(i, " + ", j, " = ", sum)

}

func main() {

        for i := 0; i < 10; i++ {

               Add(i, i)

        }

}

这个例子很简单吧,说白了就是计算0+0、1+1、2+2、3+3、.......、9+9之和,并打印出来,运行结果显而易见:

这里没有使用并发呀,好吧,为了提升计算效率,在main的for循环中使用并发,把代码修改如下:

package main

import "fmt"

func Add(i, j int) {

        sum := i + j

        fmt.Println(i, " + ", j, " = ", sum)

}

func main() {

        for i := 0; i < 10; i++ {

               go Add(i, i)

        }

}

没有花眼吧,前面加了一个go,这就并发了?

嗯,这就并发了。

在方法Add()之前增加了一个关键字go,相当于告诉Go编译器启动一个goroutine,然后把Add()方法放到goroutine中执行。

什么是goroutine?

有人把它翻译为协程,说实话挺反感的,有些单词还是不要翻译为好,比如Context,经常写Web程序的人会遇到,有人把它翻译为上下文;再如payload,经常做渗透的人会使用,怎么翻译好呢?还是不翻译了吧。

该怎么理解goroutine?

如上图所示,main()方法所在的goroutine上又创建了10个goroutine,每个goroutine各自跑一个Add()方法

OK,运行一下该程序,结果如下:

咦,怎么都没有,说好的运行结果呢?

       这是因为main()所在的goroutine创建10个goroutine后,它里面的逻辑已执行完,那么main()就退出了,它根本就不管这10个goroutine的死活。从结果也能看出,当main()退出时,这10个goroutine没有一个执行完,所以结果什么都没有打印。

如何解决这个问题呢?

一种比较容易想到的但又比较垃圾的解决办法是:“让main()等一会儿”,下面我们修改一个这个程序


package main

import (

       "fmt"

       "time"

)

func Add(i, j int) {

        sum := i + j

        fmt.Println(i, " + ", j, " = ", sum)

}

func main() {

        for i := 0; i < 10; i++ {

               go Add(i, i)

        }

        time.Sleep(time.Second * 3)  // main()所在goroutine休息3秒钟

}

首先引入"time"这个包,然后调用time.Sleep()方法,让main()所在goroutine等3s,等其它10个goroutine都运行完,这种解决办法就是“马儿你慢些跑呀慢些跑” :)

运行一下结果:

可能您会问,为何要等3秒钟而不是2秒钟?

我只能学着印度老外,一边摇头一边微笑地告诉您,我是蒙的,因为我也不知道确切地等多长时间,所以是一种垃圾的解决办法。

那有没有一种通知机制呢?当一个goroutine执行完毕后,就告诉主goroutine(即main()方法所在的goroutine):“嘿,哥们,我执行完了,你想干嘛就干嘛吧!”

有,这就是Go语言的亮点,十分耀眼的一个亮点:channel

什么是channel?

说白了就是一个通道(建议还是不翻译的为好),一个goroutine执行完毕后,就告诉主goroutine,I'm over!怎么告诉呢?就是向channel中写一个数据。

怎么向channel中写一个数据呢?

OK,follow me,要想写一个数据到channel则必须有一个channel不是?所以:

(1)建立一个channel



【备注】:所谓channel也是一种Go的类型,与int、float64、string、bool、struct、slice、map等同等地位



var ch chan int           // 声明一个变量为ch,它的类型为chan类型,这个channel里面可以存放int型的值

ch = make(chan int)  // 使用make关键字初始一个长度为0的通道

当然声明和初始化可以一块来

var ch chan int = make(chan int)

(2)向channel中写一个数据

ch <- 1

就这么简单,使用符号”<-“,前面声明了channel的类型为int ,所以就把1写入ch;若声明channel类型为bool,就可以把布尔值写入channel,即ch <- true

(3)从channel中读数据

<- ch

嗯,还是这么简单

无论写还是读都是用符号”<-“,就看<-后面是谁

OK,既然知道有channel这东东了,我们修改一下上面的程序:


package main

import (

        "fmt"

        // "time"   // 删除掉time包

)

var ch chan int = make(chan int)     // 初始化一个类型为channel的变量ch,其中channel里面放int型数据

func Add(i, j int) {

        sum := i + j

        fmt.Println(i, " + ", j, " = ", sum)

        ch <- i   // 这个方法执行完,意味着方法所属的goroutine即将退出,就告诉主goroutine,I'm完事了

}

func main() {

        for i := 0; i < 10; i++ {

               go Add(i, i)

        }

        /*

         * 由于有10个goroutine,所以从channel中读10次

         * 这10个goroutine都告诉主gorouinte说完事了,那么主gorouinte也就退出了

         */

        for i := 0; i < 10; i++ {

               fmt.Println("i=", <-ch)

        }

  

        // time.Sleep(time.Second * 3)    // 不用这种机制了,留着也没有用

}

运行结果如下:

目的达到了,我好人做到底,再解释一下:

可以这样理解,有10个厨师1个端菜工,这10个厨师各自做各自的菜,做完之后就放到channel,这个端菜工就从channel中取菜。当channel中没有菜时,端菜工就一直等待直到有菜为止;厨师做好一个菜后,发现channel中没有菜,就把自己的菜放到channel中,若发现channel在有菜还没有端正,厨师就拿着自己的菜一直等到channel中的菜被端菜工端走后再把自己的菜放进去。

可能您又要说了,这种channel很类似同步操作,这个channel只能放一个菜,端菜工端一个菜;channel中没有菜端菜工等待;channel中有菜厨师等待。效率不高呀。

好吧,Go设计师已提前为您想好处理办法了,即这个channel可以放10个菜,只要这个channel还没有放满10个菜,厨师就可以向上面放,这样厨师就不用等待了。剩下的就是端菜工要提高自己的工作效率了。

怎么放10个菜?

var ch chan int = make(chan int,10)

OK,搞定!代码如下:


package main

import (

       "fmt"

       // "time"

)

var ch chan int = make(chan int, 10)

func Add(i, j int) {

        sum := i + j

        fmt.Println(i, " + ", j, " = ", sum)

        ch <- i

}

func main() {

        for i := 0; i < 10; i++ {

                go Add(i, i)

        }

        for i := 0; i < 10; i++ {

                fmt.Println("i=", <-ch)

        }

        // time.Sleep(time.Second * 3)

}

运行结果如下:

仔细看,再仔细看,看出什么东西来了没有?若没有,请与上一个运行结果对比着看 :)

还是没有看来?

没有发现这两个基本上是一模一样的吗?除了运行程序所花的时间不同之外!

从运行时间上来看,好像效率提升了一些,这是因为厨师不用等待了,一旦指明了channel的容量,相对于厨师来说就变成异步的了;没有指明channel容量,相对于厨师来说就是同步的。

同步异步不是我想让您观察的重点,您难道没有发现0+0、1+1、2+2、3+3、......、9+9,这个顺序太正常了吗?若真正并发的话,这个顺序肯定是乱的!

看一下我电脑信息:

好呆CPU也是四核的,并发的顺序是这么的正常,太不可思议了 :)

好吧,我再解开这谜团吧

我用的Go版本是1.4,可以在命令窗口中执行go version查看。由于Go语言1.4版本对多核的处理还没有做太多的改进,据说1.5版本有突破,后面可以关注一下,所以在这个版本还是使用的一个核。对于单核CPU进程、线程在执行的过程中,系统会把运行的CPU强行切给另一个进程或线程。

       有人如果看过其他人的博客、书,都会把goroutine翻译为协程,所谓协程就是用户态的线程,可以这样理解:”一个协程在执行时,系统不会强行切换时间片“。即一个goroutine在执行的过程中,Go语言会让这个goroutine疯狂地执行,直到它运行完为止,再让另外一个gorouinte运行,所以从结果来看运行顺序是固定的。

如果利用多核?

Go语言也提供了一种方式,具体代码如下:


package main

import (

        "fmt"

         // "time"

        "runtime"   // 引入runtime包

)

var ch chan int = make(chan int, 10)

func Add(i, j int) {

        sum := i + j

        fmt.Println(i, " + ", j, " = ", sum)

        ch <- i

}

func main() {

        runtime.GOMAXPROCS(runtime.NumCPU())   // 让Go使用多个核

        for i := 0; i < 10; i++ {

                go Add(i, i)

        }

        for i := 0; i < 10; i++ {

                fmt.Println("i=", <-ch)

        }

        // time.Sleep(time.Second * 3)

}

多运行几次,结果如下:

上面就是Go的并发核心内容,当然还有关于并发的其它内容,如单向写channel、单向读channel、传统并发方式等内容。本文就先写到这里,内容多了不容易消化 :)

本文出自 “青客” 博客,请务必保留此出处http://qingkechina.blog.51cto.com/5552198/1678483

时间: 2024-09-06 07:45:37

【Go语言】【16】GO语言的并发的相关文章

Facebook推出IE工具栏:新增16种语言

北京时间1月29日午间消息,据国外媒体报道,Facebook周四宣布,继两个月前推出火狐工具栏后,该公司又推出了IE工具栏,并且新增了16种全新的语言版本. Facebook火狐工具栏此前只支持英文,而这两个版本的工具条现在都新增了阿拉伯语.中文(简体和繁体).德语.法语等16种语言. 这款工具栏提供了Facebook多种功能的链接,包括用户主页.收件箱.好友资料,并且还会显示尚未查看的好友请求.收件箱信息.状态更新以及用户邀请的数目.通过这款工具栏,Facebook用户还可以与好友分享信息,并

Swift语言指南(三)--语言基础之整数和浮点数

原文:Swift语言指南(三)--语言基础之整数和浮点数   整数   整数指没有小数的整数,如42,-23.整数可以是有符号的(正数,零,负数),也可以是无符号的(正数,零). Swift提供了8,16,32,64位形式的有符号和无符号的整数,这些整数遵循与C语言相似的命名规则.如8位无符号整数的类型为UInt8,32位有符号整数的类型为Int32,和Swift语言的其它类型一样,这些整型命名以大写字母开头.   整数的边界 你可以通过min或max属性为每一个整数类型指定一个最小值或最大值:

Java语言与C++语言的差异总结

Java的设计者曾说过,设计这门语言的灵感主要来自于C++. 世上先有C++,然后才有Java,整个Java语言的发展历史就是一部对C++的填坑史.所以在Java语言学习过程中,将其与C++语言对比是一件有意义的事情.通过这些对比,我们能够了解到Java语言相对于C++语言有哪些改进,能带给我们哪些进步,这样对于更加深入理解这两种语言是大有裨益的. 下面我总结一下Java语言与C++语言的各种差异. 1.Java用来操纵对象的引用可以先初始化再指向对象,而C++的引用必须在初始化时就指向对象.

C#语言与Java语言程序的比较

迈入二十一世纪以来,信息行业飞速壮大,其中在软件开发中Java语言与C#语言都独当一面,得到了逐步完善和广泛的应用,Java语言和C#语言都是一种面向对象的语言,但Java程序和C#程序还是有一定的区别. 下面分别是两种语言的程序的例子. 1.Java语言的基本程序: Import java.util.*; Package HelloJava{ Public class Message{ Public static void main(String [] args){ System.out.pr

Thinkphp搭建包括JS多语言的多语言项目实现方法_php实例

本文实例讲述了Thinkphp搭建包括JS多语言的多语言项目实现方法.分享给大家供大家参考.具体实现方法如下: 一.问题: 项目需要开发英文版,于是需要搭建多语言项目. 项目使用Thinkphp框架,隐约记得Thinkphp有多语言设置,翻看了帮助手册,果然有,这就边实验边开始: 二.实现方法: Thinkphp采用app_begain来检测和切换语言包,语言包和项目相关,构架等都比较简单,具体的这里:http://www.thinkphp.cn/info/188.html 搭建好了,就可以使用

r语言-对一个向量的划分,求C语言或R语言实现

问题描述 对一个向量的划分,求C语言或R语言实现 1C 向量U={123456}利用R1属性划分为:U/R1={{123}{456}}利用R2属性划分为:U/R2={{12}{3456}}利用R3属性划分为:U/R3={{1234}{56}}最后得到划分的交集:U/R={{12}{3}{4}{56}}

Swift语言指南(一)--语言基础之常量和变量

原文:Swift语言指南(一)--语言基础之常量和变量 Swift 是开发 iOS 及 OS X 应用的一门新编程语言,然而,它的开发体验与 C 或 Objective-C 有很多相似之处. Swift 提供了 C 与 Objective-C 中的所有基础类型,包括表示整数的 Int,表示浮点数的 Double 与 Float,表示布尔值的 Bool,以及表示纯文本数据的 String. Swift 还为两个基本集合类型 Array 与 Dictionary 提供了强大的支持,详情可参考 (集合

各国语言缩写列表,各国语言缩写-各国语言简称,世界各国域名缩写

$amount = '12345.67'; $formatter = new \NumberFormatter('en_GB', \NumberFormatter::CURRENCY); echo 'UK: ' . $formatter->formatCurrency($amount, 'EUR') . '<br/>'; $formatter = new \NumberFormatter('de_DE', \NumberFormatter::CURRENCY); echo 'DE: '

c语言与c++语言之间的相互转化

问题描述 c语言与c++语言之间的相互转化 struct student*p0 scanf("%s",p0->num) scanf("%s",&p0->name) 解决方案 C++也能兼容C,直接编译看有没有什么语法错误 解决方案二: 对的,C++编译器本来就支持几乎所有C语法 你为何还需要改 C --> C++ #include scanf("%s",p0->num); --> std::cin >&

c语言-C语言递归函数C语言递归函数C语言递归函数

问题描述 C语言递归函数C语言递归函数C语言递归函数 #include<stdio.h> #include<stdlib.h> //用递归函数来计算N的阶乘 double factorial(int n) { double result; if(n<0) { printf("输入错误 "); } else if(n==1 ||n==0) { result=1; } else { result=factorial(n-1)*n; //n=5 5-1=4 4*