go语言的设计初衷除了在不影响程序性能的情况下减少复杂度,另一个目的是在当今互联网大量运算下,如何让程序的并发性能和代码可读性达到极致。go语言的并发关键词 "go"

go dosomething() //走,兄弟我们搞点事情

案例一:并发编程

func say(s string) {
fmt.Printf("%s say\n", s)
}
func main() {
go say("lisi")
say("zhangsan")
}

执行结果

zhangsan say

上面的案例执行了2次say方法,但只有zhangsan执行成功了。原因是因为lisi是开了一个goroutine去执行,还没执行完但此时的main函数已经退出了。

案例二:并发编程

lisi估计是有点害羞,说话语速比较慢,因此我们要等lisi一下,抛开串行执行和sleep外我们用一个消息管道类通知,这里我们就要zhangsan和lisi一起说

func say(s string, c chan int) {
fmt.Printf("%s say\n", s)
c <- 1 //在消息管道里传1,代表我已经说过了
} func main() {
c := make(chan int)
go say("lisi", c)
go say("zhangsan", c)
v1, v2 := <-c, <-c
fmt.Printf("lisi:%d , zhangsan:%d\n", v1, v2)
}

执行结果如下,当然也有可能lisi say 在zhangsan say的前面,等于1代表他俩都说过话了

zhangsan say
lisi say
lisi:1 , zhangsan:1

过程分解

1、创建一个无缓冲的channel

2、异步执行 go say("lisi", c)

3、异步执行 go say("zhangsan", c)

4、假设zhangsan先执行,那么zhangsan的1先放入管道c,如果这时候正好lisi在执行,不好意思管道c只有1个长度放不下了。此时lisi: c<- 1阻塞

5、v1 := <- c 执行,zhangsan的值1从管道里拿出来了。

6、lisi执行 c <- 1

7、v2 := <- c执行,lisi的值1也从管道里拿出来了

8、执行fmt.Printf

并发编程就这么用的,不过大家发现问题没有,过程分解步骤4有阻塞,同一时刻5和7也是阻塞的(等待管道里拿值迟迟拿不到)

适当改版一下,如下:

func say(s string, c chan int) {
fmt.Printf("%s say\n", s)
c <- 1 //在消息管道里传1,代表我已经说过了
} func main() {
c := make(chan int, 2) //改动点,管道长度设成了2
go say("lisi", c)
go say("zhangsan", c)
v1, v2 := <-c, <-c
fmt.Printf("lisi:%d , zhangsan:%d\n", v1, v2)
}

这时候的过程分解

1、创建一个缓冲为2的channel

2、异步执行 go say("lisi", c)

3、异步执行 go say("zhangsan", c)

4、假设zhangsan先执行,那么zhangsan的1先放入管道c,如果这时候正好lisi在执行,lisi的1也放入管道c

5、v1 := <- c 执行,zhangsan的值1从管道里拿出来了。

6、v2 := <- c执行,lisi的值1也从管道里拿出来了

7、执行fmt.Printf

理论上来说应该是少了一步,实际情况可能会更好一些,因为步骤4没有阻塞(也就是zhangsan和lisi的值1可以同时放进去)。

