Go语言学习 二十二 并发

本文深入探讨了Go语言中的并发特性,包括Goroutine、无缓冲和带缓冲的信道、select语句以及互斥锁等同步机制。通过示例展示了如何使用Goroutine和信道实现线程安全的通信,强调了"不要通过共享内存来通信,而应通过通信来共享内存"的设计理念。此外,还介绍了如何使用sync包中的Once和WaitGroup进行一次性初始化和等待任务完成。
摘要由CSDN通过智能技术生成

本文最初发表在我的个人博客,查看原文,获得更好的阅读体验


并发是每个编程语言绕不开的一个话题,Go在并发编程方面提供了许多特性,帮助简化并发模型,如轻量级的线程goroutine,信道等,同样也提供了如sync.Mutex等的锁机制。

为实现对共享变量的正确访问,Go语言提供了一种特殊的控制方式,即将共享的值通过信道传递。信道是一种带有方向的管道,数据可以在其中流转。在任意一个的时间点,只有一个goroutine能够访问该值,既无需加锁,也无需同步。数据竞争从设计上就被杜绝了。这种思想,被总结为一句话:
不要通过共享内存来通信,而应通过通信来共享内存。

Go的并发处理方式源于Hoare的通信顺序处理(Communicating Sequential Processes, CSP)

一 并发处理

1.1 快速开始

在Go中,通过使用关键字go,可以快速创建一个goroutine,例如:

package main

import (
	"fmt"
	"time"
)

func main() {
   
	go printWord("A") // 开启一个新的goroutine
	printWord("C")
}

func printWord(s string) {
   
	for i := 0; i < 5; i++ {
   
		fmt.Println(s)
		time.Sleep(500 * time.Millisecond)
	}
}

上面一个例子中,在多核CPU系统中,会交替打印出字母"A"、“C”,在单核CPU中则稍有不同。

1.2 Goroutine

goroutine是一种轻量级的线程。它具有简单的模型:它与其它goroutine并发运行在同一地址空间,因此,访问共享的内存时必须进行同步(或者使用信道)。它的所有消耗几乎就只有栈空间的分配,而且栈最开始是非常小的,所以它们很廉价,仅在需要时才会随着堆空间的分配(和释放)而变化。

goroutine在多线程操作系统上可实现多路复用,因此若一个线程阻塞,比如说等待I/O,那么其它的线程会继续运行。goroutine的设计隐藏了线程创建和管理的诸多复杂性。

直接在函数或方法前添加go关键字即可在新的goroutine中调用它。当调用完成后,该goroutine也会安静地退出。(效果有点像Unix Shell中的&符号,它可以让命令在后台运行。)

有些地方将Goroutine翻译为Go程,但这个词感觉并不怎么合适(也不好听),所以本文索性就将其称之为goroutine

前面的示例中我们已经看到过:

func main() {
   
	go printWord("A") // 开启一个新的goroutine
	printWord("C")
}

func printWord(s string) {
   
	for i := 0; i < 5; i++ {
   
		fmt.Println(s)
		time.Sleep(500 * time.Millisecond)
	}
}

有时,为了简化程序,可以直接将函数定义为一个函数字面量(即匿名函数):

package main

import (
	"fmt"
	"time"
)

func main() {
   
	s := "hello"
	go func() {
    // 函数字面量,开启一个新的goroutine
		for i := 0; i < 5; i++ {
   
			fmt.Println(s+":", i)
			time.Sleep(500 * time.Millisecond)
		}
	}()

	// 等待主程序运行结束
	time.Sleep(3000 * time.Millisecond)
}

这些示例都是一些简单的函数,要想体现出Go的精妙,还需要配合另一种数据类型,即信道。

二 信道

信道(channel)是一种重要的数据类型,既可以用作信号量,也可以用于数据传递,其结果值充当了对底层数据结构的引用。信道具有方向,可选的信道操作符<-指定了通道的方向,发送或接收。如果没有给出方向,则它是双向的,信道可以通过分配或显式转换来限制只发送或接收。信道的零值是nil,尝试往未初始化的信道或已关闭的信道发送或接收值都会导致运行时恐慌。

chan<- float64  // 单向信道,只能用于发送float64的值
<-chan int      // 单向信道,只能用于接收int值

<-运算符与最左边的chan关联:

chan<- chan int    // 同 chan<- (chan int)
chan<- <-chan int  // 同 chan<- (<-chan int)
<-chan <-chan int  // 同 <-chan (<-chan int)
chan (<-chan int)

“箭头”就是数据流的方向。

2.1 无缓冲信道

和映射一样,在使用前需要先使用make初始化:

// 创建一个可用于发送和接收字符串类型的信道
c := make(chan string)

重新看一下本文开头的例子,如果我们只保留一个goroutine中的打印,会怎么样:

package main

import (
	"fmt"
	"time"
)

func main() {
   
	go printWord("A") // 开启一个新的goroutine
}

func printWord(s string) {
   
	for i := 0; i < 5; i++ {
   
		fmt.Println(s)
		time.Sleep(500 * time.Millisecond)
	}
}

结果发现,这次什么也没有输出。
为什么?一方面,新开启的goroutine不会阻塞当前的main函数,另一方面,一旦main函数执行完毕,整个程序也就结束,所以上面那个goroutine还没来得及打印,主程序就已经结束了。除了用time.Sleep函数让main函数等待一段时间,我们还可以借助信道来更优雅的实现:

package main

import (
	"fmt"
	"time"
)

func main() {
   
	c := make(chan bool)
	go printWord("A", c) // 开启一个新的goroutine
	<-c                  // 等待打印完毕,丢弃传递过来的值
}

func printWord(s string, c chan bool) {
   
	for i := 0; i < 5; i++ {
   
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值