第七章:并发编程 2.Channels --Go 语言轻松入门

时间:2024-12-04 09:23:38

Go语言中的通道(Channel)是一种特殊的类型,用于在不同的goroutine之间传递数据和同步执行。通道提供了一种安全的方式来避免数据竞争,并且简化了并发编程的复杂性。下面是关于Go Channels的一些关键点:

1. 基本概念

  • 定义var ch chan int 定义了一个整数类型的通道。
  • 创建:使用 make 函数创建通道,例如 ch := make(chan int) 或者 ch := make(chan int, 10) 创建一个带缓冲的通道。
  • 方向性:可以定义单向通道,如 chan<- int 只能发送,<-chan int 只能接收。
package main

import (
	"fmt"
	"time"
)

// 定义一个只发送整数的函数
func sendNumbers(ch chan<- int) {
	for i := 0; i < 5; i++ {
		fmt.Printf("Sending: %d\n", i)
		ch <- i
		time.Sleep(1 * time.Second)
	}
	close(ch) // 发送完成后关闭通道
}

// 定义一个只接收整数并处理的函数
func processNumbers(ch <-chan int) {
	for v := range ch {
		fmt.Printf("Processing: %d\n", v)
		time.Sleep(1 * time.Second)
	}
}

func main() {
	// 创建一个无缓冲的整型通道
	ch := make(chan int)

	// 启动发送者goroutine
	go sendNumbers(ch)

	// 启动处理者goroutine
	go processNumbers(ch)

	// 等待一段时间让所有goroutines完成
	time.Sleep(10 * time.Second)
	fmt.Println("All messages processed, exiting.")
}

在这里插入图片描述

2. 操作

  • 发送ch <- value 将值发送到通道中。
  • 接收value := <- ch 从通道中接收值。
  • 多值接收value, ok := <- ch 接收值的同时检查通道是否关闭 (okfalse 表示通道已关闭)。

3. 阻塞与非阻塞

  • 无缓冲通道:发送和接收操作会一直阻塞,直到另一方准备好。
package main

import (
	"fmt"
)

func main() {
	// 创建一个无缓冲的整型通道
	ch := make(chan int)

	// 启动一个发送者goroutine
	go func() {
		for i := 0; i < 5; i++ {
			fmt.Printf("Sender: Sending %d\n", i)
			ch <- i // 将i发送到通道ch
			// time.Sleep(1 * time.Second) // 模拟耗时操作
		}
		close(ch) // 发送完成后关闭通道
	}()

	// 主goroutine作为接收者
	for v := range ch {
		fmt.Printf("Receiver: Received %d\n", v)
	}

	fmt.Println("All messages received, exiting.")
}

在这里插入图片描述

  • 有缓冲通道:只有当缓冲区满时发送才会阻塞;只有当缓冲区为空时接收才会阻塞。
package main

import (
	"fmt"
)

func main() {
	// 创建一个带有缓冲区大小为2的整型通道
	ch := make(chan int, 2)

	// 启动一个发送者goroutine
	go func() {
		for i := 0; i < 10; i++ {
			fmt.Printf("Sender: Sending %d\n", i)
			ch <- i // 将i发送到通道ch
			// time.Sleep(1 * time.Second) // 模拟耗时操作
		}
		close(ch) // 发送完成后关闭通道
	}()

	// 主goroutine作为接收者
	for v := range ch {
		fmt.Printf("Receiver: Received %d\n", v)
	}

	fmt.Println("All messages received, exiting.")
}

在这里插入图片描述

4. 关闭通道

  • 使用 close(ch) 关闭通道。关闭后的通道不能再发送数据,但可以继续接收剩余的数据直到通道为空。

5. 选择器

  • select 语句允许在多个通信操作中进行选择,类似于 switch 语句,但是只处理通信操作。
  • default 子句可以让 select 在没有可用的case时执行。
package main

import (
	"fmt"
	"time"
)

// main 是程序的入口点。
func main() {
	// 创建一个无缓冲的channel,用于接收异步操作的结果。
	ch := make(chan int)

	// 启动一个goroutine,在后台执行操作。
	go func() {
		// 模拟一些耗时操作,比如等待I/O完成。
		time.Sleep(3 * time.Second)
		// 将结果发送到channel。
		ch <- 42
	}()

	// 2秒后超时,生成一个超时信号。
	timeout := time.After(2 * time.Second)

	// 无限循环,等待结果或超时。
	for {
		select {
		// 当channel接收到值时。
		case value := <-ch:
			// 打印接收到的值并退出程序。
			fmt.Println("Received:", value)
			return
		// 当超时信号触发时。
		case <-timeout:
			// 打印超时信息并退出程序。
			fmt.Println("Timed out")
			return
		// 默认情况,即没有接收到值也没有超时。
		default:
			// 打印等待信息,并短暂暂停以避免忙等待。
			fmt.Println("Still waiting...")
			time.Sleep(500 * time.Millisecond)
		}
	}
}

在这里插入图片描述

6. 范围循环

  • for range 循环可以用来遍历通道中的数据,直到通道被关闭并且所有数据都被读取。

7. 应用场景

  • 并行计算:通过通道收集结果。
  • 事件分发:作为事件处理器之间的消息队列。
  • 资源池:管理一组可重用资源。

8. 注意事项

  • 不要对同一个通道同时进行多次接收或发送操作。
package main

import (
	"fmt"
)

// main 是程序的入口点
func main() {
	// 创建一个带有缓冲区的整型通道
	ch := make(chan int, 5) // 缓冲区大小为5

	// 启动两个发送者goroutine
	go func() {
		for i := 0; i < 5; i++ {
			fmt.Printf("Sender 1: Sending %d\n", i)
			ch <- i
		}
	}()

	go func() {
		for i := 10; i < 15; i++ {
			fmt.Printf("Sender 2: Sending %d\n", i)
			ch <- i
		}
	}()

	// 接收者goroutine
	for i := 0; i < 10; i++ {
		fmt.Printf("Receiver: Received %d\n", <-ch)
	}
}

在这里插入图片描述

  • 当通道不再使用时应该关闭它,以便接收方能够检测到通道关闭。
  • 使用 range 迭代通道时确保最终会关闭通道,否则会导致死锁。

通过理解和运用这些概念,你可以利用Go的通道来构建高效、可靠的并发程序。