Go协程为并发编程提供了强大的工具,结合轻量级、高效的特点,为开发者带来了独特的编程体验。本文深入探讨了Go协程的基本原理、同步机制、高级用法及其性能与最佳实践,旨在为读者提供全面、深入的理解和应用指导。

关注公众号【TechLeadCloud】,分享互联网架构、云服务技术的全维度知识。作者拥有10+年互联网服务架构、AI产品研发经验、团队管理经验,同济本复旦硕,复旦机器人智能实验室成员,阿里云认证的资深架构师,项目管理专业人士,上亿营收AI产品研发负责人。

1. Go协程简介

Go协程(goroutine)是Go语言中的并发执行单元,它比传统的线程轻量得多,并且是Go语言并发模型中的核心组成部分。在Go中,你可以同时运行成千上万的goroutine,而不用担心常规操作系统线程带来的开销。

什么是Go协程?

Go协程是与其他函数或方法并行运行的函数或方法。你可以认为它类似于轻量级的线程。其主要优势在于它的启动和停止开销非常小,相比于传统的线程来说,可以更有效地实现并发。

package main

import (
"fmt"
"time"
) func sayHello() {
for i := 0; i < 5; i++ {
time.Sleep(100 * time.Millisecond)
fmt.Println("Hello!")
}
} func main() {
go sayHello() // 启动一个Go协程
for i := 0; i < 5; i++ {
time.Sleep(150 * time.Millisecond)
fmt.Println("Hi!")
}
}

输出:

Hi!
Hello!
Hi!
Hello!
Hello!
Hi!
Hello!
Hi!
Hello!

处理过程:

在上面的代码中,我们定义了一个sayHello函数,它在一个循环中打印“Hello!”五次。在main函数中,我们使用go关键字启动了sayHello作为一个goroutine。此后,我们又在main中打印“Hi!”五次。因为sayHello是一个goroutine,所以它会与main中的循环并行执行。因此,输出中“Hello!”和“Hi!”的打印顺序可能会变化。

Go协程与线程的比较

  1. 启动开销:Go协程的启动开销远小于线程。因此,你可以轻松启动成千上万个goroutine。
  2. 内存占用:每个Go协程的堆栈大小开始时很小(通常在几KB),并且可以根据需要增长和缩小,而线程通常需要固定的、较大的堆栈内存(通常为1MB或更多)。
  3. 调度:Go协程是由Go运行时系统而不是操作系统调度的。这意味着Go协程之间的上下文切换开销更小。
  4. 安全性:Go协程为开发者提供了简化的并发模型,配合通道(channels)等同步机制,减少了并发程序中常见的错误。

示例代码:

package main

import (
"fmt"
"time"
) func worker(id int, ch chan int) {
for {
fmt.Printf("Worker %d received data: %d\n", id, <-ch)
}
} func main() {
ch := make(chan int) for i := 0; i < 3; i++ {
go worker(i, ch) // 启动三个Go协程
} for i := 0; i < 10; i++ {
ch <- i
time.Sleep(100 * time.Millisecond)
}
}

输出:

Worker 0 received data: 0
Worker 1 received data: 1
Worker 2 received data: 2
Worker 0 received data: 3
...

处理过程:

在这个示例中,我们启动了三个工作goroutine来从同一个通道接收数据。在main函数中,我们发送数据到通道。每当通道中有数据时,其中一个工作goroutine会接收并处理它。由于goroutines是并发运行的,所以哪个goroutine接收数据是不确定的。

Go协程的核心优势

  1. 轻量级:如前所述,Go协程的启动开销和内存使用都远远小于传统线程。
  2. 灵活的调度:Go协程是协同调度的,允许用户在适当的时机进行任务切换。
  3. 简化的并发模型:Go提供了多种原语(如通道和锁),使并发编程变得更加简单和安全。

总的来说,Go协程为开发者提供了一个高效、灵活且安全的并发模型。与此同时,Go的标准库提供了丰富的工具和包,进一步简化了并发程序的开发过程。


