GoLang 使用 goroutine 停止的几种办法

简介: GoLang 使用 goroutine 停止的几种办法

前言

我们有很多情况下需要主动关闭goroutine,如需要实现一个系统自动熔断的功能就需要主动关闭goroutine

为什么要中断GoRoutine?

场景:

俩个相互依赖的的操作,“依赖”是指如果其中一个失败,那么另一个就没有意义,而不是第二个操作依赖第一个操作的结果(那种情况下,两个操作不能并行)。在这种情况下,如果我们很早就知道其中一个操作失败,那么我们就会希望能取消所有相关的操作。

goroutine介绍

goroutine是Go语言实现并发编程的利器,是 Go语言中的轻量级线程实现,由 Go 运行时(runtime)管理,简单的一个指令go function就能启动一个goroutine;Go 程序会智能地将 goroutine 中的任务合理地分配给每个 CPU。

但是,Go语言并没有提供终止goroutine的接口,也就是说,我们不能从外部去停止一个goroutine,只能由goroutine内部退出(main函数终止除外);

几种停止的办法

1. 使用 for-range

for-range 从 channel 上接收值,直到 channel 关闭,该结构在Go并发编程中很常用,这对于从单一通道上获取数据去执行某些任务是十分方便的

package main

import (
  "fmt"
  "sync"
)

//源码&面试>>https://javapub.blog.csdn.net/category_11938137.html

var wg sync.WaitGroup //等待组,用来阻塞程序

func worker(ch chan int) {
  defer wg.Done() //等待组 -1
  for v := range ch {
    fmt.Println(v)
  }
}

func main() {

  ch := make(chan int)
  wg.Add(1) //等待组 +1
  go worker(ch)

  for i := 0; i < 5; i++ {
    ch <- i
  }

  close(ch) //必须要加close,因为在打印完0、1、2、3、4后会发生阻塞,直到chan关闭。
  wg.Wait()
}

去掉close的情况


2. 使用 for-select (向退出通道发出退出信号)

当channel比较多时,for-range结构借不是很方便了;

Go语言提供了另外一种和channel相关的语法: select;

select能够让goroutine在多个通信操作上等待(可以理解为监听多个channel);

由于这个特性,for-select结构在Go并发编程中使用的频率很高;

我在使用Go的开发中,这是我用的最多的一种组合形式:

这里用 quit通道接收退出信号。


package main

import (
  "fmt"
  "sync"
  "time"
)

var wg sync.WaitGroup

func worker(in, quit <-chan int) {
  defer wg.Done()
  for {
    select {
    case <-quit:
      fmt.Println("收到退出信号")
      return //必须return,否则goroutine不会结束
    case v := <-in:
      fmt.Println(v)
    }
  }
}

func main() {
  quit := make(chan int) //退出通道
  in := make(chan int)

  wg.Add(1)
  go worker(in, quit)

  for i := 0; i < 3; i++ {
    in <- i
    time.Sleep(1 * time.Second)
  }

  quit <- 1 //想通道写入退出信号
  wg.Wait()
}


3. 使用for-select(关闭退出通道)

当我们就需要向 quit 通道中发送100次数据,如果再用以上的代码就很麻烦,有一个很简单的方法,关闭 channel,这样所有监听 quit channel 的 goroutine 就都会收到关闭信号。

package main

//源码&面试>>https://javapub.blog.csdn.net/category_11938137.html

import (
  "fmt"
  "sync"
  "time"
)

var wg sync.WaitGroup

func worker(in, quit <-chan int) {
  defer wg.Done()
  for {
    select {
    case <-quit:
      fmt.Println("收到退出信号")
      return //必须return,否则goroutine不会结束
    case v := <-in:
      fmt.Println(v)
    }
  }
}

func main() {
  quit := make(chan int) //退出通道
  in := make(chan int)

  wg.Add(1)
  go worker(in, quit)

  for i := 0; i < 3; i++ {
    in <- i
    time.Sleep(1 * time.Second)
  }

  // quit <- 1 //想通道写入退出信号
  close(quit) // 直接关闭通道,程序退出
  wg.Wait()
}


4. 使用for-select(关闭多个channel)

如果select上监听了多个通道,需要所有的通道都关闭后才能结束goroutine,这里就利用select的一个特性,select不会在nil的通道上进行等待,因此将channel赋值为nil即可,此外,还需要利用channel的ok值。

package main

//源码&面试>>https://javapub.blog.csdn.net/category_11938137.html

import (
  "fmt"
  "sync"
  "time"
)

var wg sync.WaitGroup

func worker(in1, in2 <-chan int) {
  defer wg.Done()

  for {
    select {
    case v, ok := <-in1:
      if !ok {
        fmt.Println("收到退出信号1")
        in1 = nil
      }
      fmt.Println(v)
    case v, ok := <-in2:
      if !ok {
        fmt.Println("收到退出信号2")
        in2 = nil
      }
      fmt.Println(v)
    }
    if in1 == nil && in2 == nil {
      return
    }
  }
}

func main() {
  in1 := make(chan int)
  in2 := make(chan int)
  wg.Add(2)
  go worker(in1, in2)
  go worker(in1, in2)
  for i := 0; i < 3; i++ {
    in1 <- i
    time.Sleep(1 * time.Second)
    in2 <- i
  }
  close(in1)
  close(in2)
  wg.Wait()
}


5. 使用context包

context包是官方提供的一个用于控制多个goroutine写作的包;

使用context的cancel信号,可以终止goroutine的运行,context是可以向下传递的

package main

