Golang学习之Goroutines和 channel

本文深入解析Go语言中的goroutine和channel机制,阐述其如何实现高效并发处理,包括goroutine的启动与执行,channel的通信原理及应用,以及单向通道和缓冲通道的特性。

Goroutines

在Go语言中,每一个并发的执行单元叫作一个goroutine,可以简单地把goroutine类比作一个线程,本质上是一个协程。但它与协程有俩点不同:

  • goroutinue可以实现并行,也就是说,多个协程可以在多个处理器同时跑。而协程同一时刻只能在一个处理器上跑(把宿主语言想象成单线程的就好了)。
  • goroutine之间的通信是通过channel,而协程的通信是通过yield和resume()操作。

在Go里实现goroutine非常简单,只需要在函数的调用前面加关键字go即可:

go fun()

下面的例子演示,启动10个goroutines分别打印索引:

package main
import (
"fmt"
"time"	
)

func main() {
	for i:=1;i<10;i++ {
		go func(i int) {
			fmt.Println(i)
		}(i)
	}
	//暂停一会,保证打印全部结束
	time.Sleep(1e9)
}

上面的例子中,启动了10个goroutine,再加上main函数的主goroutine,总共有11个goroutines。由于goroutine类似于”守护线程“,如果主goroutine不等待片刻,可能程序就没有输出打印了。上面的例子输出如下:(输出的索引是完全随机的)

5
4
8
7
9
6
1
3
2

Channels

如果说goroutine是Go语言程序的并发体的话,那么channels则是它们之间的通信机制。一个channel是一个通信机制,它可以让一个goroutine通过它给另一个goroutine发送值信息。每个channel都有一个特殊的类型,也就是channels可发送数据的类型。一个可以发送int类型数据的channel一般写为chan int。

channel可以形象比喻为工厂里的传送带,一头的生产者goroutine往传输带放东西,另一头的消费者goroutinue则从输送带取东西。channel实际上是一个有类型的消息队列,遵循先进先出的特点。

声明一个通道很简单,我们使用chan关键字即可,除此之外,还要指定通道中发送和接收数据的类型,这样我们才能知道,要发送什么类型的数据给通道,也知道从这个通道里可以接收到什么类型的数据。

ch:=make(chan int)

通道类型和Map这些类型一样,可以使用内置的make函数声明初始化,这里我们初始化了一个chan int类型的通道,所以我们只能往这个通道里发送int类型的数据,当然接收也只能是int类型的数据。

一个channel有发送和接受两个主要操作,都是通信行为。一个发送语句将一个值从一个goroutine通过channel发送到另一个执行接收操作的goroutine。发送和接收两个操作都使用<-运算符。在发送语句中,<-运算符分割channel和要发送的值。在接收语句中,<-运算符写在channel对象之前。一个不使用接收结果的接收操作也是合法的。

ch <- 2 //发送数值2给这个通道
x:=<-ch //从通道里读取值,并把读取的值赋值给x变量
<-ch //从通道里读取值,然后忽略

通道我们还可以使用内置的close函数关闭。

close(ch)

如果一个通道被关闭了,我们就不能往这个通道里发送数据了,如果发送的话,会引起painc异常。但是,我们还可以接收通道里的数据,如果通道里没有数据的话,接收的数据是nil。

刚刚我们使用make函数初始化的时候,只有一个参数,其实make还可以有第二个参数,用于指定通道的大小。默认没有第二个参数的时候,通道的大小为0,这种通道也被称为无缓冲通道。如果通道的容量大于零,那么该通道就是带缓冲的通道。

ch = make(chan int)    // unbuffered channel
ch = make(chan int, 0) // unbuffered channel
ch = make(chan int, 3) // buffered channel with capacity 3

无缓冲通道

无缓冲的通道指的是通道的大小为0,也就是说,这种类型的通道在接收前没有能力保存任何值,它要求发送goroutine和接收goroutine同时准备好,才可以完成发送和接收操作。

从上面无缓冲的通道定义来看,发送goroutine和接收gouroutine必须是同步的,同时准备后,如果没有同时准备好的话,先执行的操作就会阻塞等待,直到另一个相对应的操作准备好为止。这种无缓冲的通道我们也称之为同步通道。

func main() {
    ch := make(chan int)
    go func() {
            var sum int = 0
            for i := 0; i < 10; i++ {
                sum += i
            }
            ch <- sum
    }()

    fmt.Println(<-ch)

}

在计算sum和的goroutine没有执行完,把值赋给ch通道之前,fmt.Println(<-ch)会一直等待,所以main主goroutine就不会终止,只有当计算和的goroutine完成后,并且发送到ch通道的操作准备好后,同时<-ch就会接收计算好的值,然后打印出来。