2. Go协程的基本使用

在Go中,协程是构建并发程序的基础。创建协程非常简单,并且使用go关键字就可以启动。让我们探索一些基本用法和与之相关的示例。

创建并启动Go协程

启动一个Go协程只需使用go关键字,后跟一个函数调用。这个函数即可以是匿名的,也可以是预定义的。

示例代码:

package main

import (
"fmt"
"time"
) func printNumbers() {
for i := 1; i <= 5; i++ {
time.Sleep(200 * time.Millisecond)
fmt.Println(i)
}
} func main() {
go printNumbers() // 启动一个Go协程
time.Sleep(1 * time.Second)
fmt.Println("End of main function")
}

输出:

1
2
3
4
5
End of main function

处理过程:

在这个示例中,我们定义了一个printNumbers函数,它会简单地打印数字1到5。在main函数中,我们使用go关键字启动了这个函数作为一个新的Go协程。主函数与Go协程并行执行。为确保主函数等待Go协程执行完成,我们使主函数休眠了1秒钟。

使用匿名函数创建Go协程

除了启动预定义的函数,你还可以使用匿名函数直接启动Go协程。

示例代码:

package main

import (
"fmt"
"time"
) func main() {
go func() {
fmt.Println("This is a goroutine!")
time.Sleep(500 * time.Millisecond)
}()
fmt.Println("This is the main function!")
time.Sleep(1 * time.Second)
}

输出:

This is the main function!
This is a goroutine!

处理过程:

在这个示例中,我们在main函数中直接使用了一个匿名函数来创建Go协程。在匿名函数中,我们简单地打印了一条消息并使其休眠了500毫秒。主函数先打印其消息,然后等待1秒来确保Go协程有足够的时间完成执行。

Go协程与主函数

值得注意的是,如果主函数(main)结束,所有的Go协程都会被立即终止,不论它们的执行状态如何。

示例代码:

package main

import (
"fmt"
"time"
) func main() {
go func() {
time.Sleep(500 * time.Millisecond)
fmt.Println("This will not print!")
}()
}

处理过程:

在上面的代码中,Go协程在打印消息前休眠了500毫秒。但由于主函数在此期间已经结束,所以Go协程也被终止,因此我们不会看到任何输出。

总结,Go协程的基本使用非常简单和直观,但需要注意确保主函数在所有Go协程执行完毕之前不会结束。


3. Go协程的同步机制

在并发编程中,同步是确保多个协程能够有效、安全地共享资源或协同工作的关键。Go提供了几种原语,帮助我们实现这一目标。

1. 通道 (Channels)

通道是Go中用于在协程之间传递数据和同步执行的主要方式。它们提供了一种在一个协程中发送数据,并在另一个协程中接收数据的机制。

示例代码:

package main

import "fmt"

func sendData(ch chan string) {
ch <- "Hello from goroutine!"
} func main() {
messageChannel := make(chan string)
go sendData(messageChannel) // 启动一个Go协程发送数据
message := <-messageChannel
fmt.Println(message)
}

输出:

Hello from goroutine!

处理过程:

我们创建了一个名为messageChannel的通道。然后启动了一个Go协程sendData,将字符串"Hello from goroutine!"发送到这个通道。在主函数中,我们从通道接收这个消息并打印它。

2. sync.WaitGroup

sync.WaitGroup是一个等待一组协程完成的结构。你可以增加一个计数来表示应等待的协程数量,并在每个协程完成时减少计数。

示例代码:

package main

import (
"fmt"
"sync"
"time"
) func worker(id int, wg *sync.WaitGroup) {
defer wg.Done()
fmt.Printf("Worker %d starting\n", id)
time.Sleep(time.Second)
fmt.Printf("Worker %d done\n", id)
} func main() {
var wg sync.WaitGroup for i := 1; i <= 5; i++ {
wg.Add(1)
go worker(i, &wg)
} wg.Wait()
fmt.Println("All workers completed.")
}