//源码&面试>>https://javapub.blog.csdn.net/category_11938137.html

import (
  "context"
  "errors"
  "fmt"
  "time"
)

func operation1(ctx context.Context) error {
  // 让我们假设这个操作会因为某种原因失败
  // 我们使用time.Sleep来模拟一个资源密集型操作
  time.Sleep(100 * time.Millisecond)
  return errors.New("failed")
}

func operation2(ctx context.Context) {
  // 我们使用在前面HTTP服务器例子里使用过的类似模式
  select {
  case <-time.After(500 * time.Millisecond):
    fmt.Println("done")
  case <-ctx.Done():
    fmt.Println("halted operation2")
  }
}

func main() {
  // 新建一个上下文
  ctx := context.Background()
  // 在初始上下文的基础上创建一个有取消功能的上下文
  ctx, cancel := context.WithCancel(ctx) //需要取消时,就调用cancel(),发出取消事件。
  // 在不同的goroutine中运行operation2
  go func() {
    operation2(ctx)
  }()

  err := operation1(ctx)
  fmt.Println(err)
  // 如果这个操作返回错误,取消所有使用相同上下文的操作
  if err != nil {
    cancel()
  }
}

// func main() {
//  // 创建一个监听8000端口的服务器
//  http.ListenAndServe(":8000", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
//    ctx := r.Context()
//    // 输出到 STDOUT 展示处理已经开始
//    fmt.Fprint(os.Stdout, "processing request\n")
//    // 通过select监听多个channel
//    select {
//    case <-time.After(2 * time.Second):
//      // 如果两秒后接受到了一个消息后,意味请求已经处理完成
//      // 我们写入"request processed"作为响应
//      w.Write([]byte("request processed"))
//    case <-ctx.Done():

//      // 如果处理完成前取消了,在STDERR中记录请求被取消的消息
//      fmt.Fprint(os.Stderr, "request cancelled\n")
//    }
//  }))
// }

目录
相关文章
|
存储 监控 Linux
Golang 语言的 goroutine 调度器模型 GPM
Golang 语言的 goroutine 调度器模型 GPM
83 0
|
4月前
|
安全 Go
Golang语言goroutine协程并发安全及锁机制
这篇文章是关于Go语言中多协程操作同一数据问题、互斥锁Mutex和读写互斥锁RWMutex的详细介绍及使用案例,涵盖了如何使用这些同步原语来解决并发访问共享资源时的数据安全问题。
105 4
|
4月前
|
Go 调度
Golang语言goroutine协程篇
这篇文章是关于Go语言goroutine协程的详细教程,涵盖了并发编程的常见术语、goroutine的创建和调度、使用sync.WaitGroup控制协程退出以及如何通过GOMAXPROCS设置程序并发时占用的CPU逻辑核心数。
92 4
Golang语言goroutine协程篇
|
5月前
|
NoSQL Unix 编译器
Golang协程goroutine的调度与状态变迁分析
文章深入分析了Golang中goroutine的调度和状态变迁,包括Grunnable、Gwaiting、Grunning和Gsyscall等状态,以及它们之间的转换条件和原理,帮助理解Go调度器的内部机制。
60 0
|
8月前
|
监控 Go 开发者
Golang深入浅出之-Goroutine泄漏检测与避免:pprof与debug包
【5月更文挑战第2天】本文介绍了Go语言并发编程中可能遇到的Goroutine泄漏问题,以及如何使用`pprof`和`debug`包来检测和防止这种泄漏。常见的问题包括忘记关闭channel和无限制创建goroutine。检测方法包括启动pprof服务器以监控Goroutine数量,使用`debug.Stack()`检查堆栈,以及确保每个Goroutine有明确的结束条件。通过这些手段,开发者可以有效管理Goroutine,维持程序性能。
283 7
|
8月前
|
Java Go
Golang深入浅出之-Goroutine泄漏检测与避免:pprof与debug包
【5月更文挑战第1天】本文介绍了Go语言中goroutine泄漏的问题及其影响,列举了忘记关闭通道、无限循环和依赖外部条件等常见泄漏原因。通过引入`net/http/pprof`和`runtime/debug`包,可以检测和避免goroutine泄漏。使用pprof的HTTP服务器查看goroutine堆栈,利用`debug`包的`SetGCPercent`和`FreeOSMemory`函数管理内存。实践中,应使用`sync.WaitGroup`、避免无限循环和及时关闭通道来防止泄漏。理解这些工具和策略对维护Go程序的稳定性至关重要。
202 4
|
8月前
|
安全 Go 开发者
Golang深入浅出之-Go语言并发编程面试:Goroutine简介与创建
【4月更文挑战第22天】Go语言的Goroutine是其并发模型的核心,是一种轻量级线程,能低成本创建和销毁,支持并发和并行执行。创建Goroutine使用`go`关键字,如`go sayHello(&quot;Alice&quot;)`。常见问题包括忘记使用`go`关键字、不正确处理通道同步和关闭、以及Goroutine泄漏。解决方法包括确保使用`go`启动函数、在发送完数据后关闭通道、设置Goroutine退出条件。理解并掌握这些能帮助开发者编写高效、安全的并发程序。
105 1
|
Go
Golang 语言怎么控制并发 goroutine?
Golang 语言怎么控制并发 goroutine?
53 0
|
8月前
|
程序员 Go 调度
第十六章 Golang中goroutine和channel
第十六章 Golang中goroutine和channel
57 3
|
存储 SQL 安全
Golang 语言标准库 context 包控制 goroutine
Golang 语言标准库 context 包控制 goroutine
54 0