GoWeb——用goroutine和通道实现并发

文章详细介绍了Go语言中的goroutine和通道,包括goroutine的开启、调度原理以及通道的定义、声明、创建、数据发送与接收。goroutine是Go的轻量级线程,通过`go`关键字启动,采用协同式调度。通道作为goroutine间安全通信的机制,遵循“先入先出”原则,支持同步和异步操作。文章还提到了通道的缓冲区、select多路复用以及遍历和关闭通道的操作。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1、goroutine简介

1.1、goroutine

Go语言的并发机制运用起来非常简便:只需要通过go关键字来开启goroutine,和其他编程语言相比这种方式更加轻量。

开启一个goroutine的形式如下:

go foo(a,b,c)

在函数foo(a,b,c)之前加上go关键字,就开启了一个新的goroutine。函数名可以是包含func关键字的匿名函数:

//创建一个匿名函数并开启goroutine
go func(paraml,param2){
}(va11,va12).

开启goroutine的示例如下。

import (
	"fmt"
	"time"
)

func main() {
	go Echo("go")
	Echo("web programe")
}

func Echo(s string) {
	for i := 0; i < 3; i++ {
		time.Sleep(100 * time.Millisecond)
		fmt.Println(s)
	}
}

执行以上代码后会看到,输出的“go”和“web program”是没有固定先后顺序。因为它们是两个goroutine在并发执行:

go
web programe
web programe
go
go
web programe

通过上面的示例可以看到,利用go关键字很方便地实现并发编程。多个goroutine运行在同一个进程中,共享内存数据。Go语言遵循“不通过共享内存来通信,而通过通信来共享内存”原则。

1.2、goroutine的调度

goroutine的调度方式是协同式的。在协同式调度中没有“时间片”的概念。为了并行执行goroutine,调度器会在以下几个时刻对其进行切换:

  • 在通道发送或者接收数据且造成阻塞时。
  • 在一个新的goroutine被创建时。
  • 在可以造成系统调用被阻塞时,如在进行文件操作时。

goroutine在多核CPU环境下是并行的。如果代码块在多个goroutine中执行,则会实现代码的并行。在被调用的函数返回时,这个goroutine也自动结束。需要注意的是,如果这个函数有返回值,则该返回值会被丢弃。看下面的代码:

func main() {
	for i := 0; i < 5; i++ {
		go Add(i, i)
	}
}

func Add(a, b int) {
	c := a + b
	fmt.Println(c)
}

执行上面的代码会发现,屏幕什么也没打印出来,程序就退出了。对于上面的例子,main函数启动了5个goroutine,这时程序就退出了,而被启动的执行Add()函数的goroutine没来得及执行。

如果要让main()函数等待所有goroutine退出后再返回,则需要知道goroutine是何时退出的。但如何知道goroutine都退出了呢?这就引出了多个goroutine之间通信的问题。

2、通道

2.1、通道的定义

通道(channel)是用来传递数据的一个数据结构。Go语言提倡使用通信来代替共享内存。当一个资源需要在goroutine之间共享时,通道在goroutine之间架起了一个管道,并提供了确保同步交换数据的机制。

在声明通道时,需要指定将要被共享的数据的类型。可以通过通道共享内置类型、命名类型、结构类型和引用类型的值或者指针。

Go语言中的通道是一种特殊的类型。在任何时候,同时只能有一个goroutine访问通道进行发送和接收数据,如图下图所示。

在这里插入图片描述

在地铁站、火车站、机场等公共场所人很多的情况下,大家养成了排队的习惯。目的是避免拥挤、插队导致低效的资源使用和交换。代码与数据也是如此,多个goroutine为了争抢数据,势必造成执行的低效率。

使用队列的方式是最高效的,通道是一种与队列类似的结构。通道总是遵循“先入先出(First In First Out)”的规则,从而保证收发数据的顺序。

2.2、通道的声明

通道本身需要用一个类型进行修饰,就像切片类型需要标识元素类型。通道的元素类型就是在其内部传输的数据类型。通道的声明形式如下:

var:channel name chan type

说明如下:

  • channel name:保存通道的变量。
  • type:通道内的数据类型。
  • chan:类型的空值是nil,声明后需要配合make后才能使用。

2.3、创建通道

通道是引用类型,需要使用make()函数进行创建,格式如下:

通道实例:= make(chan数据类型)

