zoukankan      html  css  js  c++  java
  • golang ----并发 && 并行

     Go 语言的线程是并发机制,不是并行机制。

    那么,什么是并发,什么是并行?

    并发是不同的代码块交替执行,也就是交替可以做不同的事情。

    并行是不同的代码块同时执行,也就是同时可以做不同的事情。

    举个生活化场景的例子:

    你正在家看书,忽然电话来了,然后你接电话,通话完成后继续看书,这就是并发,看书和接电话交替做。

    如果电话来了,你一边看书一遍接电话,这就是并行,看书和接电话一起做。

    先看实例

    package main
    
    import (
    	"fmt"
    	"runtime"
    	"sync"
    )
    
    func main() {
    	runtime.GOMAXPROCS(1)
    	wg := sync.WaitGroup{}
    	wg.Add(20)
    	for i := 0; i < 10; i++ {
    		go func() {
    			fmt.Println("go routine 1 i: ", i)
    			wg.Done()
    		}()
    	}
    	for i := 0; i < 10; i++ {
    		go func(i int) {
    			fmt.Println("go routine 2 i: ", i)
    			wg.Done()
    		}(i)
    
    	}
    	wg.Wait()
    }
    

      输出:

    go routine 2 i: 9
    
    go routine 1 i: 10
    
    go routine 1 i: 10
    
    go routine 1 i: 10
    
    go routine 1 i: 10
    
    go routine 1 i: 10
    
    go routine 1 i: 10
    
    go routine 1 i: 10
    
    go routine 1 i: 10
    
    go routine 1 i: 10
    
    go routine 1 i: 10
    
    go routine 2 i: 0
    
    go routine 2 i: 1
    
    go routine 2 i: 2
    
    go routine 2 i: 3
    
    go routine 2 i: 4
    
    go routine 2 i: 5
    
    go routine 2 i: 6
    
    go routine 2 i: 7
    
    go routine 2 i: 8
    

     为什么会是这样的结果?

    并发不等于并行

    golang的核心开发人员Rob Pike专门提到了这个话题(有兴趣可以看这个视频或者看原文PPT)

    虽然我们在for循环中使用了go 创建了一个goroutine,我们想当然会认为,每次循环变量时,golang一定会执行这个goroutine,然后输出当时的变量。 这时,我们就陷入了思维定势。 默认并发等于并行。

    诚然,通过go创建的goroutine是会并发的执行其中的函数代码。 但一定会按照我们所设想的那样每次循环时执行吗? 答案是否定的!

    Rob Pike专门提到了golang中并发指的是代码结构中的某些函数逻辑上可以同时运行,但物理上未必会同时运行。而并行则指的就是在物理层面也就是使用了不同CPU在执行不同或者相同的任务。

    golang的goroutine调度模型决定了,每个goroutine是运行在虚拟CPU中的(也就是我们通过runtime.GOMAXPROCS(1)所设定的虚拟CPU个数)。 虚拟CPU个数未必会和实际CPU个数相吻合。每个goroutine都会被一个特定的P(虚拟CPU)选定维护,而M(物理计算资源)每次回挑选一个有效P,然后执行P中的goroutine。

    每个P会将自己所维护的goroutine放到一个G队列中,其中就包括了goroutine堆栈信息,是否可执行信息等等。默认情况下,P的数量与实际物理CPU的数量相等。因此当我们通过循环来创建goroutine时,每个goroutine会被分配到不同的P队列中。而M的数量又不是唯一的,当M随机挑选P时,也就等同随机挑选了goroutine。

    在本题中,我们设定了P=1。所以所有的goroutine会被绑定到同一个P中。 如果我们修改runtime.GOMAXPROCS的值,就会看到另外的顺序。 如果我们输出goroutine id,就可以看到随机挑选的效果:

    package main
    
    import (
    	"fmt"
    	"runtime"
    	"strconv"
    	"strings"
    	"sync"
    )
    
    func main() {
    	wg := sync.WaitGroup{}
    	wg.Add(20)
    	for i := 0; i < 10; i++ {
    		go func() {
    			var buf [64]byte
    			n := runtime.Stack(buf[:], false)
    
    			idField := strings.Fields(strings.TrimPrefix(string(buf[:n]), "goroutine "))[0]
    
    			id, err := strconv.Atoi(idField)
    			if err != nil {
    				panic(fmt.Sprintf("cannot get goroutine id: %v", err))
    			}
    			fmt.Printf("go routine 1 : i -- %d  id-- %d
     ", i, id)
    			wg.Done()
    		}()
    	}
    	for i := 0; i < 10; i++ {
    		go func(i int) {
    			var buf [64]byte
    			n := runtime.Stack(buf[:], false)
    			idField := strings.Fields(strings.TrimPrefix(string(buf[:n]), "goroutine "))[0]
    			id, err := strconv.Atoi(idField)
    			if err != nil {
    				panic(fmt.Sprintf("cannot get goroutine id: %v", err))
    			}
    			fmt.Printf("go routine 2 : i -- %d  id-- %d 
    ", i, id)
    			wg.Done()
    		}(i)
    
    	}
    	wg.Wait()
    }
    

      输出:

    go routine 1 : i -- 10  id-- 20
     go routine 1 : i -- 10  id-- 19
     go routine 1 : i -- 10  id-- 22
     go routine 1 : i -- 10  id-- 25
     go routine 1 : i -- 10  id-- 28
     go routine 2 : i -- 8  id-- 37 
    go routine 1 : i -- 10  id-- 23
     go routine 1 : i -- 10  id-- 21
     go routine 2 : i -- 0  id-- 29 
    go routine 2 : i -- 9  id-- 38 
    go routine 2 : i -- 1  id-- 30 
    go routine 2 : i -- 5  id-- 34 
    go routine 2 : i -- 2  id-- 31 
    go routine 2 : i -- 6  id-- 35 
    go routine 2 : i -- 3  id-- 32 
    go routine 1 : i -- 10  id-- 24
     go routine 2 : i -- 7  id-- 36 
    go routine 1 : i -- 10  id-- 27
     go routine 2 : i -- 4  id-- 33 
    go routine 1 : i -- 10  id-- 26
    

     

    我们再回到这道题中,虽然在循环中通过go定义了一个goroutine。但我们说到了,并发不等于并行。因此虽然定义了,但此刻不见得就会去执行。需要等待M选择P之后,才能去执行goroutine。 关于golang中goroutine是如何进行调度的(GPM模型),可以参考Scalable Go Scheduler Design Doc或者LearnConcurrency

    这时应该就可以理解为什么会先输出goroutine2然后再输出goroutine1了吧。

    下面我们来解释为什么goroutine1中输出的都是10.

    goroutine如何绑定变量

    在golang的for循环中,golang每次都使用相同的变量实例(也就是题中所使用的i)。 而golang之间是共享环境变量的。

    当调度到这个goroutine时,它就直接读取所保存的变量地址,此时就会出现一个问题:goroutine保存的只是变量地址,所以变量是有可能被修改的

    再结合题中的for循环,每次使用的都是同一个变量地址,也就是说i每次都在变化,到循环结束之时,i就变成了10. 而goroutine中保存的也只有i的内存地址而已,所以当goroutine1执行时,毫不犹豫的就把i的内容读了出来,多少呢? 10!

    但为什么goroutine2不是10呢?

    反过来看goroutine2,就容易理解了。因为在每次循环中都重新生成了一个新变量,然后每个goroutine保存的是各自新变量的地址。 这些变量相互之间互不干扰,不会被任何人所篡改。因此在输出时,会从0 - 9依次输出。

     

  • 相关阅读:
    在数组中的两个数字如果前面一个数字大于后面的数字, 则这两个数字组成一个逆序对。 输入一个数组,求出这个数组中的逆序对的总数
    输入一个正整数数组,把数组里所有数字拼接起来排成一个数。打印能拼接出所有数字中最小的一个
    输入一个整型数组,数组里有正数,也有负数。求所有子数组的和的最大值
    数组中有一个数字出现的次数超过数组长度的一半
    输入一个字符串,打印出该字符串中字符的所有排列
    输入一颗二叉搜索树,将该二叉搜索树转换成一个排序的双向链表
    输入一颗二叉树和一个整数,打印出二叉树中结点值的和为输入整数的所有路径
    判断某数组是否是某二叉搜索树的后序遍历的结果
    栈的压人、弹出序列
    Valid Number
  • 原文地址:https://www.cnblogs.com/saryli/p/11645879.html
Copyright © 2011-2022 走看看