Go36-16,17-goroutine

go语句及其执行规则

学习之前先看一下下面这句话:

Don’t communicate by sharing memory; share memory by communicating.
不要通过共享数据来通讯,要以通讯的方式共享数据。

通道(也就是 channel)类型的值可以被用来以通讯的方式共享数据。更具体地说,它一般被用来在不同的goroutine之间传递数据。
这篇主要讲goroutine是什么。简单来说,goroutine代表着并发编程模型中的用户级线程。

调度器

Go语言不但有着独特的并发编程模型,以及用户级线程goroutine,还拥有强大的用于调度goroutine、对接系统级线程的调度器。
这个调度器是Go语言运行时系统的重要组成部分,它主要负责统筹调配Go并发编程模型中的三个主要元素:

  • G(goroutine 的缩写),用户级线程
  • P(processor 的缩写),一种可以承载若干个G,且能够使用这些G适时的与M进行对接,并得到真正运行的中介
  • M(machine 的缩写),系统级线程

主goroutine

这里需要知道一个与主goroutine有关的重要特性,一旦主goroutine中的代码(也就是main函数中的那些代码)执行完毕,当前的 Go 程序就会结束运行。
先看下面这个例子:

package main

import "fmt"

func main() {
    for i := 0; i < 10; i++ {
        go func() {
            fmt.Println(i)
        }()
    }
}

上面的程序运行之后,不会有打印任何内容。
只要go语句本身执行完毕,Go程序完全不会等待go函数的执行,它会立刻去执行后面的语句。这就是所谓的异步并发地执行。
在上面的例子中,在for语句执行完毕后,里面包装的10个goroutine还没有获得运行的机会,主goroutine中的代码执行完了,Go程序就会立即结束运行。

使用Sleep等待

上面的例子中,如果要让程序在其他goroutine运行完之后再退出。最简单粗暴的办法是Sleep一段时间:

package main

import (
    "fmt"
    "time"
)

func main() {
    for i := 0; i < 10; i++ {
        go func() {
            fmt.Println(i)
        }()
    }
    time.Sleep(time.Second)
}

这个办法可行,但是Sleep的时间需要预估。太长会浪费时间,太短则不能保证所有goroutine都运行完毕。不容易预估时间,最好是让其他的goroutine在运行完毕后发送通知。

让主goroutine等待其他goroutine

使用通道,通道的长度与启用的goroutine的数量一致。每个goroutine运行完毕前,都向通道发送一个值。在主goroutine则是从这个通道接收值,接收了足够数量的次数后就说明所有goroutine都运行完毕了,可以继续往下执行了(就是退出):

package main

import "fmt"

func main() {
    sign := make(chan struct{}, 10)
    for i := 0; i < 10; i++ {
        go func() {
            fmt.Println(i)
            sign <- struct{}{}
        }()
    }
    for j := 0; j < 10; j++ {
        <- sign
    }
}

这里声明的通道的类型是 chan struct{} ,是一个空结构体。它谭勇的内存空间是0字节。这个值在整个Go程序中永远都只会存在一份。虽然可以无数次的使用这个值字面量,但是用到的都是同一个值。当把通道仅仅刀座是传递某个简单信号的介质的时候,使用空结构体是最好的。
其他方式
在标准库中,有一个sync包,里面有一个sync.WaitGroup类型。这应该是一个更好的实现方式。不过这要等后面讲sync包的时候再说了。

让多个goroutine按照既定的顺序执行

首先改造一下一只使用的例子,把变量i的值传递给每个goroutine,这样输出的是0-9各一次,不过是乱序的:

