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

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

又有问题了!

  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")
}

错误示范:死锁

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) //前面死锁,这里无法输出
}

执行报错内容:

goroutine简析:

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

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

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) }

再次改版下

c := make(chan int)    //重点,这里改成无缓冲的
结论:明显无缓冲区耗时多了接近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();
}
}
04-28 23:03