单向通道

有时候,我们有一些特殊场景,比如限制一个通道只可以接收,但是不能发送;有时候限制一个通道只能发送,但是不能接收,这种通道我们称为单向通道。
定义单向通道也很简单,只需要在定义的时候,带上<-即可。

var send chan<- int //只能发送
var receive <-chan int //只能接收

注意<-操作符的为止,在后面是只能发送,对应发送操作;在前面是只能接收,对应接收操作。
单向通道应用于函数或者方法的参数比较多,比如;

func counter(out chan<- int) {
}

缓冲通道

在无缓冲通道的基础上,为通道增加一个有限大小的存储空间形成带缓冲通道。带缓冲通道在发送时无需等待接收方接收即可完成发送过程,并且不会发生阻塞,只有当存储空间满时才会发生阻塞。同理,如果缓冲通道中有数据,接收时将不会发生阻塞,直到通道中没有数据可读时,通道将会再度阻塞。
创建缓冲通道:

通道实例 := make(chan 通道类型, 缓冲大小)

下面通过一个例子中来理解带缓冲通道的用法,参见下面的代码:

package main
import "fmt"
func main() {
    // 创建一个3个元素缓冲大小的整型通道,带缓冲的通道在创建完成时,内部的元素是空的,因此使用 len() 获取到的返回值为 0。
    ch := make(chan int, 3)
    // 查看当前通道的大小
    fmt.Println(len(ch))
    // 发送3个整型元素到通道,因为使用了缓冲通道。即便没有 goroutine 接收,发送者也不会发生阻塞。
    ch <- 1
    ch <- 2
    ch <- 3
    // 查看当前通道的大小,由于填充了 3 个通道,此时的通道长度变为 3。
    fmt.Println(len(ch))
}

运行结果是:

0
3
### GolangChannel 的用法与示例 ChannelGo 语言中的一个重要特性,用于在 Goroutines 之间安全地传递数据。它提供了一种简洁的方式来实现并发控制通信。 #### 创建初始化 Channel 可以使用 `make` 函数创建一个新的 channel。下面是一个简单的例子: ```go ch := make(chan int) ``` 此代码片段定义了一个整数类型的 channel[^1]。 #### 发送接收数据 通过 `<-` 运算符可以在 channel 上发送或接收数据。以下是基本操作的例子: ```go // 向 channel 发送数据 ch <- value // 从 channel 接收数据 value := <-ch ``` 这些语句分别表示向 channel 发送数据channel 获取数据的操作[^2]。 #### 使用带缓冲区的 Channel 无缓冲的 channel 只有当接收方准备好时才会继续执行;而带缓冲的 channel 则允许存储一定数量的数据项而不阻塞。可以通过指定第二个参数来设置缓冲大小: ```go bufferedCh := make(chan int, 10) ``` 这里创建了一个容量为 10 的带缓冲 channel[^3]。 #### 关闭 Channel 遍历其值 关闭一个 channel 表明不会再有任何新的值被写入其中。这通常发生在生产者完成所有工作之后。消费者可以通过 range 循环自动处理直到 channel 被关闭为止的所有值: ```go close(ch) for val := range ch { fmt.Println(val) } ``` 注意,在多生产者的场景下应小心管理谁负责关闭 channel,以免引发 panic 错误。 #### Select 语句支持多个通道操作 Go 提供了 select 语句用来等待多个 communication 操作之一变得可用。这是一个典型的非阻塞 I/O 实现方式: ```go select { case msg1 := <-ch1: fmt.Println("received", msg1) case ch2 <- "ping": default: fmt.Println("no activity") } ``` 上述代码展示了如何监听来自不同 channels 的消息或者尝试发送一条新消息给某个 channel。 #### 完整示例:计数器应用 以下展示的是利用 goroutine unbuffered channel 构建的一个简单计数器应用程序: ```go package main import ( "fmt" ) func counter(out chan<- int) { for i := 0; i < 100; i++ { out <- i // 将当前数值传送到 out channel } close(out) // 结束后关闭该 channel } func square(in <-chan int, out chan<- int) { for v := range in { // 遍历输入 channel 的每一个值 out <- v * v // 计算平方并传送至下一个 stage } close(out) // 处理完成后关闭 output channel } func printer(in <-chan int) { for v := range in { // 打印接收到的所有值 fmt.Println(v) } } func main() { naturals := make(chan int) squares := make(chan int) go counter(naturals) go square(naturals, squares) printer(squares) } ``` 在这个程序里,我们启动三个独立的任务——生成自然数序列、计算它们各自的平方以及打印最终的结果集。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值