for i := 0; i < 10; i++ {
    go func(i int) {
        fmt.Println(i)
        sign <- struct{}{}
    }

讲师的例子

package main

import (
    "fmt"
    "sync/atomic"
    "time"
)

var count uint32

func trigger (i uint32, fn func()) {
    for {
        if n := atomic.LoadUint32(&count); n == i {
            fn()
            atomic.AddUint32(&count, 1)
            break
        }
        time.Sleep(time.Nanosecond)
    }
}

func main() {
    for i := uint32(0); i < 10; i++ {
        go func(i uint32) {
            fn := func() {
                fmt.Println(i)
            }
            trigger(i, fn)
        }(i)
    }
    trigger(10, func() {})
}

主要就是trigger函数。在trigger里会检查i,并把要执行的语句打包成fn函数也传入,只有在trigger里判断后符合条件,就会执行fn函数的语句。
trigger里会检查i和count是否相等,在执行了fn函数后,需要把count加1,这里用了原子操作。里有是trigger函数会被多个goroutine并发的调用,所以这个变量被多个用户级线程共用了。因此对它的操作就产生了竞态条件(race condition),破坏了程序的并发安全性。
在最后退出的时候,应该有了trigger函数,只要检查count是否到10了,就表示其他goroutine都执行完了,所以也就不需要通道了。
另外在trigger函数里,是一个for语句的无限循环,在判断条件不成立后,先进行了一个1纳秒的Sleep。如果不加这句的话,测试下来,偶尔会出现程序卡住的情况(甚至是死机)。这里加上Sleep语句应该是希望这个时候程序可以进行一下切换,否则当前应该执行的那个goroutine如果拿不到执行的机会,其他goroutine也都无法通过if条件的判断。

自己的实现

package main

import (
    "fmt"
    "time"
)

func main() {
    sign := make(chan struct{}, 10)
    var count int
    for i := 0; i < 10; i++ {
        go func(i int) {
            for {
                if count == i{
                    fmt.Println(i)
                    count ++
                    sign <- struct{}{}
                    break
                }
                time.Sleep(time.Nanosecond)
            }
        }(i)
    }
    for j := 0; j < 10; j++ {
        <- sign
    }
}

主要两个问题,当时没有意识到在for无限循环之后,进入下一个迭代前,这个1纳秒Sleep的意义。还有就是我没有使用原子操作。不过这里即使不用原子操作也没问题的样子,因为逻辑上通知只有一个goroutine满足条件会去操作共用的变量count。所以这里和上面讲师的示例就差在对变量count的比较和判断是否是原子操作的问题上了。

原子操作
这里再自我做一些补充。

原子操作,即执行过程不能被中断的操作(并发)。
经典问题:i++是不是原子操作?
答案是否,因为i++看上去只有一行,但是背后包括了多个操作:取值,加法,赋值。

原文地址:http://blog.51cto.com/steed/2342296

时间: 2024-10-09 05:36:15

Go36-16,17-goroutine的相关文章

《构建之法》13,14,15,16,17章读后感

1.13章说的是软件测试,怎么样去测试是最有效率的? 2.14章说到质量保障,具体的花费是多少? 3.15章说到ZBB,如果一个软件遇到了不可修复的bug,还算是一个稳定的软件么? 4.16章说到创新,有实际例子吗? 5.17章的职业道德指的是什么?

16.17

1 import java.awt.Graphics; 2 import java.awt.event.*; 3 import javax.swing.*; 4 5 public class Test_16_17 extends JFrame{ 6 7 public Test_16_17(){ 8 add(new JP()); 9 } 10 public static void main(String[] args) { 11 // TODO Auto-generated method stub

阅读第13,14,15,16,17章

13章:效能测试 不一样的机器运行软件效率是不一样的,怎么能做到效能测试的权威性 14章:软件质量保障 这是一个很重要的问题,国外公司很注重维护,中国公司也注重更新维护吗? 15章:稳定和发布阶段 软件发布后,逐步冻结为什么要从人机界面开始冻结 16章 IT行业的创新 创新者都是一马当先,为什么美国易贝网站最先在互联网世界提出用户对用户交易平台,然而在中国却基本听不懂易贝交易平台 17章 读一个程序猿的生命周期 这让我懂得人生不管干什么方向很重要,更重要的是要手上有实力

2019.10.16&amp;17小结

话说也蛮久没写小结了,主要这两次考试失分严重,还是总结下吧. 10.16 T1 小奇挖矿2 100/0 [题目背景] 小奇飞船的钻头开启了无限耐久+精准采集模式!这次它要将原矿运到泛光之源的矿石交易市场,以便为飞船升级无限非概率引擎. [问题描述] 现在有m+1个星球,从左到右标号为0到m,小奇最初在0号星球. 有n处矿体,第i处矿体有ai单位原矿,在第bi个星球上. 由于飞船使用的是老式的跳跃引擎,每次它只能从第x号星球移动到第x+4号星球或x+7号星球.每到一个星球,小奇会采走该星球上所有的

一张表里面有ID自增主键,当insert了17条记录之后,删除了第15,16,17条记录,再把mysql重启,再insert一条记录,这条记录的ID是18还是15 ?

转:https://blog.csdn.net/xzp_12345/article/details/79458586 一般情况下,我们创建的表的类型是InnoDB,如果新增一条记录(不重启mysql的情况下),这条记录的id是18:但是如果重启(文中提到的)MySQL的话,这条记录的ID是15.因为InnoDB表只把自增主键的最大ID记录到内存中,所以重启数据库或者对表OPTIMIZE操作,都会使最大ID丢失. 但是,如果我们使用表的类型是MylSAM,那么这条记录的ID就是18.因为MylSA

第16&amp;17题 Remove Duplicates from Sorted List

Remove Duplicates from Sorted List I Given a sorted linked list, delete all duplicates such that each element appear only once. For example, Given 1->1->2, return 1->2. Given 1->1->2->3->3, return 1->2->3. Solution: <span sty

15 16 17 神器的print函数 py赋值 布尔(true false)

第一课 神奇的print函数 # coding:utf-8 # 神奇的print函数 支持可变参数 print("hello world") #结果为 hello world print("a","b","c","d","e") #结果为 a b c d e #name = Bill print("name =","Bill") #结果为 name

java 22 - 17 多线程之等待唤醒机制(接16)

先来一张图,看看什么叫做等待唤醒机制 接上一章的例子. 例子:学生信息的录入和获取 * 资源类:Student  * 设置学生数据:SetThread(生产者) * 获取学生数据:GetThread(消费者) * 测试类:StudentDemo * 资源类:Student (为了使用等待唤醒机制,添加了个布尔类型的变量,默认为flase) 1 public class Student { 2 String name; 3 int age; 4 boolean flag; // 默认情况是没有数据

NOIP模拟 17.8.16

A 债务文件名 输入文件 输出文件 时间限制 空间限制debt.pas/c/cpp debt.in debt.out 1s 128MB[题目描述]小 G 有一群好朋友,他们经常互相借钱.假如说有三个好朋友 A,B,C.A欠 B 20 元,B 欠 C 20 元,总债务规模为 20+20=40 元.小 G 是个追求简约的人,他觉得这样的债务太繁杂了.他认为,上面的债务可以完全等价为 A 欠 C 20 元,B 既不欠别人,别人也不欠他.这样总债务规模就压缩到了 20 元.现在给定 n 个人和 m 条债

Java语法基础常见疑惑解答8,16,17,21图片补充

8. 16. 17. 21 原文地址:https://www.cnblogs.com/zzstdruan1707-4/p/9744448.html