GO语言入门指南:基础语法和常用特性解析(并发) | 青训营

随着硬件的发展,并发程序变得越来越重要。Web服务器会一次处理成千上万的请求。平板电脑和手机app在渲染用户画面同时还会在后台执行各种计算任务和网络请求。即使是传统的批处理问题--读取数据,计算,写输出--现在也会用并发来隐藏掉I/O的操作延迟以充分利用现代计算机设备的多个核心。

1. 并发与并行

并发(concurrency):指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,通过cpu时间片轮转使多个进程快速交替的执行。

并行(parallel):指在同一时刻,有多条指令在多个处理器上同时执行。

2. go 并发编程

go 的并发编程采用的 CSP (Communicating Sequential Process) 模型,主要基于协程 goroutine 和通道 channel .

2.1 go协程

2.1.1 协程基本概念

goroutine是Go语言中的一种轻量级线程,用于实现并发。与传统的线程相比,goroutine的创建和销毁的代价更小,并且可以高效地复用系统资源。

Goroutine 是 Go 语言支持并发的核心,在一个Go程序中同时创建成百上千个goroutine是非常普遍的,一个goroutine会以一个很小的栈开始其生命周期,一般只需要2KB。区别于操作系统线程由系统内核进行调度, goroutine 是由Go运行时(runtime)负责调度。例如Go运行时会智能地将 m个goroutine 合理地分配给n个操作系统线程,实现类似m:n的调度机制,不再需要Go开发者自行在代码层面维护一个线程池。

在Go语言编程中你不需要去自己写进程、线程、协程,你的技能包里只有一个技能------goroutine,当你需要让某个任务并发执行的时候,你只需要把这个任务包装成一个函数,开启一个 goroutine 去执行这个函数就可以了,就是这么简单粗暴。

在 go 语言中,并发编程使用关键字 go 即可快速启动一个并发运行的 goroutine. 只需在函数调⽤语句前添加 go 关键字,就可创建并发执⾏单元,但go关键字后面必须跟一个函数,不能是语句或其他,函数的返回值将被忽略。

2.1.1 启动方式

Goroutine启动的两种方式:有名函数和匿名函数

go 复制代码
匿名函数
package main

import "time"

func main() {
   go func() {
      sum := 0
      for i := 0; i < 1000; i++ {
         sum += i
      }
      println(sum)
   }()
   //设置等待时间,防止main函数提前退出
   time.Sleep(5 * time.Second)
}

有名函数
package main

import "time"

func sum() {
   sum := 0
   for i := 0; i < 1000; i++ {
      sum += i
   }
   println(sum)
}

func main() {
   go sum()
   //设置等待时间,防止main函数提前退出
   time.Sleep(5 * time.Second)
}

2.1.2 goroutine特性

  • Go的执行是非阻塞的,不会等待。
  • Go后面的函数的返回值会被忽略。调度器不能保证多个goroutine的执行次序。
  • 没有父子goroutine的概念,所有的goroutine是平等地被调度和执行的。
  • Go程序在执行时会单独为main函数创建一个goroutine,遇到其他go关键字时再去创建其他的goroutine。
  • Go 没有暴露 goroutine id 给用户,所以不能在一个goroutine里面显式地操作另一个goroutine,不过runtime 包提供了一些函数访问和设置 goroutine的相关信息。

2.2 channel

2.2.1 通道基本知识

单纯地将函数并发执行是没有意义的。函数与函数间需要交换数据才能体现并发执行函数的意义。虽然可以使用共享内存进行数据交换,但是共享内存在不同的 goroutine 中容易发生竞态问题。为了保证数据交换的正确性,很多并发模型中必须使用互斥量对内存进行加锁,这种做法势必造成性能问题。

Go语言采用的并发模型是CSP(Communicating Sequential Processes),提倡通过通信实现共享内存 而不是通过共享内存而实现通信 。如果说 goroutine 是Go程序并发的执行体,channel就是它们之间的连接。channel是可以让一个 goroutine 发送特定值到另一个 goroutine 的通信机制。

Go 语言中的通道(channel)是一种特殊的类型。通道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序。每一个通道都是一个具体类型的导管,也就是声明channel的时候需要为其指定元素类型。