输出:

Worker 1 starting
Worker 2 starting
Worker 3 starting
Worker 4 starting
Worker 5 starting
Worker 1 done
Worker 2 done
Worker 3 done
Worker 4 done
Worker 5 done
All workers completed.

处理过程:

我们定义了一个名为worker的函数,它模拟一个需要一秒钟才能完成的工作任务。在这个函数中,我们使用defer wg.Done()来确保在函数退出时减少WaitGroup的计数。在main函数中,我们启动了5个这样的工作协程,每启动一个,我们就使用wg.Add(1)来增加计数。wg.Wait()则会阻塞,直到所有工作协程都通知WaitGroup它们已完成。

3. 互斥锁 (sync.Mutex)

当多个协程需要访问共享资源时(例如,更新一个共享变量),使用互斥锁可以确保同时只有一个协程能访问资源,防止数据竞态。

示例代码:

package main

import (
"fmt"
"sync"
) var counter int
var lock sync.Mutex func increment() {
lock.Lock()
counter++
lock.Unlock()
} func main() {
var wg sync.WaitGroup for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
increment()
}()
} wg.Wait()
fmt.Println("Final Counter:", counter)
}

输出:

Final Counter: 1000

处理过程:

我们有一个全局变量counter,我们希望在多个Go协程中并发地增加它。为了确保每次只有一个Go协程能够更新counter,我们使用了互斥锁lock来同步访问。

这些是Go协程同步机制的一些基本方法。正确地使用它们可以帮助你编写更安全、更高效的并发程序。


4. Go协程的高级用法

Go协程的高级用法涉及更复杂的并发模式、错误处理和协程控制。我们将探索一些常见的高级用法和它们的具体应用示例。

1. 选择器 (select)

select语句是Go中处理多个通道的方法。它允许你等待多个通道操作,执行其中一个可以进行的操作。

示例代码:

package main

import (
"fmt"
"time"
) func main() {
ch1 := make(chan string)
ch2 := make(chan string) go func() {
time.Sleep(1 * time.Second)
ch1 <- "Data from channel 1"
}() go func() {
time.Sleep(2 * time.Second)
ch2 <- "Data from channel 2"
}() for i := 0; i < 2; i++ {
select {
case msg1 := <-ch1:
fmt.Println(msg1)
case msg2 := <-ch2:
fmt.Println(msg2)
}
}
}

输出:

Data from channel 1
Data from channel 2

处理过程:

我们创建了两个通道ch1ch2。两个Go协程分别向这两个通道发送数据,但它们的休眠时间不同。在select语句中,我们等待两个通道中的任何一个准备好数据,然后进行处理。由于ch1的数据先到达,因此它的消息首先被打印。

2. 超时处理

使用select,我们可以轻松实现对通道操作的超时处理。

示例代码:

package main

import (
"fmt"
"time"
) func main() {
ch := make(chan string) go func() {
time.Sleep(3 * time.Second)
ch <- "Data from goroutine"
}() select {
case data := <-ch:
fmt.Println(data)
case <-time.After(2 * time.Second):
fmt.Println("Timeout after 2 seconds")
}
}

输出:

Timeout after 2 seconds

处理过程:

Go协程会休眠3秒钟后再向ch发送数据。在select语句中,我们等待这个通道的数据或2秒的超时。由于Go协程在超时之前没有发送数据,因此超时的消息被打印。

3. 使用context进行协程控制

context包允许我们共享跨多个协程的取消信号、超时和其他设置。

示例代码:

package main

import (
"context"
"fmt"
"time"
) func work(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("Received cancel signal, stopping the work")
return
default:
fmt.Println("Still working...")
time.Sleep(1 * time.Second)
}
}
} func main() {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel() go work(ctx) time.Sleep(5 * time.Second)
}

输出:

