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\n ", 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 \n", 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依次输出。

 

golang ----并发 && 并行的更多相关文章

  1. 马蜂窝搜索基于 Golang 并发代理的一次架构升级

    搜索业务是马蜂窝流量分发的重要入口.很多用户在使用马蜂窝时,都会有目的性地主动搜索与自己旅行需求相关的各种信息,衣食住行,事无巨细,从而做出最符合需求的旅行决策. 因此在马蜂窝,搜索业务交互的下游模块 ...

  2. 消息/事件, 同步/异步/协程, 并发/并行 协程与状态机 ——从python asyncio引发的集中学习

    我比较笨,只看用await asyncio.sleep(x)实现的例子,看再多,也还是不会. 已经在unity3d里用过coroutine了,也知道是“你执行一下,主动让出权限:我执行一下,主动让出权 ...

  3. Golang并发原理及GPM调度策略(一)

    其实从一开始了解到go的goroutine概念就应该想到,其实go应该就是在内核级线程的基础上做了一层逻辑上的虚拟线程(用户级线程)+ 线程调度系统,如此分析以后,goroutine也就不再那么神秘了 ...

  4. Golang - 并发编程

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

  5. golang并发编程

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

  6. 多线程爬坑之路--并发,并行,synchonrized同步的用法

    一.多线程的并发与并行: 并发:多个线程同时都处在运行中的状态.线程之间相互干扰,存在竞争,(CPU,缓冲区),每个线程轮流使用CPU,当一个线程占有CPU时,其他线程处于挂起状态,各线程断续推进. ...

  7. Golang 并发简介

    并发概要 随着多核CPU的普及, 为了更快的处理任务, 出现了各种并发编程的模型, 主要有以下几种: 模型名称 优点 缺点 多进程 简单, 隔离性好, 进程间几乎无影响 开销最大 多线程 目前使用最多 ...

  8. golang并发(1)介绍

    概述 简而言之,所谓并发编程是指在一台处理器上“同时”处理多个任务. 随着硬件的发展,并发程序变得越来越重要.Web服务器会一次处理成千上万的请求.平板电脑和手机app在渲染用户画面同时还会后台执行各 ...

  9. golang 并发顺序输出数字

    参考 package main import ( "fmt" "sync/atomic" "time" ) func main() { va ...

随机推荐

  1. Linux shell case条件判断及位置变量

    case语句使用于需要进行多重分支的应用情况 case分支判断结构 语法: case 变量名称 in      value1)          statement          statemen ...

  2. 【若泽大数据】玩转大数据之Spark零基础到实战

    https://www.bilibili.com/video/av29407581?p=1 若泽大数据官网 http://www.ruozedata.com/ tidb 系列三:有了sparkjdbc ...

  3. Go Programming Language

    [Go Programming Language] 1.go run %filename 可以直接编译并运行一个文件,期间不会产生临时文件.例如 main.go. go run main.go 2.P ...

  4. python基本数据类型的时间复杂度

    1.list 内部实现是数组 2.dict 内部实现是hash函数+哈希桶.一个好的hash函数使到哈希桶中的值只有一个,若多个key hash到了同一个哈希桶中,称之为哈希冲突. 3.set 内部实 ...

  5. axios 下载文件

    axio请求里必须加  responseType: 'blob' 参数,如下 //下载文件 api.download=function(id) { return request({ url: this ...

  6. scrapy 改 scrapy-redis

    1.spider 修改 class CgysSpider(scrapy.Spider): name = 'clispider' start_urls = ['https://search.bilibi ...

  7. linux 利用 crontab 实现 程序开机启动/crontab任务的多种实现方法

    方法一,用户登录服务器,直接修改: crontab -e 然后添加: @reboot [nohup] {命令} ctrl + O ctrl + x 方法二,指定用户进行修改: sudo crontab ...

  8. oneinstack——证书更新

    如果你安装好 oneinstack 你的定时任务就会自动添加 "/root/.acme.sh"/acme.sh --cron --home "/root/.acme.sh ...

  9. vector中erase()与insert()用法

    erase()用法:https://blog.csdn.net/duan19920101/article/details/50717748 注:erase是删除指定位置的元素,不能删除给定元素值.若要 ...

  10. svg形状相关的学习(二)

    _ 阅读目录 一:线段 二:笔画特性 1. stroke-width 2. stroke-opacity 3. stroke-dasharray 属性 三:常见的形状 1. 矩形 2. 圆角矩形 3. ...