加入收藏 | 设为首页 | 会员中心 | 我要投稿 北几岛 (https://www.beijidao.com.cn/)- 科技、建站、经验、云计算、5G、大数据,站长网!
当前位置: 首页 > 大数据 > 正文

第九章 goroutine

发布时间:2021-07-06 06:55:48 所属栏目:大数据 来源: https://www.jb51.cc
导读:@H_404_0@接下来学习并发编程,并发编程是go语言最有特色的地方,go对并发编程是原生支持. @H_404_0@goroutine是go中最近本的执行单元 @H_404_0@每一个go程序至少有一个goroutine,那就是主goroutine. 当程序启动时,他会自动创建. 也就是main方法 @H_404_0@main
@H_404_0@接下来学习并发编程,并发编程是go语言最有特色的地方,go对并发编程是原生支持.

@H_404_0@goroutine是go中最近本的执行单元

@H_404_0@每一个go程序至少有一个goroutine,那就是主goroutine. 当程序启动时,他会自动创建. 也就是main方法

@H_404_0@main方法也是一个goroutine

@H_404_0@?

一. 如何定义一个协程.?

package main

import (
    "fmt"
    time"
)

func main() {
    for i := 0; i<1000; i++ {
        go func(i int) {
            for   {
                fmt.Printf("goroutine: %d n",i)
            }
        }(i)
    }

    time.Sleep(time.Second)
}
  • 定义协程使用go 关键字.

二. 对goroutine的理解

@H_404_0@goroutine和Coroutine比较相似,Coroutine是协程. 其他语言都有这个叫法,但不是所有语言都支持.

  • 协程和线程的区别
    • 线程(Thread):有时被称为轻量级进程(Lightweight Process,LWP),是程序执行流的最小单元。一个标准的线程由线程ID,当前指令指针(PC),寄存器集合和堆栈组成。另外,线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。
@H_404_0@  线程拥有自己独立的栈和共享的堆,共享堆,不共享栈,线程的切换一般也由操作系统调度。

    • 协程(Coroutine): 是一种轻量级的"线程",作用是处理并发任务,这一点和线程差不多. 协程为什么是轻量级的线程呢?
      • 非抢占式多任务处理,由协程主动交出控制权. 对比线程,线程随时都有可能被cpu切换,线程是抢占式任务处理. 我们是没有控制权的. 任务执行一半,操作系统有可能就切换到另一个线程去了. 然后还有可能在切换回来. 那么这样切换的时候,就要考虑保存切换前的状态. 协程不同,协程是非抢占式的,什么时候交出控制权,由协程自己说了算. 因为是非抢占式,所以不用存储那么多状态,节省了很多资源
      • 编译器/解释权/虚拟机层面的多任务. 线程是操作系统层面的多任务. 而协程是虚拟机,编译器,解释器层面的多任务.? 在go语言中,协程可以看作是编译器级别的多任务. 编译器会把一个go func解释为一个协程. 具体在执行上,go语言后面会有一个调度器. (操作系统有一个调度器,go语言还有自己的调度器)
      • 多个协程可以在一个或多个线程上运行. 这是由go调度器决定的
@H_404_0@?

1. 非抢占式多任务处理

@H_404_0@  

var a [10]int
    10; i++ {
        go func(i int) {
            for   {
                a[i] ++
            }
        }(i)
    }

    time.Sleep(time.Second)
    fmt.Println(a)
}
@H_404_0@猜一下,这段代码的运行结果. 结合非抢占式多任务处理

@H_404_0@结果是: 这段代码是一个死循环. 当第一次进入到循环体以后. 由于goroutine是非抢占式,所以第一次循环一直持有,没有主动释放. 所以,这段代码的结果是死循环

@H_404_0@?

2. 手动交出控制权  

runtime.Gosched()
@H_404_0@这样就可以手动交出控制权,让其他协程运行

@H_404_0@?

3. race condition 数据访问冲突

@H_404_0@如果我们在协程中没有传变量i会怎么样呢?

@H_404_0@

@H_404_0@?

@H_404_0@?

@H_404_0@没错,报错了. 为什么报错了呢? 我们通过race 来看一下?

go run -race goroutine.go
@H_404_0@

@H_404_0@?

@H_404_0@?

@H_404_0@可以看到报错的原因是,同一块空间,在第七个协程读,在主协程写. 这样就是有问题的了.

@H_404_0@?

@H_404_0@接下来分析一下这段代码为什么报错?

runtime0; i <  {
        go func() {
            for   {
                a[i] ++
                runtime.Gosched()
            }
        }()
    }

    time.Sleep(time.Second)
    fmt.Println(a)
}
@H_404_0@程序启动的时候都做了哪些事?