Still working...
Still working...
Still working...
Received cancel signal, stopping the work

处理过程:

在这个示例中,我们创建了一个带有3秒超时的context。Go协程work会持续工作,直到接收到取消信号或超时。经过3秒后,context的超时被触发,Go协程接收到了取消信号并停止工作。

这些高级用法为Go协程提供了强大的功能,使得复杂的并发模式和控制成为可能。掌握这些高级技巧可以帮助你编写更健壮、更高效的Go并发程序。


5. Go协程的性能与最佳实践

Go协程为并发编程提供了轻量级的解决方案。但为了充分利用其性能优势并避免常见的陷阱,了解一些最佳实践和性能考虑因素是很有必要的。

1. 限制并发数

虽然Go协程是轻量级的,但无节制地创建大量的Go协程可能会导致内存耗尽或调度开销增大。

示例代码:

package main

import (
"fmt"
"sync"
) func worker(id int, wg *sync.WaitGroup) {
defer wg.Done()
fmt.Printf("Worker %d started\n", id)
} func main() {
var wg sync.WaitGroup
numWorkers := 1000 for i := 1; i <= numWorkers; i++ {
wg.Add(1)
go worker(i, &wg)
} wg.Wait()
fmt.Println("All workers done")
}

输出:

Worker 1 started
Worker 2 started
...
Worker 1000 started
All workers done

处理过程:

这个示例创建了1000个工作Go协程。尽管这个数字可能不会导致问题,但如果不加限制地创建更多的Go协程,可能会导致问题。

2. 避免竞态条件

多个Go协程可能会同时访问共享资源,导致不确定的结果。使用互斥锁(Mutex)或其他同步机制来确保数据的一致性。

示例代码:

package main

import (
"fmt"
"sync"
) var (
counter int
mu sync.Mutex
) func increment(wg *sync.WaitGroup) {
defer wg.Done()
mu.Lock()
counter++
mu.Unlock()
} func main() {
var wg sync.WaitGroup for i := 0; i < 1000; i++ {
wg.Add(1)
go increment(&wg)
} wg.Wait()
fmt.Println("Final counter value:", counter)
}

输出:

Final counter value: 1000

处理过程:

我们使用sync.Mutex确保在增加计数器时的互斥访问。这确保了并发访问时的数据一致性。

3. 使用工作池模式

工作池模式是创建固定数量的Go协程来执行任务的方法,避免过度创建Go协程。任务通过通道发送。

示例代码:

package main

import (
"fmt"
"sync"
) func worker(tasks <-chan int, wg *sync.WaitGroup) {
defer wg.Done()
for task := range tasks {
fmt.Printf("Worker processed task %d\n", task)
}
} func main() {
var wg sync.WaitGroup
tasks := make(chan int, 100) // Start 5 workers.
for i := 0; i < 5; i++ {
wg.Add(1)
go worker(tasks, &wg)
} // Send 100 tasks.
for i := 1; i <= 100; i++ {
tasks <- i
} close(tasks)
wg.Wait()
}

输出:

Worker processed task 1
Worker processed task 2
...
Worker processed task 100

处理过程:

我们创建了5个工作Go协程,它们从tasks通道中接收任务。这种模式可以控制并发数并重复使用Go协程。

遵循这些最佳实践不仅可以使你的Go协程代码更加健壮,而且还可以更有效地利用系统资源,提高程序的整体性能。


6.总结

随着计算技术的进步,并发和并行成为了现代软件开发中的关键元素。Go语言作为一个现代编程语言,通过其内置的goroutine为开发者提供了一种简洁而强大的并发编程模式。但正如我们在前面的章节中所看到的,理解其工作原理、同步机制、高级用法及性能与最佳实践是至关重要的。

