zoukankan      html  css  js  c++  java
  • golang并发编程goroutine+channel(一)

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

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

    案例一:并发编程

    func say(s string) {
    	fmt.Printf("%s say
    ", 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
    ", 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
    ", 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
    ", 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
    ", 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
    ", 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
    ", name)
    	stu := Stu{Name: name, Age: 18}
    	return stu
    }
    func main() {
    	c := make(chan int)
    	go func() {
    		stu := say("lisi") //返回一个学生实体
    		fmt.Printf("我叫%s,年龄%d
    ", 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
    ", s)
    	//c <- 1 这里本来应该给c管道传值的,结果没传
    }
    func main() {
    	c := make(chan int)
    	go say("lisi", c)
    	v1 := <-c //这里会一直阻塞,导致死锁
    	fmt.Printf("lisi:%d
    ", 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
    ", 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",网上资料比较少了解不多。

  • 相关阅读:
    Oracle之:Function :dateToNumber()
    Oracle之:Function :getcurrdate()
    Oracle之:Function :getdate()
    Hadoop-No.15之Flume基于事件的数据收集和处理
    Hadoop-No.14之文件传输的特点
    Hadoop-No.13之数据源系统以及数据结构
    Hadoop-No.12之数据采集的时效性
    Hadoop-No.11之元数据
    Hadoop-No.10之列簇
    Hadoop-No.9之表和Region
  • 原文地址:https://www.cnblogs.com/nickchou/p/8548234.html
Copyright © 2011-2022 走看看