【Go系列】 Goroutine和Channel

承上启下

在前面的文章中,我们介绍的内容和其他的高级编程语言都一样,而今天这篇要介绍的Goroutine和Channel应该就是Go语言的核心了。语言和语言之间的差异性不大,但是为什么Go语言能够异军突起,除了它的跨平台编译性以外,goroutine和channel应该是很重要的一个原因。Goroutine我们可以简单地理解成C#上的Coroutine或者说是Python的协程,也可以认为是thread,但是它却没有对应的控制句柄,没法直接thread.close或者stop这种方法。这就是它的机制区别,我们今天这篇文章会重点介绍一下。

开始学习

我们都知道线程是CPU调度的单位,CPU的时间片是分配给各个线程进行使用的,而切换线程时,就需要保存上一个线程的现场,加载下一个线程的现场,然后再开始运行。这就叫做上下文切换,这块内容由寄存器,程序计数器等模块实现。今天介绍的Goroutine实际上是运行的,它也是一种线程,但是它是一种轻量级线程,它的上下文是由go runtime实现的,我们接下来详细介绍一下。

Goroutine

Goroutine是Go语言实现并发编程的基础。它是一种轻量级的线程,由Go运行时(runtime)管理。以下是Goroutine的一些特点:

  1. 轻量级:Goroutine的栈是动态分配的,初始大小很小,通常是2KB,并且在需要时可以扩展和收缩,这使得创建和销毁Goroutine的成本非常低。

  2. 高效:由于Goroutine的轻量级特性,一个Go程序可以轻松创建数以万计的Goroutine。

  3. 简单易用 :在Go中,只需使用go关键字即可启动一个新的Goroutine。例如:

    go funcName(params)
    

    或者直接在函数调用前加上go

    go func(params) {
        // 执行代码
    }(args)
    
  4. 调度:Goroutine的调度是由Go运行时管理的,它使用了一种称为M:N调度的技术,其中M个操作系统线程可以映射到N个Goroutine。

和其他高级语言的区别:

Goroutine、Java线程、C#的Coroutine以及Python的协程都是各自编程语言中用于并发执行的机制,但它们在实现细节和用法上有所不同。以下是它们之间的主要区别:

Goroutine (Go)
  • 轻量级:Goroutine非常轻量,初始栈大小通常为2KB,并且栈的大小可以根据需要动态调整。
  • 调度:Goroutine的调度是由Go的运行时(runtime)管理的,使用M:N调度模型,即多个Goroutine可能会映射到少量的OS线程上。
  • 通信:Goroutine之间通过Channel进行通信,这提供了一种基于消息传递的并发模型,减少了锁的使用。
  • 栈管理:Goroutine的栈是自动管理的,可以根据需要动态扩展和收缩。
Java线程
  • 重量级:Java线程是操作系统的原生线程,每个线程都会占用较大的内存空间,通常为1MB。
  • 调度:Java线程的调度是由JVM和操作系统共同管理的,通常采用1:1调度模型,即每个Java线程直接映射到一个OS线程。
  • 同步:Java线程通常使用synchronized关键字、锁(如ReentrantLock)和并发工具类(如Semaphore、CountDownLatch)进行同步。
  • 栈管理:Java线程的栈大小是固定的,在创建线程时需要指定,通常默认为1MB。
C#的Coroutine (Unity中常用)
  • 协作式:Coroutine是一种特殊的函数,它在执行过程中可以暂停并在未来的某个时间点恢复,它依赖于Unity引擎的协同程序调度器。
  • 控制流 :Coroutine通过yield return语句来暂停执行,并在下一次迭代或特定事件发生时恢复。
  • 调度:Coroutine通常用于Unity游戏开发中,以实现更平滑的游戏循环和资源管理,它们不是操作系统级别的线程。
  • 并发:Coroutine不是真正的并发执行,它们是单线程的,但在Unity引擎中可以用来模拟并发行为。
Python的协程
  • 协作式 :Python的协程是使用async/await语法实现的,它们也是协作式的,可以在等待I/O操作时让出控制权。
  • 异步I/O:Python的协程非常适合处理异步I/O操作,可以有效地提高I/O密集型应用程序的性能。
  • 调度:Python的协程通常由事件循环(如asyncio)管理,而不是由操作系统线程调度。
  • 并发:虽然Python的协程在单个线程内执行,但它们可以与事件循环一起使用,以实现并发行为。

与线程对比

Goroutine和线程是两种不同的并发执行单元,它们在概念、实现和用法上都有显著的区别。以下是Goroutine和线程之间的详细比较:

