golang协程goroutine教程

发布时间:2024年01月11日

前言

项目经常遇到一些批量任务执行太慢,需要开启多线程去处理,记录下在Golang中协程使用的一些操作。


协程介绍

协程是计算机程序的一类组件,推广了协作式多任务的子例程,允许执行被挂起与被恢复。相对子例程而言,协程更为一般和灵活,但在实践中使用没有子例程那样广泛。协程更适合于用来实现彼此熟悉的程序组件,如协作式多任务、异常处理、事件循环、迭代器、无限列表和管道。

协程与线程的区别在于:

  • 协程的创建和切换开销更小,因此可以创建更多的协程。
  • 协程之间是异步的,也就是说,一个协程的执行不会阻塞另一个协程的执行。
  • 协程的调度是由系统来进行的,程序员不需要手动进行调度。

协程的优势在于:

  • 可以实现高效的并发编程。
  • 可以简化并发编程的代码。
  • 可以提高并发编程的性能。

协程的常见用例包括:

  • 异步网络操作。
  • 异步文件操作。
  • 异步数据库操作。
  • 异步计算。
  • 事件循环。

Golang 语言还提供了 channel 数据结构来用于协程之间的通信。

channel 介绍

在Go语言中,通道(Channel)是一种用于在协程之间进行通信和同步的数据结构。通道提供了一种安全的方式,使一个协程可以向另一个协程发送数据,并保证在接收方准备好之前,发送方会被阻塞。通道是Go语言并发编程的核心机制之一。

以下是关于Golang中通道的一些基本介绍:

  1. 通道的创建:
    使用make函数创建一个通道,通道的类型是数据传递的类型。通道可以是带缓冲的(Buffered Channel)或无缓冲的(Unbuffered Channel)。

    // 无缓冲通道
    ch := make(chan int)
    
    // 带缓冲通道,容量为3
    chBuffered := make(chan int, 3)
    
  2. 通道的发送和接收:
    使用<-运算符进行通道的发送和接收操作。发送和接收操作都是原子的,这有助于避免竞态条件。

    // 发送数据到通道
    ch <- 42
    
    // 从通道接收数据
    value := <-ch
    
  3. 无缓冲通道:
    无缓冲通道在发送和接收数据时,发送方和接收方必须同时准备好,否则它们将阻塞等待。

    ch := make(chan int)
    
    go func() {
        // 发送数据到通道
        ch <- 42
    }()
    
    // 从通道接收数据
    value := <-ch
    
  4. 带缓冲通道:
    带缓冲通道允许在通道满之前缓存一定数量的元素,发送和接收操作变得非阻塞,直到通道满或为空。

    ch := make(chan int, 3)
    
    // 发送数据到通道,不会阻塞
    ch <- 42
    ch <- 43
    ch <- 44
    
  5. 关闭通道:
    使用close函数关闭通道,关闭后的通道不能再发送数据,但可以继续接收已发送的数据。

    close(ch)
    
  6. 通道的选择(select语句):
    select语句用于在多个通道操作中进行选择,以处理多路通信。

    select {
    case msg1 := <-ch1:
        fmt.Println("Received", msg1)
    case msg2 := <-ch2:
        fmt.Println("Received", msg2)
    default:
        fmt.Println("No communication")
    }
    
  7. 通道的方向:
    可以通过在通道类型中指定方向,限制通道的发送或接收操作。

    // 只能发送数据到通道
    func sendData(ch chan<- int) {
        ch <- 42
    }
    
    // 只能从通道接收数据
    func receiveData(ch <-chan int) {
        value := <-ch
    }
    

通道是Go语言中非常强大和灵活的并发原语,能够帮助你构建安全、高效的并发程序。通过良好的通道使用,可以避免共享数据的竞态条件,提高程序的可维护性和稳定性。

基础使用

在Go语言中,协程(Goroutine)是一种轻量级的线程,由Go语言的运行时系统调度。协程使得并发编程更加容易和高效。下面是一些关于Golang协程基础使用的示例:

  1. 创建协程:
    使用关键字 go 可以创建一个新的协程。以下是一个简单的例子:

    package main
    
    import (
        "fmt"
        "time"
    )
    
    func main() {
        go sayHello()
        time.Sleep(1 * time.Second) // 等待协程执行
    }
    
    func sayHello() {
        fmt.Println("Hello, Goroutine!")
    }
    

    这个例子中,sayHello 函数被放在一个新的协程中运行。

  2. 传递参数给协程:
    如果你需要将参数传递给协程,可以使用闭包。以下是一个例子:

    package main
    
    import (
        "fmt"
        "time"
    )
    
    func main() {
        msg := "Hello, Goroutine!"
        go func(m string) {
            fmt.Println(m)
        }(msg)
        time.Sleep(1 * time.Second)
    }
    

    在这个例子中,我们创建了一个匿名函数,并在协程启动时传递了 msg 参数。

  3. 等待协程执行完成:
    如果主程序需要等待协程执行完成,可以使用 sync 包或者 channel 来实现。以下是一个使用 sync 包的例子:

    package main
    
    import (
        "fmt"
        "sync"
    )
    
    func main() {
        var wg sync.WaitGroup
        wg.Add(1)
    
        go func() {
            defer wg.Done()
            fmt.Println("Hello, Goroutine!")
        }()
    
        wg.Wait()
    }
    

    在这个例子中,sync.WaitGroup 用于等待协程执行完成。

  4. 使用 Channel 进行通信:
    协程之间通过 Channel 进行通信。以下是一个简单的例子:

    package main
    
    import (
        "fmt"
    )
    
    func main() {
        messages := make(chan string)
    
        go func() {
            messages <- "Hello, Goroutine!"
        }()
    
        msg := <-messages
        fmt.Println(msg)
    }
    

    这个例子中,messages 是一个字符串类型的 channel,协程通过 <- 运算符发送消息,主程序通过 <- 运算符接收消息。

