广告
返回顶部
首页 > 资讯 > 后端开发 > GO >go语言中的协程详解
  • 699
分享到

go语言中的协程详解

2024-04-02 19:04:59 699人浏览 泡泡鱼
摘要

协程的特点 1.该任务的业务代码主动要求切换,即主动让出执行权限 2.发生了io,导致执行阻塞(使用channel让协程阻塞) 与线程本质的不同 C#、java中我们执行多个线程,是

协程的特点

1.该任务的业务代码主动要求切换,即主动让出执行权限

2.发生了io,导致执行阻塞(使用channel让协程阻塞)

线程本质的不同

C#、java中我们执行多个线程,是通过时间片切换来进行的,要知道进行切换,程序需要保存上下文等信息,是比较消耗性能的

Go语言中的协程,没有上面这种切换,一定是通过协程主动放出权限,不是被动的。

例如:

C# 中创建两个线程

可以看到1和2是交替执行的

Go语言中用协程实现一下

runtime.GOMAXPROCS(1)

这个结果就是 执行了1 在执行2

上述两种方式来进行协程的切换

  • 1.该任务的业务代码主动要求切换,即主动让出执行权限

  • 2.通过channel进行阻塞

执行的结果一样

后面又继续执行1了

一、Goroutine的并行

package main

import (
  "fmt"
  "runtime"
  "sync"
)

func main() {
  runtime.GOMAXPROCS(1)
  var wg sync.WaitGroup
  wg.Add(3)
  fmt.Println("Start Goroutines")

  go func() {
    defer wg.Done()
    for i:=1;i<=10;i++{
      fmt.Print("1")
    }
  }()

  go func() {
    defer wg.Done()
    for i:=1;i<=10;i++ {
            fmt.Print("2")
    }
  }()

  go func() {
    defer wg.Done()
    for i:=1;i<=10;i++ {
           fmt.Print("3")
    }
  }()

  fmt.Println("等待执行结束")
  wg.Wait()
}

运行结果

Start Goroutines

等待执行结束

333333333311111111112222222222

(1)runtime.GOMAXPROCS(1) 的作用是什么?

runtime包的GOMAXPROCS 函数。这个函数允许程序更改调度器可以使用的逻辑处理器的数量。如果不想在代码里做这个调用,也可以通过修改和这个函数名字一样的环境变量的值来更改逻辑处理器的数量。

(2)为什么先输出3后输出1和2?

调度器源码中有体现: 有一个队列,还有一个优先执行——即最后一个, 在只有一个逻辑处理器的情况下,先执行优先的那个,再顺序执行队列中的。不过经过各种实验发现这个执行顺序其实是变的,貌似纠结这个顺序也没有什么意思。

(3)WaitGroup 是什么意思?

WaitGroup 是一个计数信号量,可以用来记录并维护运行的goroutine。如果WaitGroup的值大于0,Wait 方法就会阻塞。为了减小WaitGroup 的值并最终释放main 函数,使用defer 声明在函数退出时