@H_404_0@首先. 开了10个协程. i从1遍历到10,发现10 < 10,for循环退出了. 但是,由于协程里面的i是直接饮用的外部的i. 当for循环完成以后,i的值变成10了. 协程里对a[10]进行++,那自然就会报异常了.?

@H_404_0@所以,为了安全起见,我们把每一次开协程的时候,把i带过去.

@H_404_0@修改后的

func main() {
    go func(i int) {
            for   {
                a[i] ++
                runtime.Gosched()
            }
        }(i)
    }

    time.Sleep(time.Second)
    fmt.Println(a)
}
@H_404_0@这时候我们在-race一下,查看是否还有数据访问冲突?

@H_404_0@?

@H_404_0@?

@H_404_0@?

@H_404_0@依然有数据访问冲突. 主goroutine在读,第7个协程在写. 所以这样是有问题的. 这个问题可以通过chan来解决.

4. 子程序是协程的一个特例

@H_404_0@

@H_404_0@?

@H_404_0@?我们知道每一个函数都是一个子程序,子程序是协程的一个speical case,那怎样才算是一个special case呢?

普通函数和协程的区别

@H_404_0@

@H_404_0@?

@H_404_0@  ?1) 普通函数的调用: 首先main方法启动,main方法里调了另一个doWork方法. 当doWork方法都执行完了以后,在继续回到main方法里,一次往下执行. 所以普通的函数是单线程.

@H_404_0@  ?2) 协程的调用: 协程也是main和doWork,? 但是main和doWork之间不是单向的箭头. 中间有一个双向的通道.

@H_404_0@    main和dowork之间可以双向的流通. 控制权也可以双向的流通.就像两个线程,各做各的事情,中间还可以通信,控制权可以相互交换.

@H_404_0@    那么main和dowork运行在哪里呢?

@H_404_0@    可能是一个线程,也可能是多个线程. 这个事情不需要程序员管了,调度器可能开一个线程,也可能开两个线程进行执行.?

@H_404_0@?

5. go语言的协程

@H_404_0@  

@H_404_0@?

@H_404_0@?

@H_404_0@?

@H_404_0@  1) 首先有一个go语言的进程,他下面会有一个调度器,调度器的作用就是调度协程?

@H_404_0@  2) 调度器会分配,一个协程在一个线程里运行,也可能是两个协程在一个线程里运行,也可能是多个协程在一个线程里运行. 这是调度器做的事,程序员不用管.

6. 协程的定义

@H_404_0@  

@H_404_0@?

@H_404_0@?

@H_404_0@?  1) 在函数前加go,就可以交给调度器运行

@H_404_0@  2) 不需要再定义时区分是否是异步函数. 这个是相对于python来说的

@H_404_0@  3) 调度器在合适的点进行切换. 由调度器操作执行,一般不需要我们来操作

@H_404_0@  4)使用-race来检测数据访问冲突. 这个在上面已经讲过了.

@H_404_0@?

7. goroutine可能切换的点

@H_404_0@  

@H_404_0@?

@H_404_0@?

@H_404_0@?

@H_404_0@  调度器在哪些个点有可能切换协程呢?

@H_404_0@  1. I/O,select : I/O和select可能会切换. 之前fmt.Println("")为什么会切换呢? 因为他是一个I/O

@H_404_0@  2) channel

@H_404_0@  3) 等待锁

@H_404_0@  4) 函数调用(有时)

@H_404_0@  5) runtime.Gosched() 手动提供切换的点

@H_404_0@总结: 以上只是一个参考,不能保证遇到这些地方一定切换,也不能保证其他地方就不切换.

8. 观察启动1000个协程,我们的系统分配了多少个线程.

0; i < 1000; i++   {
                fmt.Printf(hello goroutine: %d n,i)
            }
        }(i)
    }

    time.Sleep(time.Minute)
}
@H_404_0@上面这段程序,看重点

@H_404_0@  1) 开了1000个协程

@H_404_0@  2) 主线程等待1分钟

@H_404_0@?

@H_404_0@我们top一下,看看效果

@H_404_0@

@H_404_0@?

@H_404_0@?

@H_404_0@红色圈出的是go运行的程序

@H_404_0@cpu的占用率是186.6%,? 12/3 表示开了多少个线程. 我们发现开了12个线程,但是最终运行的线程数最多是4个. 原因是我的cpu是4核的.

@H_404_0@?

(编辑:北几岛)

【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!

    推荐文章
      热点阅读