• goroutine学习


    goroutine 为什么比java中的线程更加轻量?

    两种并发机制在资源消耗、调度方式、创建和管理上有明显的差异:

    1. 堆栈大小:

      • Goroutine 的初始堆栈大小相对较小,通常在几 KB(2KB或4KB)左右,而且可以根据需要动态伸缩。这意味着在同样的内存条件下,可以创建更多的 goroutine。
      • Java 线程的堆栈大小通常较大,可能从几百 KB 到几 MB 不等,这取决于 JVM 的配置和操作系统。Java 线程的堆栈不会动态伸缩,因此每个线程都会占用更多的内存资源。
    2. 调度:

      • Goroutine 是由 Go 运行时(runtime)管理的,使用的是 M:N 调度模型(多个 goroutine 可以映射到多个或单个操作系统线程)。Go 运行时包含自己的调度器,它可以在用户态进行调度,避免了内核态与用户态之间的频繁切换,这使得创建和切换 goroutine 的成本比线程低得多。
      • Java 线程通常是直接映射到操作系统原生线程(1:1 模型),由操作系统内核进行调度。这意味着线程的创建和切换需要进行用户态和内核态之间的切换,这比用户态的调度更耗时。
    3. 创建和管理:

      • Goroutine 的创建和管理是由 Go 语言的运行时直接处理的,这使得它们的创建和销毁非常快速和高效。
      • Java 线程 的创建和管理则是由 JVM 和操作系统共同处理的,这通常比 goroutine 更复杂,因此也更耗费资源。
    4. 上下文切换:

      • Goroutine 之间的上下文切换通常比 Java 线程之间的上下文切换要快,因为它们需要保存和恢复的状态更少。

    1、创建goroutine:

    go func() {

        // 业务逻辑

    }()

    2、goroutine中发生了panic怎么办:

    (1)使用recover函数来恢复程序的执行。

    (2)recover函数只有在defer函数中调用才能起作用,它会捕获当前goroutine中的panic,并返回panic所传递的值。
      注意:recover函数只能在同一个goroutine中恢复panic,不能跨越多个goroutine。

    go func() {

        defer func() {

            if err := recover(); err != nil {

                fmt.println(err)

            }

        }()

    }()

    3、协程间通信如何实现:

    (1)channel 是一种用于协程间通信和同步的重要机制。

             当我们创建一个 channel 时,实际上是在内存中分配了一个 channel 对象,包括一个队列和一些控制信息,用于协程之间的数据传输。

    (2)channel 对象是什么时候被销毁的?

             当没有任何协程在使用一个 channel 时,该 channel 会被垃圾回收器回收。
            也就是说,当所有使用该 channel 的协程都已经结束或退出时,该 channel 对象才会被销毁。

    4、goroutine中使用channel,channel阻塞了怎么办:

           往已满的channel中发送数据 或 从一个空channel中接收数据时,channel 会阻塞当前 goroutine,等待操作的另一方执行相应的操作后才会继续执行。

           如果 channel 阻塞了,可以考虑以下几种方式:

        (1)、使用非阻塞式的 channel 操作:可以使用 select 语句进行非阻塞的 channel 操作。通过在 select 语句中使用 default 分支或者带有超时的 case 分支,可以在 channel 阻塞时执行其他操作或者超时退出。

        (2)、使用带缓冲的 channel:带缓冲的 channel 可以在一定程度上避免 channel 阻塞的问题。通过给 channel 设置缓冲区,可以在 channel 不满或不空时发送或接收数据,并避免阻塞。

        (3)、调整 goroutine 的数量:减少 goroutine 的数量或者增加并发操作的数量,可以在一定程度上避免 channel 阻塞的问题。

         扩展一:带超时的case分支:

    // 在下面的代码中,使用 <-time.After(time.Second) 创建一个定时器,它将在 1 秒后向一个无缓冲的 channel 发送一个时间值。

    // 因此,在 select 语句中,如果 1 秒内没有任何其他 channel 发生变化,就会执行超时操作。

    // 需要注意的是,time.After 函数返回的是一个 channel,因此在 select 语句中需要使用 <- 符号来接收其返回值

    select {

    case <-ch1:

        // 处理 ch1 的数据

    case <-ch2:

        // 处理 ch2 的数据

        // <-time.After(time.Second) 创建一个定时器,它将在 1 秒后向一个无缓冲的 channel 发送一个时间值。

    case <-time.After(time.Second):

        // 在 1 秒后执行超时操作

    }

    5、goroutine是什么时候销毁的:

    (1)启动一个 goroutine 时,实际上是在创建一个新的协程并在其中执行指定的函数,与主协程并发地执行。
             当函数执行结束后,该 goroutine 也就自然而然地被销毁了。
    (2)需要注意的是,goroutine 的生命周期与其所在的程序进程相关联,它们在执行结束后会自动退出并被 Go 运行时系统回收。
             也就是说,只要程序进程没有结束,goroutine 就会一直存在,除非它自己调用了 return、panic 或 recover 函数,或者被调度器强制终止。
             在一般情况下,goroutine 在执行结束后就会自动退出,并被 Go 运行时系统回收。

    6、goroutine 和 消息中间件 使用场景有什么区别:

    (1)它俩都是在编写并发程序时常用的工具,但是使用场景有所不同。
    (2)Goroutine 适用于处理大量短时间的任务,比如网络请求、IO 操作等。
    (3)消息中间件是一种用于分布式系统中消息传递的工具,可以在不同的进程、主机甚至不同的系统之间传递消息,适用于大规模分布式系统中的任务调度、数据传输等场景。

    因此,Goroutine 适用于单机并发场景,而消息中间件适用于分布式系统中的任务调度和消息传递场景。
    当需要处理的任务量很大时,可以考虑使用消息中间件来进行任务调度和消息传递,而不是仅仅使用 Goroutine 来实现并发。

    7、goroutine中调用远程服务,失败了怎么办:

    (1)返回错误信息并重试:可以使用循环或递归等方式进行重试,直到成功或达到最大重试次数为止。注意:在进行重试时需要考虑到错误的类型和重试的间隔时间等因素,以免重复出现相同的错误。
    (2)通过日志记录错误:在发生错误时,可以将错误信息记录到日志中,方便后续排查问题。
    (3)抛出异常并结束程序:如果错误是无法处理的,比如服务端已经停止了服务,可以抛出异常并结束程序。在捕获异常时,可以使用recover 函数来捕获异常并进行处理。

      无论采用哪种方式来处理错误,在进行远程服务调用时,一定要做好异常处理和错误处理,以确保程序的稳定性和健壮性。

    8、go语言recover知识点:


    (1)仅在 defer 函数中有效:recover 函数只能在 defer 函数中使用。
    (2)捕获 panic:recover 函数只能捕获到当前 goroutine 中的 panic。当 panic 被捕获后,程序会从 panic 处开始恢复执行。
    (3)返回值:当 recover 函数捕获到一个 panic 时,它会返回传递给 panic 函数的值。如果 recover 函数没有捕获到任何 panic,则返回 nil。
    (4)阻止程序崩溃:recover 函数的主要目的是阻止程序崩溃。当程序发生 panic 时,通过使用 recover 函数可以避免程序终止。

    package main

    import "fmt"

    func main() {

        defer func() {

            if r := recover(); r != nil {

                fmt.Println("Recovered from:", r)

            }

        }()

        fmt.Println("Starting the program")

        panic("A panic occurred")

        fmt.Println("End of the program"// 这行代码将不会被执行,因为程序在 panic 处恢复执行。

    }

    // 输出

    Starting the program

    Recovered from: A panic occurred

    请注意,尽管可以使用 recover 来处理异常,但在 Go 中,建议尽可能使用错误值而不是 panic 和 recover,因为错误值通常更容易理解和处理。
    panic 和 recover 主要用于处理那些无法预料或难以处理的错误情况。

    9、多个goroutine直接怎么保障顺序执行:


    可以使用无缓冲的信道来实现同步,可以确保一个goroutine在另一个goroutine完成后才开始执行。

    package main

    import (

        "fmt"

        "time"

    )

    func firstTask(done chan bool) {

        fmt.Println("Executing first task...")

        time.Sleep(1 * time.Second)

        // 第一个goroutine完成时,向done1发送一个值

        done <- true

    }

    func secondTask(done chan bool) {

        fmt.Println("Executing second task...")

        time.Sleep(1 * time.Second)

        // 第二个goroutine在完成时向done2发送一个值

        done <- true

    }

    func main() {

        // 创建两个无缓冲的信道。

        done1 := make(chan bool)

        done2 := make(chan bool)

        // 当第一个goroutine完成时,它将向done1发送一个值,

        go firstTask(done1)

        // 主goroutine会阻塞等待接收这个值,当接收到这个值后,主goroutine才会继续执行第二个goroutine。

        <-done1

        // 第二个goroutine在完成时向done2发送一个值,

        go secondTask(done2)

        // 主goroutine阻塞等待接收这个值。

        <-done2

        // 当接收到这个值后,主goroutine将继续执行,输出 "All tasks completed."。

        fmt.Println("All tasks completed.")

    }

    在这个示例中,done1 和 done2 是无缓冲的信道。

    当第一个goroutine完成时,它将向done1发送一个值,主goroutine会阻塞等待接收这个值,当接收到这个值后,主goroutine才会继续执行第二个goroutine。

    类似地,第二个goroutine在完成时向done2发送一个值,主goroutine阻塞等待接收这个值。当接收到这个值后,主goroutine将继续执行,输出 "All tasks completed."。

    10、开启一个goroutine,怎么控制关闭它:

    可以使用一个 chan bool 类型的 channel 来控制一个 goroutine 的生命周期,从而实现控制其关闭的功能。
    具体实现方式如下:
    (1)在主函数或其他需要控制 goroutine 的地方,创建一个 bool 类型的 channel,并将其传递给要启动的 goroutine。
    (2)在 goroutine 中,使用一个无限循环来监听这个 channel 的状态,如果 channel 接收到了信号,就退出循环,从而结束 goroutine。
    (3)当需要关闭 goroutine 时,向该 channel 发送一个信号,通知 goroutine 退出循环。

    func startWorker(stopCh chan bool) {

        for {

            // 在这里执行 goroutine 的任务逻辑

            // 在 goroutine 中,使用一个无限循环来监听这个 channel 的状态,如果 channel 接收到了信号,就退出循环,从而结束 goroutine。

            select {

            case <-stopCh:

                // 收到关闭信号,退出循环

                return

            default:

                // 如果没有收到关闭信号,则继续循环执行任务逻辑

            }

        }

    }

    func main() {

        // 主函数创建一个 bool 类型的 channel,并将其传递给要启动的 goroutine。

        stopCh := make(chan bool)

        go startWorker(stopCh)

        // 等待一段时间后关闭 goroutine

        time.Sleep(time.Second * 5)

        stopCh <- true

    }

    在上面的代码中,我们创建了一个 stopCh channel,然后在 startWorker 函数中通过一个无限循环来执行 goroutine 的任务逻辑。

    在每次循环开始时,使用 select 语句来监听 stopCh channel 的状态。如果收到了关闭信号,就退出循环并结束 goroutine。
    在 main 函数中,我们通过 go startWorker(stopCh) 启动了一个 goroutine。然后在等待 5 秒后,向 stopCh channel 发送了一个信号,通知 goroutine 结束循环并退出。

  • 相关阅读:
    Python自学笔记
    c#中使用METest单元测试
    SQL知识大全(二):SQL的基础知识你都掌握了吗?
    多线程 3
    Redis笔记 Redis主从同步
    Greenplum常用sql语句
    基于 Kubernetes 的 DevOps
    【PowerQuery】Excel的PowerQuery的复制
    【Maven】基础
    C++——模板
  • 原文地址:https://blog.csdn.net/h2604396739/article/details/138222399