调用Done 方法。(defer 有点像C#当中的fianlly)

补充:调度算法中,如果多个goroutine中某个执行过长,此时会将其停止让给其他goroutine继续执行,待到其他都执行完成,在将其进行执行。如下图:G4和G5就进行了切换

如下代码可以验证上面的问题:

package main

import (
    "fmt"
    "runtime"
    "sync"
)

var wg sync.WaitGroup

func main() {
    runtime.GOMAXPROCS(1)

    wg.Add(2)

    fmt.Println("Create Goroutines")
    go printPrime("A")
    go printPrime("B")

    fmt.Println("Waiting To Finish")
    wg.Wait()

    fmt.Println("Terminating Program")
}


func printPrime(prefix string) {
    defer wg.Done()

next:
    for outer := 2; outer < 5000; outer++ {
        for inner := 2; inner < outer; inner++ {
            if outer%inner == 0 {
                continue next
            }
        }
        fmt.Printf("%s:%d\n", prefix, outer)
    }
    fmt.Println("Completed", prefix)
}

printPrime 这个函数作用是查找显示 5000 以内的素数值,这是一个比较耗时的程序。

运行结果:数字比较多,不打印了,但是可以看到结果是A和B两个协程之间的切换。

上面的代码都是设置GOMAXPROCS为1的情况,给每个可用的核心分配一个逻辑处理器

runtime.GOMAXPROCS(runtime.NumCPU())

这样运行第一个程序的结果如下

Start Goroutines
等待执行结束133333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333332111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222

可以看到1、2、3没有什么规律。

二、Goroutine的竞争

什么竞争状态:如果两个或者多个goroutine 在没有互相同步的情况下,访问某个共享的资源,并试图同时读和写这个资源,就处于相互竞争的状态,这种情况被称作竞争状态(race candition)

竞争状态是比较容易出现问题的地方,所以对一个共享资源的读和写操作必须是原子化的,换句话说,同一时刻只能有一个goroutine 对共享资源进行读和写操作。

package main

import (
    "fmt"
    "runtime"
    "sync"
)

var (
    counter int
    wg sync.WaitGroup
)

func main() {
    wg.Add(2)

    go incCounter(1)
    go incCounter(2)

    wg.Wait()
    fmt.Println("Final Counter:", counter)
}

func incCounter(id int) {
    defer wg.Done()
    for count := 0; count < 2; count++ {
        value := counter
        runtime.Gosched()
        value++
        counter = value
    }
}

运行结果:

Final Counter: 2

变量 counter 会进行4 次读和写操作,每个goroutine 执行两次。但是,程序终止时,counter变量的值为2,这是因为两个协程之间产生了竞争,对同一个counter进行读写,下面这个图很好的诠释了为什么最后counter是2

补充:runtime.Gosched()用于让出CPU时间片。

三、使用Goroutine的

一种修正代码、消除竞争状态的办法是,使用Go 语言提供的锁机制,来锁住共享资源,

从而保证goroutine 的同步状态。Go 语言提供了传统的同步goroutine 的机制,就是对共享资源加锁。

1、原子函数

使用atomic包来提供对数值类型的安全访问。

package main

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

var (
    counter int64
    wg sync.WaitGroup
)

func main() {
    wg.Add(2)

    go incCounter(1)
    go incCounter(2)

    wg.Wait()

    fmt.Println("Final Counter:", counter)
}

func incCounter(id int) {
    defer wg.Done()
    for count := 0; count < 2; count++ {
        atomic.AddInt64(&counter, 1)
        runtime.Gosched()
    }
}

运行结果

Final Counter: 4

atmoic 包的AddInt64 函数。这个函数会同步整型值的加法,方法是强制同一时刻只能有一个goroutine 运行并完成这个加法操作。当goroutine 试图去调用任何原子函数时,这些goroutine 都会自动根据所引用的变量做同步处理。

另外两个有用的原子函数是LoadInt64 和StoreInt64。这两个函数提供了一种安全地读和写一个整型值的方式。

package main

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

var (
    shutdown int64
    wg sync.WaitGroup
)

func main() {
    wg.Add(2)

    go doWork("A")
    go doWork("B")

    time.Sleep(1 * time.Second)

    fmt.Println("Shutdown Now")
    atomic.StoreInt64(&shutdown, 1)
    wg.Wait()
}

func doWork(name string) {
    defer wg.Done()

    for {
        fmt.Printf("Doing %s Work\n", name)
        time.Sleep(250 * time.Millisecond)

        // Do we need to shutdown.
        if atomic.LoadInt64(&shutdown) == 1 {
            fmt.Printf("Shutting %s Down\n", name)
            break
        }
    }
}

主协程main函数执行了1秒,然后将shutdown中的值设置为1,A和B的协程中通过读取shutdown是否等于1来判断是否结束协程。

运行结果:

Doing A Work
Doing B Work
Doing A Work
Doing B Work
Doing A Work
Doing B Work
Doing A Work
Doing B Work
Shutdown Now
Shutting A Down
Shutting B Down

2、互斥锁

另一种同步访问共享资源的方式是使用互斥锁(mutex)。互斥锁这个名字来自互斥(mutualexclusion)的概念。互斥锁用于在代码上创建一个临界区,保证同一时间只有一个goroutine 可以执行这个临界区代码

package main

import (
    "fmt"
    "runtime"
    "sync"
)

var (
    counter int
    wg sync.WaitGroup
    mutex sync.Mutex
)

func main() {
    wg.Add(3)

    go incCounter(1)
    go incCounter(2)
    go incCounter(3)

    wg.Wait()
    fmt.Printf("Final Counter: %d\n", counter)
}

func incCounter(id int) {
    defer wg.Done()

    for count := 0; count < 2; count++ {
        mutex.Lock()
        {
            value := counter
            runtime.Gosched()
            value++
            counter = value
        }
        mutex.Unlock()
    }
}

运行结果:

Final Counter: 6

如上面的代码,最终的结果还是6.

mutex.lock he mutex.Unlock之间的代码,同一时刻只允许一个goroutine进入,所以保证数据的正确性。

补充对于 runtime.Gosched()的理解,有两个问题 1. 当执行了这句话,貌似让出了执行,那么它后面的语句还能执行么

到此这篇关于go语言协程的文章就介绍到这了。希望对大家的学习有所帮助,也希望大家多多支持编程网。

您可能感兴趣的文档:

--结束END--

本文标题: go语言中的协程详解

本文链接: https://www.lsjlt.com/news/164376.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

本篇文章演示代码以及资料文档资料下载

下载Word文档到电脑,方便收藏和打印~

下载Word文档
猜你喜欢
  • go语言中的协程详解
    协程的特点 1.该任务的业务代码主动要求切换,即主动让出执行权限 2.发生了IO,导致执行阻塞(使用channel让协程阻塞) 与线程本质的不同 C#、java中我们执行多个线程,是...
    99+
    2022-11-13
  • go语言中的协程实例分析
    这篇文章主要讲解了“go语言中的协程实例分析”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“go语言中的协程实例分析”吧!协程的特点该任务的业务代码主动要求切换,即主动让出执行权限发生了IO,...
    99+
    2023-07-02
  • Go语言中的闭包详解
    一、函数的变量作用域和可见性 1.全局变量在main函数执行之前初始化,全局可见 2.局部变量在函数内部或者if、for等语句块有效,使用之后外部不可见 3.全局变量和局部变量同名的...
    99+
    2022-11-13
  • Go语言中的包Package详解
    问题一、Go使用Package组织源码的好处是什么? 1.任何源码属于一个包 2.用包组织便于代码的易读和复用 问题二、Go语言中Package的种类 Go语言中存在两种包、一种是可...
    99+
    2022-11-13
  • Go语言中的函数详解
    1.函数的声明定义 //func关键字 //getStudent函数名 //(id int, classId int) 参数列表 //(name string,age int)...
    99+
    2022-11-13
  • go语言协程指的是什么
    这篇文章主要讲解了“go语言协程指的是什么”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“go语言协程指的是什么”吧!在go语言中,协程(goroutine)是指在后台中运行的轻量级执行线程;...
    99+
    2023-07-04
  • go语言限制协程并发数的方案详情
    目录前言一、使用带缓冲的通道限制并发数1.1方案详情1.2评估总结2.2评估总结其它前言 在使用协程并发处理某些任务时, 其并发数量往往因为各种因素的限制不能无限的增大. 例如网络请...
    99+
    2022-11-13
  • Go语言如何多开协程
    这篇文章主要讲解了“Go语言如何多开协程”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“Go语言如何多开协程”吧!Go语言中的goroutine,类似于线程,但是更轻量级、更高效。使用goro...
    99+
    2023-07-06
  • Go语言中容器的加载过程详解
    在Go语言中,容器是非常重要的概念。容器的概念来源于操作系统,它可以帮助我们管理多个进程和线程,并且提供了一个隔离的环境,使得不同的进程和线程之间不会互相干扰。在Go语言中,我们可以通过使用goroutine和channel来实现容器的管...
    99+
    2023-09-18
    关键字 load 容器
  • go语言中decimal的用法详解
    目录1. 精度丢失的case2. decimal的应用场景3. 使用decimal4. decimal其他实用的场景4.1 获取结果的整数部分4.2 小数点后填充4.3 比较数字的大...
    99+
    2023-03-07
    go语言decimal用法 go语言decimal
  • 详解go语言的并发
    目录1、启动go语言的协程2、runtime.Goexit()方法。立即终止当前的协程3、runtime.GOMAXPROCS()表示go使用几个cpu执行代码4、管道定义和创...
    99+
    2022-06-07
    详解go语言 GO 并发 go语言
  • Go语言的接口详解
    目录1、接口的用途2、类型断言3、类型选择4、空接口5、匿名空接口6、实现多个接口7、接口嵌套8、接口零值9、make和new的区别总结接口就是一系列方法的集合(规范行为) 在面向对...
    99+
    2022-11-12
  • Go语言中sync.Cond使用详解
    目录sync.Cond 可以用来干什么? 与 Sync.Mutex 的区别 sync.Cond 使用场景 sync.Cond sync.Cond 有哪些方法 NewCond 创建实例...
    99+
    2022-11-12
  • go语言中协程与线程的区别是什么
    区别:1、一个线程可以多个协程,一个进程也可以单独拥有多个协程;2、线程是同步机制,而协程则是异步;3、协程能保留上一次调用时的状态,线程不行;4、线程是抢占式,协程是非抢占式的;5、线程是被分割的CPU资源,协程是组织好的代码流程,协程需...
    99+
    2023-05-14
    Golang go语言 协程 线程
  • go语言中协程与线程的区别有哪些
    这篇文章主要介绍“go语言中协程与线程的区别有哪些”的相关知识,小编通过实际案例向大家展示操作过程,操作方法简单快捷,实用性强,希望这篇“go语言中协程与线程的区别有哪些”文章能帮助大家解决问题。区别:1、一个线程可以多个协程,一个进程也可...
    99+
    2023-07-05
  • GO语言的控制语句详解包括GO语言的指针语法
    GO语言的控制语句 判断结构:if-else 和大多数编程语言一样,if-else的用法基本都一样,直接来一个GO语言的例子 package main import ( "f...
    99+
    2022-12-19
    GO语言的控制语句详解 GO语言指针语法 GO语言switch结构 GO语言for结构
  • 详解Go语言中的内存对齐
    目录前言基础知识看个问题什么是内存对齐为什么需要内存对齐unsafe.AlignOf()内存对齐规则举个例子空结构体的对齐规则总结前言 前面有篇文章我们学习了 Go 语言空...
    99+
    2022-11-11
  • Go语言中interface语法与使用详解
    目录初识interface基本语法其他注意事项interface底层实现ifaceeface侵入式与非侵入式的理解interface的应用场景类型转换实现多态功能补充:interfa...
    99+
    2022-11-13
  • go语言怎么强制结束协程
    在Go语言中,我们可以使用runtime.Goexit()函数来强制结束当前协程。该函数会立即终止当前协程的执行,并将控制权返回给调...
    99+
    2023-10-21
    go语言
  • GO语言协程互斥锁Mutex和读写锁RWMutex用法实例详解
    sync.Mutex Go中使用sync.Mutex类型实现mutex(排他锁、互斥锁)。在源代码的sync/mutex.go文件中,有如下定义: // A Mutex is a m...
    99+
    2022-11-13
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作