并发编程在 Go 语言中是一项核心功能,使得同时运行多个进程或任务成为可能。
Go 使用协程(goroutines)通道(channels)来实现并发,这使得编写并发程序变得简单且安全。🦌💻

协程

协程(goroutines) 是由 Go 运行时管理的轻量级线程。
启动一个新的协程非常简单,只需在函数调用前加上 go 关键字。
这个函数将在一个新的协程中异步执行。

// 一个简单的函数
func sayHello() {
    fmt.Println("Hello, 世界!")
}

func main() {
    // 在新的协程中运行 sayHello 函数
    go sayHello()
    // 主协程继续执行其他操作
}

在上面的代码中,sayHello 函数将在它自己的协程中执行,而 main 函数会继续执行下一行代码而不会等待 sayHello

例子:

package main

import (
	"fmt"
	"time"
)
// 定义一个接口
type Greeter interface {
	Greet() string
}

// 定义一个结构体
type EnglishSpeaker struct{}

// 实现接口方法
func (es EnglishSpeaker) Greet() string {
	return "Hello!"
}

// 定义另一个结构体
type ChineseSpeaker struct{}

// 实现接口方法
func (cs ChineseSpeaker) Greet() string {
	return "你好!"
}

// 任何满足Greeter接口的类型都可以传递给这个函数
func sayGreeting(g Greeter) {
	fmt.Println(g.Greet())
}

// 这个函数会在协程中运行
func greetEverySecond(greeter Greeter) {
	for i := 0; i < 5; i++ {
		fmt.Println(greeter.Greet())
		time.Sleep(1 * time.Second)
	}
}

func main() {
	var greeter Greeter = EnglishSpeaker{}

	// 开始一个新的Go协程
	go greetEverySecond(greeter)

	// 主线程继续执行其他任务
	fmt.Println("这是主线程")

	// 等待协程完成
	time.Sleep(6 * time.Second)
	fmt.Println("主线程结束")
}

输出:

image.png

在这个示例中,greetEverySecond 函数会打印问候语并暂停一秒钟,重复五次。
我们使用 go 关键字在新的 Go 协程中执行这个函数,同时主线程继续执行。

例子:

func task1() {
    for i := 0; i < 5; i++ {
        fmt.Println("Task 1")
        time.Sleep(time.Second)
    }
}

func task2() {
    for i := 0; i < 5; i++ {
        fmt.Println("Task 2")
        time.Sleep(time.Second)
    }
}

func main() {
    go task1() // 开启一个新的goroutine来运行task1
    go task2() // 开启另一个新的goroutine来运行task2

    // 等待足够长的时间以观察到两个任务的输出
    time.Sleep(6 * time.Second)
}

输出:

image.png

在这个例子中,task1task2 函数几乎同时运行,因为它们分别在自己的 goroutines 中执行。
主函数在等待时不会阻塞这些 goroutines 的执行,所以我们可以看到两个任务的输出几乎是交替出现的。

例子:

package main

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

/*
goroutine 开启时进行 wg.Add(1) 加 1
goroutine 结束时进行 wg.Done() 减 1
wg.Wait() 会判断当前的 goroutine 是否为 0,为 0 则退出
*/
// 定义一个计数器
var wg sync.WaitGroup

func main() {
	// 开启一个协程计数器+1
	wg.Add(1)
	go test()
	// 计数器为0时则退出
	wg.Wait()
	fmt.Println("主函数运行结束!")
}

func test() {
	for i := 0; i < 10; i++ {
		fmt.Println(i)
		time.Sleep(100 * time.Microsecond)
	}
	// 协程执行完毕,计数器-1
	wg.Done()
}

输出:

image.png


通道

通道(channels) 是用来在协程之间安全地传递数据的管道。
你可以发送数据到一个通道,并在另一个通道接收数据。

// 创建一个传递 int 类型数据的通道
ch := make(chan int)

// 在新的协程中发送数据到通道
go func() {
    ch <- 42 // 把 42 发送到通道
}()

// 在主协程中接收通道的数据
val := <-ch
fmt.Println("接收到的值:", val) // 输出接收到的值: 42

在这个例子中,我们创建了一个通道 ch,在一个新的协程中向它发送了一个值 42,然后在主协程中接收并打印这个值。

例子:

// 创建一个通道用来传递数据
ch := make(chan int)

// 发送者goroutine
go func() {
    for i := 0; i < 5; i++ {
        // 发送数据到通道
        ch <- i
    }
    close(ch) // 发送完成后,关闭通道
}()

// 接收者goroutine
go func() {
    for value := range ch {
        // 从通道接收数据
        fmt.Println("Received:", value)
    }
}()