步骤5和6虽然有阻塞(这里的阻塞跟c#里的await是一个意思),但是管道c一旦有值会立马拿出来,等v1和v2都有值了然后执行fmt.Printf

又有问题了!

  1. 如果say方法有返回值怎么办? 如下代码案例说明
func say(s string) int {
fmt.Printf("%s say\n", s)
return 1
}
func main() {
msg:= go say("lisi", c) //PS:这里会报错syntax error: unexpected go, expecting expression
}
  1. 这个chan只能传int或者string 如果我的返回只是一个struct结构体(实体)怎么办?
  2. 如果say方法是别人写的,他的参数没有chan管道我又想并发执行怎么办?

还是看代码吧

package main

import (
"fmt"
) //学生结构体(实体)
type Stu struct {
Name string
Age int
} func say(name string) Stu {
fmt.Printf("%s say\n", name)
stu := Stu{Name: name, Age: 18}
return stu
}
func main() {
c := make(chan int)
go func() {
stu := say("lisi") //返回一个学生实体
fmt.Printf("我叫%s,年龄%d\n", stu.Name, stu.Age)
c <- 1 //信号位表示调用完毕
}()
fmt.Println("go func")
<-c
fmt.Println("end")
}

执行结果:

go func

lisi say

我叫lisi,年龄18

end

错误示范:死锁

func say(s string, c chan int) {
fmt.Printf("%s say\n", s)
//c <- 1 这里本来应该给c管道传值的,结果没传
}
func main() {
c := make(chan int)
go say("lisi", c)
v1 := <-c //这里会一直阻塞,导致死锁
fmt.Printf("lisi:%d\n", v1) //前面死锁,这里无法输出
}

执行报错内容:

fatal error: all goroutines are asleep - deadlock!

goroutine简析:

goroutine也叫协程是一种轻量级别用户空间线程,不受操作系统的调度,所以需要用户自行调度(一般是加锁和信道),协程能做的事情进程和线程同样能做。进程和线程的切换主要依赖于时间片的控制,而协程的切换则主要依赖于自身,这样的好处是避免了无意义的调度,由此可以提高性能,但也因此,程序员必须自己承担调度的责任

什么是协程:from百科

协程与子例程一样,协程(coroutine)也是一种程序组件。相对子例程而言,协程更为一般和灵活,但在实践中使用没有子例程那样广泛。协程源自 Simula 和 Modula-2 语言,但也有其他语言支持

ps:子例程是某个主程序的一部分代码

goroutine可以看作是协程的go语言实现,它是语言原生支持的,相对于一般由库实现协程的方式,goroutine更加强大,它的调度一定程度上是由go运行时(runtime)管理。其好处之一是,当某goroutine发生阻塞时(例如同步IO操作等),会自动出让CPU给其它goroutine。

后面会单独的在介绍进程、线程、协程之前的关系,也可以参考以下几篇文章

  1. 进程、线程、轻量级进程、协程和go中的Goroutine 那些事儿
  2. golang的goroutine是如何实现的?

channel 简析

channel是Go语言在语言级别提供的goroutine间的通信方式。我们可以使用channel在两个或 多个goroutine之间传递消息。channel是进程内的通信方式,因此通过channel传递对象的过程和调用函数时的参数传递行为比较一致,比如也可以传递指针等。如果需要跨进程通信,我们建议用 分布式系统的方法来解决,比如使用Socket或者HTTP等通信协议。Go语言对于网络方面也有非常完善的支持。 channel是类型相关的。也就是说,一个channel只能传递一种类型的值,这个类型需要在声明channel时指定。如果对Unix管道有所了解的话,就不难理解channel,可以将其认为是一种类 型安全的管道。

关于channel有必要详细了解下。可以参考

golang的channel使用

重点来了,goroutine号称是轻松开上万个并发

package main

import (
"fmt"
"time"
) var sum int = 0 func todo(i int, c chan int) {
//c <- 1 //执行一次放一个值1
c <- i //把i的值放进去
}
func getSum(count int, c chan int, ce chan int) {
for i := 0; i <= count; i++ {
sum += <-c
// k, isopen := <-c
// if !isopen {
// fmt.Printf("channel is close")
// break
// } else {
// fmt.Printf("sum:%d,k:%d\n", sum, k)
// sum += k
// }
}
ce <- 1
}
func main() {
count := 100000 //10W个goroutine
c := make(chan int, count) //有缓冲channel
ce := make(chan int) //计算getSum信号量
//开始计时
begin := time.Now()
fmt.Println("开始时间:", begin)
for i := 0; i <= count; i++ {
go todo(i, c)
}
//再开一个goroutine去计算channel里的值求Sum
go getSum(count, c, ce)
<-ce //这里是getSum方法执行结束信号量
end := time.Now()
fmt.Println("结束时间:", end, time.Since(begin))
fmt.Println(sum) }

硬件信息

环境:THINKPAD L460、WIN7x64、8G内存、i5-6200U 2.3GHz 双核4线程

语言:LiteIDE X33、golang 1.9.2

多次执行结果:38.5ms - 51ms之间

再次改版下

把 c := make(chan int, count) 改为 c := make(chan int) 改成无缓冲

c := make(chan int)    //重点,这里改成无缓冲的

多次执行结果:304-325ms之间

结论:明显无缓冲区耗时多了接近300ms,这部分时间实际是channel读取阻塞的时间,因此在大量并发的情况下channel的缓冲区大小会直接影响程序的性能,这也是前面提到需要用户自行调度的原因之一!!!

顺便来一发.net core 的并发代码实验,和上面goroutine同样的机器和环境

class Program
{
private static readonly object obj = new object();
static void Main(string[] args)
{
DateTime begin = DateTime.Now;
long sum = 0;
Parallel.For(1, 100001, (i) =>
{
lock (obj)
{
sum += i;
}
});
TimeSpan ts = DateTime.Now - begin;
Console.WriteLine($"{sum},耗时:{ts.TotalMilliseconds}ms");
Console.ReadLine();
}
}

运行结果 : 大约在90-120ms左右,虽然数值上差了2倍左右,其实差别并不是很大,也没有直接的可比性,因为线程和协程并不是一个数量级别,上面goroutine用到了channel通道,net core 用的lock锁,因此仅供参考。总体看来.net core的性能整体还是蛮高的



PS:题外话 其实c#里也有协程"fiber",网上资料比较少了解不多。

golang并发编程goroutine+channel(一)的更多相关文章

  1. Golang并发编程——goroutine、channel、sync

    并发与并行 并发和并行是有区别的,并发不等于并行. 并发 两个或多个事件在同一时间不同时间间隔发生.对应在Go中,就是指多个 goroutine 在单个CPU上的交替运行. 并行 两个或者多个事件在同 ...

  2. Golang - 并发编程

    目录 Golang - 并发编程 1. 并行和并发 2. go语言并发优势 3. goroutine是什么 4. 创建goroutine 5. runtime包 6. channel是什么 7. ch ...

  3. golang并发编程

    golang并发编程 引子 golang提供了goroutine快速实现并发编程,在实际环境中,如果goroutine中的代码要消耗大量资源时(CPU.内存.带宽等),我们就需要对程序限速,以防止go ...

  4. Golang并发编程优势与核心goroutine及注意细节

    Go语言为并发编程而内置的上层API基于CSP(communication sequential processes,顺序通信进程)模型.这就意味着显式锁都是可以避免的,比如资源竞争,比如多个进程同时 ...

  5. golang并发编程的两种限速方法

    引子 golang提供了goroutine快速实现并发编程,在实际环境中,如果goroutine中的代码要消耗大量资源时(CPU.内存.带宽等),我们就需要对程序限速,以防止goroutine将资源耗 ...

  6. Golang并发编程基础

    硬件 内存 作为并发编程一个基础硬件知识储备,首先要说的就是内存了,总的来说在绝大多数情况下把内存的并发增删改查模型搞清楚了其他的基本上也是异曲同工之妙. 内存芯片--即我们所知道的内存颗粒,是一堆M ...

  7. Golang并发编程进程通信channel了解及简单使用

    概念及作用 channel是一个数据类型,用于实现同步,用于两个协程之间交换数据.goroutine奉行通过通信来共享内存,而不是共享内存来通信.引用类型channel是CSP模式的具体实现,用于多个 ...

  8. Golang并发编程有缓冲通道和无缓冲通道(channel)

    无缓冲通道 是指在接收前没有能力保存任何值得通道.这种类型的通道要求发送goroutine和接收goroutine同时准备好,才能完成发送和接收操作.如果两个goroutine没有同时准备好,通道会导 ...

  9. go/wiki/MutexOrChannel Golang并发:选channel还是选锁?

    https://mp.weixin.qq.com/s/JcED2qgJEj8LaBckVZBhDA https://github.com/golang/go/wiki/MutexOrChannel M ...

随机推荐

  1. python打开.pkl的文件并显示里面的内容

    pkl文件是pyhthon里面保存文件的一种格式,如果直接打开会显示一堆序列化的东西.正确的打开方式如下: import cPickle as pickle f = open('path') info ...

  2. tomcat BIO 、NIO 、AIO

    11.11活动当天,服务器负载过大,导致部分页面出现了不可访问的状态.那后来主管就要求调优了,下面是tomcat bio.nio.apr模式以及后来自己测试的一些性能结果. 原理方面的资料都是从网上找 ...

  3. shell脚本,如何监控目录下的文件内容是否被修改。

    第一种方法是通过cmp来进行比对[root@localhost bo]# ls .html .html .html .html .html .html .html .html .html cat.sh ...

  4. Python 网络爬虫干货总结

    Python 网络爬虫干货总结 爬取 对于爬取来说,我们需要学会使用不同的方法来应对不同情景下的数据抓取任务. 爬取的目标绝大多数情况下要么是网页,要么是 App,所以这里就分为这两个大类别来进行了介 ...

  5. django 12天(跨域,文件上传,下载,cookie,session)

    django 12天(跨域,文件上传,下载) 跨域 什么是跨域 1.协议不同 2.端口不同 3.主机不同 如何解决跨域 1.安装django-cors-headers模块 2.在settings.py ...

  6. Day12装饰器

    1.装饰器 什么是装饰器:装饰器指的是为被装饰对象添加新功能的工具 装饰器本身可以是任意调用对象 被装饰对象本身也可以是任意可调用对象 2.为何要用装饰器: 开放封闭原则: ①对修改源代码和调用方式是 ...

  7. opencv中相关的矩阵运算

    一.矩阵Mat I,img,I1,I2,dst,A,B;double k,alpha;Scalar s;1.加法I=I1+I2;//等同add(I1,I2,I);add(I1,I2,dst,mask, ...

  8. JQuery中点击超链接动态修改url连接地址无效

    这篇随笔的标题真是好拗口,想表达的意思是,当点击超链接后,才去修改超链接的地址,此时超链接仍然链接的是是修改之前的页面,而不是修改之后的页面. 超链接代码如下: <a id="chao ...

  9. adb 常用命令详解

    1.把电脑上文件或目录copy到手机中:adb push <local> <remote>    - copy file/dir to device 此处的<local& ...

  10. yum安装的java配置

    vim /etc/profile JAVA_HOME=/usr/lib/jvm/javaJRE_HOME=$JAVA_HOME/jreCLASS_PATH=.:$JAVA_HOME/lib/dt.ja ...