从本文中,我们不仅了解了Go协程的基础知识和工作原理,还探讨了一些关于如何最大限度地发挥其性能的高级主题。关键的洞察包括:

  1. 轻量与高效:Go协程是轻量级的线程,但它们在实现上的特点使其在大量并发场景下更为高效。
  2. 同步与通信:Go的哲学是“不通过共享内存来通信,而是通过通信来共享内存”。这反映在其强大的channel机制中,这也是避免许多并发问题的关键。
  3. 性能与最佳实践:理解并遵循最佳实践不仅可以确保代码的健壮性,而且还可以显著提高性能。

最后,虽然Go提供了强大的工具和机制来处理并发,但真正的艺术在于如何正确地使用它们。正如我们在软件工程中经常看到的那样,工具只是手段,真正的力量在于了解它们的工作原理并正确地应用它们。

希望本文为您提供了关于Go协程的深入、全面的认识,并为您的并发编程之旅提供了有价值的洞见和指导。正如在云服务、互联网服务架构和其他复杂的系统中经常可以看到的那样,真正掌握并发是提高性能、扩展性和响应速度的关键。

关注公众号【TechLeadCloud】,分享互联网架构、云服务技术的全维度知识。作者拥有10+年互联网服务架构、AI产品研发经验、团队管理经验,同济本复旦硕,复旦机器人智能实验室成员,阿里云认证的资深架构师,项目管理专业人士,上亿营收AI产品研发负责人。

如有帮助,请多关注

个人微信公众号:【TechLeadCloud】分享AI与云服务研发的全维度知识,谈谈我作为TechLead对技术的独特洞察。

TeahLead KrisChang,10+年的互联网和人工智能从业经验,10年+技术和业务团队管理经验,同济软件工程本科,复旦工程管理硕士,阿里云认证云服务资深架构师,上亿营收AI产品业务负责人。