golang 提供了通道类型 chan,用于在并发操作时的通信,它本身就是并发安全的. 通过 chan 可以创建无缓冲、缓冲通道,单向通道,满足不同需求.chan是引用类型的数据,在作为参数传递的时候,传递的是内存地址

  • 无缓冲通道: 要求接收和发送数据的 goroutine 同时准备好,否则将会阻塞.

  • 有缓冲通道: 给予通道一个容量值,只要有值便可以接受数据,有空间便可以发送数据,可以不阻塞的完成.

  • 单向通道: 默认情况通道是双向的,可以接收及发送数据. 也可以创建单向通道,只能收或者发数据. 如下是单向接受通道

声明的通道类型变量需要使用内置的make函数初始化之后才能使用。具体格式如下:

go 复制代码
通道 := make(chan 元素类型, [缓冲大小])

单向 channel 变量的声明非常简单,只能写入数据的通道类型为`chan<-`,只能读取数据的通道类型为`<-chan`,格式如下:

var 通道实例 chan<- 元素类型    // 只能写入数据的通道  
var 通道实例 <-chan 元素类型    // 只能读取数据的通道

ch := make(chan int)
// 声明一个只能写入数据的通道类型, 并赋值为ch
var chSendOnly chan<- int = ch
//声明一个只能读取数据的通道类型, 并赋值为ch
var chRecvOnly <-chan int = ch

2.2.2 通道发送和接收数据

接收和发送的语法:

kotlin 复制代码
data := <- a // read from channel a  
a <- data // write to channel a

在通道上箭头的方向指定数据是发送还是接收。

2.2.3 发送和接收默认是阻塞的

一个通道发送和接收数据,默认是阻塞的。当一个数据被发送到通道时,在发送语句中被阻塞,直到另一个Goroutine从该通道读取数据。相对地,当从通道读取数据时,读取被阻塞,直到一个Goroutine将数据写入该通道。

这些通道的特性是帮助Goroutines有效地进行通信,而无需像使用其他编程语言中非常常见的显式锁或条件变量。

示例代码:

go 复制代码
package main

import "fmt"

func main() {
	var ch1 chan bool       //声明,没有创建
	fmt.Println(ch1)        //<nil>
	fmt.Printf("%T\n", ch1) //chan bool
	ch1 = make(chan bool)   //是引用类型的数据
	fmt.Println(ch1)
	go func() {
		for i := 0; i < 10; i++ {
			fmt.Println("子goroutine中,i:", i)
		}
		// 循环结束后,向通道中写数据,表示要结束了。。
		ch1 <- true
		fmt.Println("----结束---")
	}()
	data := <-ch1 // 从ch1通道中读取数据
	fmt.Println("data-->", data)
	fmt.Println("----main over---")
}

运行结果:

在上面的程序中,我们先创建了一个chan bool通道。然后启动了一条子Goroutine,并循环打印10个数字。然后我们向通道ch1中写入输入true。然后在主goroutine中,我们从ch1中读取数据。这一行代码是阻塞的,这意味着在子Goroutine将数据写入到该通道之前,主goroutine将不会执行到下一行代码。因此,我们可以通过channel实现子goroutine和主goroutine之间的通信。当子goroutine执行完毕前,主goroutine会因为读取ch1中的数据而阻塞。从而保证了子goroutine会先执行完毕。这就消除了对时间的需求。在之前的程序中,我们要么让主goroutine进入睡眠,以防止主要的Goroutine退出。要么通过WaitGroup来保证子goroutine先执行完毕,主goroutine才结束。

再一个例子,这个程序将打印一个数字的各位数的平方和。

go 复制代码
package main

import (
	"fmt"
)

func calcSquares(number int, squareop chan int) {
	sum := 0
	for number != 0 {
		digit := number % 10
		sum += digit * digit
		number /= 10
	}
	squareop <- sum
}
func calcCubes(number int, cubeop chan int) {
	sum := 0
	for number != 0 {
		digit := number % 10
		sum += digit * digit * digit
		number /= 10
	}
	cubeop <- sum
}
func main() {
	number := 589
	sqrch := make(chan int)
	cubech := make(chan int)
	go calcSquares(number, sqrch)
	go calcCubes(number, cubech)
	squares, cubes := <-sqrch, <-cubech
	fmt.Println("Final output", squares+cubes)
}

运行结果:

2.2.4 死锁

使用通道时要考虑的一个重要因素是死锁。如果Goroutine在一个通道上发送数据,那么预计其他的Goroutine应该接收数据。如果这种情况不发生,那么程序将在运行时出现死锁。