这些是一些基础的 Golang 协程使用的例子。在实际开发中,协程的使用可以更加复杂,涉及到协程之间的同步、错误处理等方面的考虑。

协程中传参数示例

下面是一个示例,其中一个协程负责执行任务,接收参数,返回结果,而主程序等待协程完成并获取结果:

package main

import (
	"fmt"
	"sync"
)

func main() {
	var wg sync.WaitGroup
	resultCh := make(chan int, 1)

	wg.Add(1)
	go worker(5, &wg, resultCh)

	wg.Wait()

	// 从通道中获取结果
	result := <-resultCh
	fmt.Println("Result:", result)
}

func worker(value int, wg *sync.WaitGroup, resultCh chan int) {
	defer wg.Done()

	// 模拟一些工作
	result := value * 2

	// 将结果发送到通道
	resultCh <- result
}

在这个示例中,worker 函数是一个协程,负责执行任务。它接收一个整数参数 value,执行一些工作(这里简单地将参数乘以2),然后将结果发送到结果通道 resultCh

主程序创建了一个 sync.WaitGroup 用于等待协程完成,还创建了一个通道 resultCh 用于接收结果。主程序启动了一个协程并传递了参数5给 worker 函数。然后,主程序使用 Wait 方法等待协程完成。

在协程执行完成后,它会将结果发送到通道,并主程序通过 <-resultCh 语句从通道中获取结果。最后,主程序打印出获取的结果。

这个例子演示了如何在协程中执行任务,传递参数,获取结果,并等待协程完成。

协程中channelselect使用

下面是一个带有管道和 select 的复杂示例。在这个示例中,有两个协程,一个生成随机数并将其发送到管道,另一个协程从管道接收随机数并执行不同的任务。主程序使用 select 语句等待这两个协程的完成:

package main

import (
	"fmt"
	"math/rand"
	"sync"
	"time"
)

func main() {
	randCh := make(chan int)
	resultCh := make(chan int)

	var wg sync.WaitGroup
	wg.Add(2)

	go randomNumberGenerator(randCh, &wg)
	go processNumbers(randCh, resultCh, &wg)

	go func() {
		wg.Wait()
		close(resultCh)
	}()

	// 使用 select 语句等待协程完成
	for {
		select {
		case num, ok := <-resultCh:
			if !ok {
				fmt.Println("All tasks completed.")
				return
			}
			fmt.Println("Processed number:", num)
		case <-time.After(2 * time.Second):
			fmt.Println("Timeout: No result received in 2 seconds.")
			return
		}
	}
}

func randomNumberGenerator(randCh chan int, wg *sync.WaitGroup) {
	defer wg.Done()
	rand.Seed(time.Now().UnixNano())

	for i := 0; i < 5; i++ {
		randomNum := rand.Intn(100)
		fmt.Println("Generated random number:", randomNum)
		randCh <- randomNum
		time.Sleep(500 * time.Millisecond)
	}

	close(randCh)
}

func processNumbers(randCh chan int, resultCh chan int, wg *sync.WaitGroup) {
	defer wg.Done()

	for {
		select {
		case num, ok := <-randCh:
			if !ok {
				fmt.Println("Number generator closed.")
				return
			}
			// 模拟处理任务
			result := num * 2
			resultCh <- result
		}
	}
}

在这个示例中,randomNumberGenerator 协程生成随机数并将其发送到 randCh 管道。processNumbers 协程从 randCh 管道接收随机数,并执行一些任务,将结果发送到 resultCh 管道。

主程序使用 select 语句等待 resultCh 管道中的结果,同时设置一个 2 秒的超时。如果在超时之前没有接收到结果,主程序会输出超时消息。这个例子演示了如何使用管道和 select 来协调和同步不同的协程。

文章来源:https://blog.csdn.net/u010844836/article/details/135532325
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。