Go协程揭秘:轻量、并发与性能的完美结合的更多相关文章

  1. 6)协程三( asyncio处理并发)

    一:使用 asyncio处理并发 介绍 asyncio 包,这个包使用事件循环驱动的协程实现并发.这是 Python 中最大也是最具雄心壮志的库之一. 二:示例 1)单任务协程处理和普通任务比较 #普 ...

  2. 【python】-- 协程介绍及基本示例、协程遇到IO操作自动切换、协程(gevent)并发爬网页

    协程介绍及基本示例 协程,又称微线程,纤程.英文名Coroutine.一句话说明什么是协程:协程是一种用户态的轻量级线程. 协程拥有自己的寄存器上下文和栈.协程调度切换时,将寄存器上下文和栈保存到其他 ...

  3. Python进阶----异步同步,阻塞非阻塞,线程池(进程池)的异步+回调机制实行并发, 线程队列(Queue, LifoQueue,PriorityQueue), 事件Event,线程的三个状态(就绪,挂起,运行) ,***协程概念,yield模拟并发(有缺陷),Greenlet模块(手动切换),Gevent(协程并发)

    Python进阶----异步同步,阻塞非阻塞,线程池(进程池)的异步+回调机制实行并发, 线程队列(Queue, LifoQueue,PriorityQueue), 事件Event,线程的三个状态(就 ...

  4. 协程+IO切换实现并发

    from gevent import monkey # 以后代码中遇到IO都会自动执行greenlet的switch进行切换 monkey.patch_all() import requests im ...

  5. Generator(生成器),入门初基,Coroutine(原生协程),登峰造极,Python3.10并发异步编程async底层实现

    普遍意义上讲,生成器是一种特殊的迭代器,它可以在执行过程中暂停并在恢复执行时保留它的状态.而协程,则可以让一个函数在执行过程中暂停并在恢复执行时保留它的状态,在Python3.10中,原生协程的实现手 ...

  6. 利用协程和socket实现并发

    服务端代码 from gevent import monkey monkey.patch_all() from gevent import spawn import socket def commun ...

  7. Goroutine(协程)为何能处理大并发?

    简单来说:协程十分轻量,可以在一个进程中执行有数以十万计的协程,依旧保持高性能. 进程.线程.协程的关系和区别: 进程拥有自己独立的堆和栈,既不共享堆,亦不共享栈,进程由操作系统调度. 线程拥有自己独 ...

  8. PHP回顾之协程

    转载请注明文章出处: https://tlanyan.me/php-review... PHP回顾系列目录 PHP基础 web请求 cookie web响应 session 数据库操作 加解密 Com ...

  9. 深入分析 Java、Kotlin、Go 的线程和协程

    前言 协程是什么 协程的好处 进程 进程是什么 进程组成 进程特征 线程 线程是什么 线程组成 任务调度 进程与线程的区别 线程的实现模型 一对一模型 多对一模型 多对多模型 线程的"并发& ...

  10. Kotlin协程基础

    开发环境 IntelliJ IDEA 2021.2.2 (Community Edition) Kotlin: 212-1.5.10-release-IJ5284.40 我们已经通过第一个例子学会了启 ...

随机推荐

  1. 基于 python3+nginx 的 Jupyter Notebook 服务端 ssl 访问

    引言 Jupyter Notebook(原名 ipython)可是科学计算界的必备工具,友好的界面,方便的交互,支持 Markdown,集中的极客们想要的一切特点,同时又制作的如此优雅和精美,真是难能 ...

  2. 用python SMTP发送简单邮件

    python SMTP发送邮件 SMTP(Simple Mail Transfer Protocol)即简单邮件传输协议 它是一组由源地址到目的地址传送邮件得规则,由它来控制信件的中转方式. Pyth ...

  3. Spring事件监听机制使用和原理解析

    你好,我是刘牌! 前言 好久没有更新Spring了,今天来分享一下Spring的事件监听机制,之前分享过一篇Spring监听机制的使用,今天从原理上进行解析,Spring的监听机制基于观察者模式,就是 ...

  4. 混沌演练状态下,如何降低应用的 MTTR(平均恢复时间)

    在企业业务领域,锦礼是针对福利.营销.激励等员工采购场景的一站式解决方案,包含面向员工.会员等弹性激励SAAS平台.由于其直接面向公司全体员工,其服务的高可用尤其重要,本文将介绍锦礼商城大促前夕,通过 ...

  5. 基于VAE的风险分析:基于历史数据的风险分析、基于实时数据的风险分析

    目录 引言 随着人工智能和机器学习的发展,风险分析已经成为许多行业和组织中不可或缺的一部分.传统的基于经验和规则的风险分析方法已经难以满足现代风险分析的需求,因此基于VAE的风险分析方法逐渐成为了主流 ...

  6. stream流根据集合中的元素的属性进行去重的方法

    public class StreamListTest { public static void main(String[] args) { List<Student> studentLi ...

  7. Mybatis-plus自定义Sql注入器

    最近在学习mybatis-plus,知道了在mp中通过AbstractSqlInjector将BaseMapper中的方法注入到了Mybatis容器,这样这些方法才可以正常执行. 下面是一个关系图 那 ...

  8. pta第三阶段题目集

    (1)前言 pta第三阶段作业中,主要包含了如下的主要内容: 1.全程贯穿了课程设计的程序,每一次都是上一次的迭代和修改,难度较大,中间涉及到先是类与类之间的多态和继承关系,后面的修改中,转变为了组合 ...

  9. 详解nvim内建LSP体系与基于nvim-cmp的代码补全体系

    2023年,nvim以及其生态已经发展的愈来愈完善了.nvim内置的LSP(以及具体的语言服务)加上众多插件,可以搭建出支持各种类型语法检查.代码补全.代码格式化等功能的IDE.网络上关于如何配置的文 ...

  10. .net 温故知新【12】:Asp.Net Core WebAPI 中的Rest风格

    RPC RPC(Remote Procedure Call),远程过程调用),这种RPC形式的API组织形态是类和方法的形式.所以API的请求往往是一个动词用来标识接口的意思,比如 https://x ...