类似地,如果Goroutine正在等待从通道接收数据,那么另一些Goroutine将会在该通道上写入数据,否则程序将会死锁。

示例代码:

go 复制代码
package main
func main() {  
    ch := make(chan int)
    ch <- 5
}

2.2.4 关闭通道

关闭 channel

关闭 channel 非常简单,直接使用Go语言内置的 close() 函数即可。 在介绍了如何关闭 channel 之后,我们就多了一个问题:如何判断一个 channel 是否已经被关闭?我们可以在读取的时候使用多重返回值的方式:

x, ok := <-ch

这个用法与 map 中的按键获取 value 的过程比较类似,只需要看第二个 bool 返回值即可,如果返回值是 false 则表示 ch 已经被关闭。

2.4 select多路复用

在某些场景下我们可能需要同时从多个通道接收数据。通道在接收数据时,如果没有数据可以被接收那么当前 goroutine 将会发生阻塞。Go 语言内置了select关键字,使用它可以同时响应多个通道的操作。

Select 的使用方式类似于之前学到的 switch 语句,它也有一系列 case 分支和一个默认的分支。每个 case 分支会对应一个通道的通信(接收或发送)过程。select 会一直等待,直到其中的某个 case 的通信操作完成时,就会执行该 case 分支对应的语句。具体格式如下:

go 复制代码
select {
case <-ch1:
	//...
case data := <-ch2:
	//...
case ch3 <- 10:
	//...
default:
	//默认操作
}

Select 语句具有以下特点。

  • 可处理一个或多个 channel 的发送/接收操作。
  • 如果多个 case 同时满足,select 会随机选择一个执行。
  • 对于没有 case 的 select 会一直阻塞,可用于阻塞 main 函数,防止退出。

3. 总结

  • goroutine是Go语言中的一种轻量级线程,用于实现并发编程。通过关键字"go"可以创建goroutine,并在其中并发执行任务。goroutine的创建和销毁的代价较小,可以高效地复用系统资源,同时可以创建成百上千甚至更多的goroutine。通过goroutine,可以同时执行多个任务,提高系统的吞吐量和响应速度。
  • 通道(channel)是用于实现goroutine之间通信和同步的一种特殊类型。通道可以看作是一种类型安全的消息队列,用于在goroutine之间传递数据。通道可以通过内置的make函数创建,可以是有缓冲的或无缓冲的。无缓冲通道保证发送和接收的同步,发送和接收操作会阻塞,直到发送者和接收者都准备好。有缓冲通道可以在一定程度上解耦发送和接收操作,发送操作只有在通道缓冲区已满时才会阻塞,接收操作只有在通道缓冲区为空时才会阻塞。通道的发送和接收操作默认是阻塞的,可以通过select语句结合default子句实现非阻塞的发送和接收操作。通道可以实现线程安全的数据传递和同步,避免了共享内存带来的竞争条件和死锁问题。
相关推荐
Find1 个月前
MaxKB 集成langchain + Vue + PostgreSQL 的 本地大模型+本地知识库 构建私有大模型 | MarsCode AI刷题
青训营笔记
理tan王子1 个月前
伴学笔记 AI刷题 14.数组元素之和最小化 | 豆包MarsCode AI刷题
青训营笔记
理tan王子1 个月前
伴学笔记 AI刷题 25.DNA序列编辑距离 | 豆包MarsCode AI刷题
青训营笔记
理tan王子1 个月前
伴学笔记 AI刷题 9.超市里的货物架调整 | 豆包MarsCode AI刷题
青训营笔记
夭要7夜宵1 个月前
分而治之,主题分片Partition | 豆包MarsCode AI刷题
青训营笔记
三六1 个月前
刷题漫漫路(二)| 豆包MarsCode AI刷题
青训营笔记
tabzzz1 个月前
突破Zustand的局限性:与React ContentAPI搭配使用
前端·青训营笔记
Serendipity5651 个月前
Go 语言入门指南——单元测试 | 豆包MarsCode AI刷题;
青训营笔记
wml1 个月前
前端实践-使用React实现简单代办事项列表 | 豆包MarsCode AI刷题
青训营笔记
用户44710308932421 个月前
详解前端框架中的设计模式 | 豆包MarsCode AI刷题
青训营笔记