说明如下:

  • 数据类型:通道内传输的元素类型。
  • 通道实例:通过make0函数创建的通道句柄。

创建通道的示例如下:

ch1 := make(chan string)//创建一个字符串类型的通道
ch2 := make(chan interface{})//创建一个空接口类型的通道,可以存放任意格式
type Signal struct{/* 一些字段 */}
ch3 := make(chan *Signal)//创建Singal指针类型的通道,可以存放*Signal

2.4、用通道发送数据

在通道创建后,就可以使用通道发送和接收数据了。

1. 用通道发送数据的格式。

用通道发送数据使用特殊的操作符“<-”,格式如下:

通道变量<-通道值

说明如下。

  • 通道变量:通过make0函数创建好的通道实例。
  • 通道值:可以是变量、常量、表达式或者函数返回值等。通道值的类型必须与C通道中的元素类型一致。

2. 通过通道发送数据的例子。

在使用make()函数创建一个通道后,就可以使用“<-”向通道发送数据了。代码如下:

ch := make(chan interface{})//创建一个空接口通道
ch <- 6//将6放入通道中
ch <- "love"//将love字符串放入通道中

3. 发送将持续阻塞直到数据被接收。

在把数据往通道中发送时,如果接收方一直都没有接收,则发送操作将持续阻塞。Go程序在运行时能智能地发现一些永远无法发送成功的语句并做出提示。示例代码如下。

func main() {
	//创建一个字符串通道
	ch := make(chan string)
	//尝试将sleep通过通道发送
	ch <- "sleep"
}
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:
main.main()
        /Users/acton_zhang/go/gobook/src/bookWebPro/chapter7/channel.go:7 +0x31

错误的意思是:在运行时发现所有的goroutine(包括main()函数对应的goroutine)都处于等待状态,即所有goroutine中的通道并没有形成发送和接收的状态。

2.5、用通道接收数据

通道接收同样使用“<-”操作符。用通道接收数据有如下特性:

  • 通道的发送和接收操作在不同的两个goroutine间进行。由于通道中的数据在没有接收方,接收时会持续阻塞,所以通道的接收必定在另外一个goroutine中进行。
  • 接收将持续阻塞直到发送方发送数据。
  • 如果在接收方接收时,通道中没有发送方发送数据,则接收方也会发生阻塞,直到发送方发送数据为止。
  • 通道一次只能接收1个数据元素。

通道的数据接收一共有以下4种写法。

1. 阻塞接收数据:

阻塞模式在接收数据时,将接收变量作为“<-”操作符的左值,格式如下:

data := <-ch

执行该语句将会阻塞,直到接收到数据并赋值给data变量。

2. 非阻塞接收数据:

是使用非阻塞方式从通道接收数据时,语句不会发生阻塞,格式如下:

data, ok := <-ch
  • data:接收到的数据。在未接收到数据时,data为通道类型的零值。
  • ok:是否接收到数据。

3. 接收任意数据,忽略掉接收的数据:

利用下面这写法,通道在接收到数据后将其忽略掉:

<-ch

执行该语句会发生阻塞,直到接收到数据,但接收到的数据会被忽略。这个方式实际上只是通过通道在goroutine间阻塞收发,从而实现并发同步。

使用通道做并发同步的示例如下。

import "fmt"

func main() {
	ch := make(chan string) //构建一个通道
	go func() {
		fmt.Println("开始goroutine") //通过通道通知main()函数的goroutine
		ch <- "signal"
		fmt.Println("退出goroutine")
	}()
	fmt.Println("等待goroutine")
	<-ch //等待匿名gotoutine
	fmt.Println("完成")
}
等待goroutine
开始goroutine
退出goroutine
完成

4. 循环接收数据:

通道的数据接收可以借用for-range语句进行多个元素的接收操作。格式如下:

for data =range ch

通道c是可以被遍历的,遍历的结果就是接收到的数据,数据类型就是通道的数据类型。通过for遍历获得的变量只有一个,即上面例子中的data。遍历通道数据的示例如下。

import (
	"fmt"
	"time"
)

func main() {
	ch := make(chan int) //构建一个通道
	go func() {                   //开启一个并发匿名函数
		for i := 6; i <= 8; i++ { //从6循环到8
			ch <- i                 //发送6~8之间的数值
			time.Sleep(time.Second) //每次发送完时等待
		}
	}()

	for reveice := range ch { //遍历接收通道数据
		fmt.Println(reveice) //打印通道数据
		if reveice == 8 {    //当遇到数据8时,退出接收循环
			break
		}
	}
}
6
7
8