调度和管理
  • 线程:线程是由操作系统(OS)管理的执行单元。在大多数操作系统中,线程的调度是抢占式的,操作系统负责决定哪个线程在何时运行,以及运行多长时间。
  • Goroutine:Goroutine是由Go语言的运行时(runtime)管理的轻量级执行单元。Go运行时使用协作式调度,Goroutine会在执行某些操作(如I/O、Channel操作、等待锁等)时主动让出CPU,让其他Goroutine运行。
资源开销
  • 线程:线程通常有较大的内存占用,因为每个线程都有自己的堆栈空间(例如,在Linux上通常是8MB)。线程的数量受限于系统的内存大小和CPU核心数。
  • Goroutine:Goroutine非常轻量,初始栈大小通常为2KB,并且栈的大小可以根据需要动态调整。一个Go程序可以轻松创建数以万计的Goroutine。
创建和销毁开销
  • 线程:创建和销毁线程的开销相对较大,因为涉及到与操作系统交互。
  • Goroutine:Goroutine的创建和销毁非常快速和廉价,因为它们是由Go运行时管理的,并且不需要与操作系统进行交互。
同步机制
  • 线程:线程通常使用锁(如互斥锁、读写锁)、条件变量、信号量等同步机制来控制对共享资源的访问。
  • Goroutine:Goroutine推荐使用Channel进行通信,这鼓励了一种基于消息传递的并发模型,而不是传统的共享内存模型。虽然Goroutine也可以使用锁,但Channel通常是首选的同步方式。
栈空间
  • 线程:线程的栈空间通常是固定的,在创建线程时就需要指定。
  • Goroutine:Goroutine的栈是动态分配的,可以根据需要动态扩展和收缩,这进一步减少了内存的使用。
状态和上下文切换
  • 线程:线程的上下文切换涉及到保存和恢复寄存器状态、程序计数器等,这是一个相对昂贵的操作。
  • Goroutine:Goroutine的上下文切换通常更轻量,因为它们是在用户空间内由Go运行时管理的。
系统调用
  • 线程:线程可以执行任何系统调用,包括阻塞调用。
  • Goroutine:Goroutine在执行系统调用时,可能会阻塞整个线程。为了解决这个问题,Go运行时会将阻塞的系统调用转换为非阻塞调用,并在必要时启动新的线程。
并行与并发
  • 线程:线程可以实现真正的并行计算,特别是在多核处理器上,每个线程可以在不同的CPU核心上同时运行。
  • Goroutine:Goroutine本身不是并行执行的,但Go运行时可以将Goroutine调度到不同的线程上,从而在多核处理器上实现并行计算。

Channel

Channel是Go语言用于在Goroutine之间进行通信的机制。它是Go并发模型的重要组成部分。

  1. 定义:Channel是Go中的一种特殊类型,用于在不同的Goroutine之间传递值。

  2. 创建 :使用make函数创建Channel,可以指定Channel的类型和容量。例如:

    ch := make(chan int)       // 无缓冲的整型Channel
    ch := make(chan int, 10)   // 有缓冲的整型Channel,容量为10
    
  3. 发送和接收 :使用<-操作符在Channel上发送和接收数据。例如:

    ch <- value   // 发送值到Channel
    value := <-ch // 从Channel接收值
    
  4. 同步:Channel的发送和接收操作默认是阻塞的,直到另一端准备好。这使得Goroutine可以在不使用锁或其他同步机制的情况下进行通信。

  5. 关闭 :可以使用close函数关闭Channel。关闭后的Channel不能再发送数据,但可以继续接收已发送的数据。

  6. 范围循环 :可以使用for range循环从Channel中读取数据,直到Channel被关闭。

    for value := range ch {
        // 处理value
    }
    

为什么channel是并发安全

在Go语言中,Channel是并发安全的,这是因为其设计确保了在多个Goroutine之间进行通信时不会发生数据竞争。以下是Channel并发安全性的几个关键点:

内置同步机制

Channel的操作(发送和接收)是同步的。这意味着当一个Goroutine向Channel发送数据时,它会阻塞直到另一个Goroutine从该Channel接收数据。同样,当Goroutine从Channel接收数据时,它会阻塞直到有数据可以接收。这种同步机制确保了一次只有一个Goroutine可以访问Channel中的数据,从而避免了并发访问的问题。

通信顺序保证

Channel保证了数据传输的顺序性。发送到Channel的数据会按照发送的顺序被接收。这种顺序性意味着Goroutine不会看到不一致的状态,因为它们总是按照特定的顺序处理数据。

原子操作

Channel的操作(发送、接收和关闭)是不可分割的原子操作。这意味着这些操作在执行过程中不会被中断,从而保证了操作的完整性。例如,发送操作包括将数据放入Channel和通知接收方,这两个步骤是作为一个单一的操作执行的。