在这个例子中,我们创建了一个通道 ch,一个 goroutine 用于发送数据,另一个 goroutine 用于接收数据。
当发送者完成发送后,它会关闭通道来通知接收者没有更多的数据。


并发与通道示例

package main

import (
	"fmt"
	"time"
)

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

	// 生产者goroutine,发送数据到通道
	go func() {
		for i := 0; i < 5; i++ {
			fmt.Printf("Sent: %d\n", i)
			ch <- i // 将i发送到通道
			time.Sleep(time.Second) // 模拟耗时的发送操作
		}
		close(ch) // 发送完成后关闭通道
	}()

	// 消费者goroutine,从通道接收数据
	go func() {
		// 使用for range循环从通道接收数据,直到通道被关闭
		for value := range ch {
			fmt.Printf("Received: %d\n", value)
		}
	}()

	// 主函数等待足够的时间,确保所有数据都被发送和接收
	// 这里我们只是简单地使用Sleep,实际项目中你可能会使用sync包中的WaitGroup
	time.Sleep(7 * time.Second)
	fmt.Println("Finished processing")
}

输出:

image.png

在这个示例中,我们定义了一个名为 main 的函数,它是程序的入口点。
它首先创建了一个无缓冲的通道 ch。然后,启动了两个 goroutine

  • 生产者 goroutine 在一个 for 循环中发送 04 的数字,每次发送后休眠一秒钟来模拟耗时的操作。发送完成后,它使用 close 函数关闭了通道,以通知消费者没有更多的数据将会发送。
  • 消费者 goroutine 使用 for range 循环来接收通道 ch 中的数据。由于通道被关闭,循环会在接收完所有数据后退出。

main 函数在最后使用 time.Sleep 来等待一段时间,这确保了所有的数据都能够被发送和接收。
在实际的应用中,我们通常会使用 sync.WaitGroup 或其他同步机制来等待所有的 goroutine 完成,而不是使用 time.Sleep


并发与通道进阶

并发在 Go 中通过 goroutines 实现,它们是由 Go 运行时环境调度的轻量级线程。
goroutines 可以使用 通道(channels) 来通信。

例子:

package main

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

// 一个工作函数,模拟耗时的任务
func worker(id int, wg *sync.WaitGroup, jobs <-chan int, results chan<- int) {
	for job := range jobs {
		fmt.Printf("Worker %d started job %d\n", id, job)
		time.Sleep(time.Second) // 模拟工作耗时
		fmt.Printf("Worker %d finished job %d\n", id, job)
		results <- job * 2 // 将结果发送到结果通道
		wg.Done()          // 通知WaitGroup任务已完成
	}
}

func main() {
	const numJobs = 5
	jobs := make(chan int, numJobs)
	results := make(chan int, numJobs)
	var wg sync.WaitGroup

	// 启动三个worker,初始时都在等待
	for w := 1; w <= 3; w++ {
		wg.Add(1) // 增加WaitGroup计数器
		go worker(w, &wg, jobs, results)
	}

	// 发送工作任务
	for j := 1; j <= numJobs; j++ {
		jobs <- j
	}
	close(jobs) // 关闭通道表示不再发送新工作

	// 等待所有goroutines完成
	wg.Wait()
	close(results) // 关闭结果通道

	// 收集所有的结果
	for result := range results {
		fmt.Printf("Job result: %d\n", result)
	}
}

输出:

image.png

这个例子中我们创建了一个工作队列和一个结果队列,然后启动了三个 worker goroutines 去处理作业。
我们使用了 sync.WaitGroup 来等待所有作业完成。
每个 worker 在完成作业时,都会向 results 通道发送一个结果,并通过 wg.Done() 通知 WaitGroup 一个作业已完成。
main 函数等待所有作业完成后,关闭结果通道,并收集和打印出所有结果。


选择语句

选择语句(select) 用于等待多个通道操作。
它会阻塞,直到一个或多个通道准备好通信。

select {
case val := <-ch:
    fmt.Println("从通道接收到:", val)
case <-time.After(50 * time.Millisecond):
    fmt.Println("超时了!")
}

这个 select 语句等待通道 ch 接收数据或超时发生(使用 time.After)。

并发编程可以提高程序的性能,特别是在处理多个独立任务或高延迟操作时。
但它也引入了竞争条件和同步问题。
Go 的通道和协程提供了一种相对简单的方式来处理这些挑战。


文章作者: Runfa Li
版权声明: 本站所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Linux 小白鼠
GO 入门 GO go
觉得文章不错,打赏一点吧,1分也是爱~
打赏
微信 微信
支付宝 支付宝