通道可用于在两个goroutine之间通过传递一个指定类型的值来同步运行和通讯。操作符“<-”用于指定通道的方向、发送和接收。如果未指定方向,则为双向通道。

ch <- v
//把v发送到通道ch中
v := <-ch“//从ch接收数据,并把值赋给v

以下示例通过连个鬼goroutine来计算数字之和:

func main() {
	s := []int{6, 7, 8, -9, 1, 8}
	ch := make(chan int)
	go sum(s[:len(s)/2], ch)
	go sum(s[len(s)/2:], ch)
	a, b := <-ch, <-ch //从通道ch中接收
	fmt.Println(a, b, a+b)
}

func sum(s []int, ch chan int) {
	sum := 0
	for _, v := range s {
		sum += v
	}
	ch <- sum
}
0 21 21

2.6、通道缓冲区

通道可以设置缓冲区一通过make()函数的第2个参数指定缓冲区的大小。示例如下:

ch make (chan int,66)

带缓冲区的通道,允许发送方的数据发送和接收端的数据获取处于异步状态。即发送方发送的数据可以放在缓冲区中,等待接收端去接收数据,而不是立刻需要接收端去接收数据。

不过由于缓冲区的大小是有限的,所以还是必须有接收端来接收数据的,否则缓冲区一满,数据发送方就无法再发送数据了。

如果通道不带缓冲,则发送方会阻塞,直到接收方从通道中接收了数据。如果通道带缓冲,则发送方会阻塞,直到发送的值被复制到缓冲区中;如果缓冲区已满,则意味着需要等待直到某个接收方接收了数据。接收方在有值可以接收之前,会一直阻塞。

import "fmt"

func main() {
	//定义一个可以存储整数类型的、带缓冲的通道
	ch := make(chan int, 3)
	//因为ch是带缓冲的通道,所以可以同时发送多个数据,而不用立刻去同步接收数据
	ch <- 6
	ch <- 7
	ch <- 8 
	//接收这3个数据
	fmt.Println(<-ch)
	fmt.Println(<-ch)
	fmt.Println(<-ch)
}

2.7、select多路复用

在UNIX中,select()函数用来监控一组描述符,该机制常被用于实现高并发的Socket服务器程序。Go语言直接在语言级别支持select关键字,用于处理异步I/O问题。其用法示例如下:

select{
	case <-ch1:
	//如果ch1通道发送成功,则该case会接收到数据
	case ch2 <- 1:
	//如果ch2接收数据成功,则该case会收到数据
	default:
	//默认分支
}

select默认是阻塞的,只有当监听的通道中有发送或接收可以进行时才会运行。当多个通道都准备好后,select会随机地选择一个操作(发送或接收)来执行。

Go语言没有对通道提供直接的超时处理机制,但可以利用select来间接实现,例如:

import (
	"fmt"
	"time"
)

func main() {
	ch := make(chan string)
	timeout := make(chan bool, 1)
	go func() {
		time.Sleep(6 * time.Second)
		timeout <- true
	}()
	go func() {
		time.Sleep(2 * time.Second)
		ch <- "hello"
	}()
	select {
	case <-ch: //从ch通道中读取到数据
		fmt.Println("读取到了数据")
	case <-timeout: //没有从ch通道中读取到数据,但从timeout通道中读取到了数据
		fmt.Println("已超时")
	}
}

这样使用select就可以避免永久等待的问题。因为程序会在“timeout”通道中接收到一个数据后继续执行,无论对ch通道的接收是否还处于等待状态。

2.8、遍历通道与关闭通道

Go语言通过range关键字来实现遍历读取数据,类似于与数组或切片。格式如下:

v,ok := <-ch

如果通道接收不到数据,则ok的值是false。这时就可以使用close()函数来关闭通道。

通过range关键字实现遍历的示例如下。

import "fmt"

func main() {
	ch := make(chan int, 6)
	go fibonacci(cap(ch), ch)
	for j := range ch {
		fmt.Println(j)
	}
}

func fibonacci(n int, ch chan int) {
	a, b := 0, 1
	for i := 0; i < n; i++ {
		ch <- a
		a, b = b, a+b
	}
	close(ch)
}
0
1
1
2
3
5
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值