数据所有权转移

当一个值被发送到一个Channel时,该值的所有权从发送方转移到了接收方。这种所有权的转移避免了多个Goroutine同时拥有相同数据的问题,因为任何时候只有一个Goroutine可以拥有Channel中的数据。

缓冲与无缓冲Channel
  • 无缓冲Channel:无缓冲Channel在发送和接收之间没有存储空间。这意味着发送操作必须等待接收操作,反之亦然。这种设计确保了在数据交换时Goroutine之间的同步。
  • 缓冲Channel:即使是有缓冲的Channel,其内部实现也通过锁或其他同步机制来保护数据结构,确保并发访问时的安全性。当缓冲区满时,发送操作会阻塞,直到有空间可用;当缓冲区为空时,接收操作会阻塞,直到有数据可以接收。
关闭Channel的安全性

关闭Channel的操作也是安全的。当Channel被关闭时,所有阻塞的接收操作都会立即返回一个零值,并且发送操作会收到一个错误,表明Channel已被关闭。这种机制确保了在Channel关闭后,所有相关的Goroutine都能优雅地处理这种情况。

Goroutine和Channel的结合

Goroutine和Channel通常一起使用,以实现高效和简洁的并发编程模式。以下是一个简单的例子,展示了如何在Goroutine之间使用Channel进行通信:

func main() {
    message := make(chan string)

    go func() {
        message <- "Hello, World!"
    }()

    msg := <-message
    fmt.Println(msg)
}

在这个例子中,我们创建了一个字符串类型的Channel message,然后启动了一个Goroutine来发送消息。主Goroutine等待接收这个消息,并将其打印出来。

通过Goroutine和Channel的结合,Go语言提供了一种优雅的并发编程方式,使得开发者能够轻松地处理复杂的并发任务。

为什么不提供Goroutine句柄

Go语言的设计哲学鼓励使用通信(通过Channel)来共享内存,而不是通过共享内存来通信。这种设计选择影响了许多并发编程的模式,包括Goroutine的退出机制。以下是为什么Go倾向于使用Channel而不是提供句柄来控制Goroutine退出的几个原因:

通信顺序进程(CSP)

Go语言受到通信顺序进程(Communicating Sequential Processes,CSP)的影响,这是一种并发计算模型,它强调通过消息传递进行进程间通信,而不是共享状态。Channel是实现这一模型的核心组件,因此使用Channel来控制Goroutine的退出是Go语言设计哲学的自然延伸。

更清晰的并发模型

使用Channel来控制Goroutine的退出可以使并发模型更加清晰和一致。Goroutine通过Channel接收信号来决定何时退出,这样可以明确地表达它们之间的依赖关系和通信模式。

避免共享状态

如果提供句柄(例如引用或标识符)来控制Goroutine,那么就需要一种机制来修改或检查这个句柄的状态,这通常涉及到共享内存。共享内存可能导致竞态条件,需要额外的同步机制(如锁)来管理,这会增加程序的复杂性和出错的可能性。

简化资源管理

使用Channel可以让Goroutine在完成工作后自然地退出,而不是被迫等待一个外部信号。这有助于简化资源管理,因为Goroutine可以在不再需要时立即释放资源。

安全性和封装性

通过Channel控制Goroutine的退出可以更好地封装内部状态和行为。Goroutine可以决定何时以及如何响应退出信号,而不是由外部实体直接控制其生命周期。这有助于保持Goroutine的独立性和安全性。

相关推荐
jzpfbpx1 分钟前
[go] 适配器模式
开发语言·golang·适配器模式
記億揺晃着的那天6 分钟前
SpringCloud从零开始简单搭建 - JDK17
java·spring boot·后端·spring cloud·nacos
好看资源平台15 分钟前
JavaScript 数据可视化:前端开发的核心工具
开发语言·javascript·信息可视化
EPSDA15 分钟前
Java集合(三)
java·开发语言
DC102018 分钟前
Java 每日一刊(第14期):抽象类和接口
java·开发语言
憨憨憨憨憨到不行的程序员18 分钟前
Spring框架基础知识
java·后端·spring
农大蕉蕉21 分钟前
C++校招面经(二)
java·开发语言·c++
Adolf_199324 分钟前
Flask-SQLAlchemy一对多 一对一 多对多关联
后端·python·flask
编程小白煎堆24 分钟前
C语言:链表
c语言·开发语言·链表
Su4iky29 分钟前
(Python) Structured Streaming读取Kafka源实时处理图像
开发语言·python·kafka