当前位置: 首页 > article >正文

【魅力golang】之-玩转协程

1背景知识

  • 程序是存储在磁盘上的静态指令集,只有加载到内存并由 CPU 执行时,才能产生动态行为。
  • 程序运行需要有内存分配、代码执行、I/O 交互等多个资源的协调。
  • 进程是程序在操作系统中运行的实例,具有独立的内存空间。
  • 每个进程包含一个主线程,可以通过进程间通信(IPC)与其他进程交互。
  • 特点
    1. 独立性强,不同进程互不干扰。
    2. 创建与切换成本高(需要操作系统分配资源)。

1.1 并行与并发

  • 并发是指多个任务在同一时间段内交替执行。任务并不一定同时运行,而是通过合理调度,让每个任务都能在某段时间运行。并发的本质是逻辑上的同时性
  • 并行是指同时执行多个任务。多个任务在同一时间点上运行,通常需要多核或多处理器支持。并行的本质是物理上的同时性

下图对比并发与并行的主要区别与特征

特性

并行

并发

核心定义

多任务同时运行

多任务交替运行

硬件依赖

需要多核处理器支持

单核环境也可实现

实现方式

任务独立并同时执行

任务交替共享时间片

适用场景

计算密集型任务

I/O 密集型任务

开发难度

高(任务分解与同步复杂)

较低(调度器负责切换)

1.2 线程的概念

  • 线程是进程中的一个独立执行单元,多个线程共享同一进程的内存和资源。
  • 一个进程可以包含多个线程(称为多线程),各线程并行或并发执行。
  • 特点
    1. 线程间通信更快捷,资源共享方便。
    2. 线程切换比进程轻量,但仍有开销(如保存上下文)。

1.3 协程的概念

  • 协程(Coroutine)又称为微线程,是一种比线程更轻量的执行单元。可以直接与操作系统的线程对应,但是创建和调度goroutine的代价远远低于操作系统线程。
  • 区别于线程:协程由应用程序级别调度,而不是由操作系统管理。
  • 协程切换:只涉及少量的栈操作,没有上下文切换的开销。

1.4 协程对比线程的优点

  1. 轻量性:一个线程通常需要 1 MB 的栈,而协程只需要几 KB 的栈,支持大规模并发。
  2. 低开销:协程切换在用户态完成,无需系统调用,开销小。
  3. 更强的控制力:开发者可以通过代码控制协程的启动、暂停和恢复。
  4. 天然的非阻塞:Go 语言的协程和 runtime 内置的调度器让阻塞操作(如 I/O)不会阻塞其他协程。

2Golang 中的协程

2.1 协程的定义

在 Golang 中,协程被称为 Goroutine,是由 Go runtime 管理的并发任务。通过 go 关键字创建协程。Golang天生支持并发编程,通过goroutine,可以轻松创建并发程序。Goroutine的创建和销毁成本非常低,通常只占用几KB的内存,这使得并发编程更加安全和高效‌。

示例:创建协程

package main

import (
	"fmt"
	"time"
)

func printMessage(message string) {
	for i := 0; i < 5; i++ {
		fmt.Println(message, i)

		time.Sleep(100 * time.Millisecond)
	}
}

func main() {
	go printMessage("Hello from Goroutine")

	printMessage("Hello from Main")
}
  • go printMessage("Hello from Goroutine") 启动一个协程。
  • 主线程和协程并发执行。

输出:

Hello from Main 0
Hello from Goroutine 0
Hello from Goroutine 1
Hello from Main 1
Hello from Main 2
Hello from Goroutine 2
Hello from Goroutine 3
Hello from Main 3
Hello from Main 4
Hello from Goroutine 4

2.2 Golang 协程的原理与设计思路

协程模型:Go 的协程基于 M:N 模型,一个操作系统线程可以管理多个协程。Go runtime 中包含调度器,负责将协程分配到多个线程上运行。

调度器:调度器分为三个部分:M(操作系统线程)、P(处理器资源)和 G(Goroutine)。调度器通过 Work Stealing 等算法高效调度 Goroutine。

非阻塞运行:I/O 操作、网络请求等会触发协程的让步,避免阻塞其他任务。

2.3 协程与主线程的关系

  • 主线程是程序启动时的默认线程。
  • 如果主线程退出,所有协程也会终止。

示例:主线程等待协程完成

package main

import (
	"fmt"
	"time"
)

func printMessage(message string) {
	for i := 0; i < 5; i++ {
		fmt.Println(message, i)

		time.Sleep(100 * time.Millisecond)
	}
}

func main() {
	go printMessage("Goroutine")

	time.Sleep(1 * time.Second) // 主线程等待 1 秒

	fmt.Println("Main thread finished")
}

与2.1中的例子不同的是,这里主线程 sleep了1秒,等待协程,协程每次循环只sleep了100个毫秒,即0.1秒,于是,输出变成了:

Goroutine 0
Goroutine 1
Goroutine 2
Goroutine 3
Goroutine 4
Main thread finished

2.4 多协程与同步

WaitGroup

sync.WaitGroup 是 Go 中常用的同步原语,用于等待一组协程完成。

示例:使用 WaitGroup 同步协程

package main

import (
	"fmt"
	"sync"
)

func worker(id int, wg *sync.WaitGroup) {
	defer wg.Done() // 协程完成时通知 WaitGroup

	fmt.Printf("Worker %d starting\n", id)

	// 模拟任务
	for i := 0; i < 3; i++ {
		fmt.Printf("Worker %d doing task %d\n", id, i)
	}

	fmt.Printf("Worker %d done\n", id)
}

func main() {
	var wg sync.WaitGroup

	for i := 1; i <= 3; i++ {
		wg.Add(1) // 增加计数

		go worker(i, &wg)
	}

	wg.Wait() // 阻塞,直到计数归零

	fmt.Println("All workers finished")
}

输出:

Worker 3 starting
Worker 3 doing task 0
Worker 3 doing task 1
Worker 3 doing task 2
Worker 3 done
Worker 1 starting
Worker 1 doing task 0
Worker 1 doing task 1
Worker 1 doing task 2
Worker 1 done
Worker 2 starting
Worker 2 doing task 0
Worker 2 doing task 1
Worker 2 doing task 2
Worker 2 done
All workers finished

互斥锁

sync.Mutex 用于保护共享资源的并发访问。

示例:使用互斥锁

package main

import (
	"fmt"

	"sync"
)

var (
	counter int // 共享资源 计数器

	mutex sync.Mutex // 定义互斥锁
)

func increment(wg *sync.WaitGroup) {
	defer wg.Done() // 减少计数器

	mutex.Lock() // 加锁

	counter++ // 修改共享资源

	mutex.Unlock() // 解锁
}

func main() {
	var wg sync.WaitGroup // 创建一个WaitGroup

	for i := 0; i < 10; i++ {
		wg.Add(1)  // 添加10个任务

		go increment(&wg)// 开启10个goroutine
	}

	wg.Wait() // 等待所有任务完成

	fmt.Println("Final Counter:", counter)
}

输出:Final Counter: 10

读写锁

sync.RWMutex 提供读写分离的锁机制。

示例:使用读写锁

package main

import (
	"fmt"
	"sync"
	"time"
)

var (
	data int // 定义共享数据

	rwMux sync.RWMutex // 定义读写锁
)

func read(wg *sync.WaitGroup) {
	defer wg.Done() // 减少计数器

	rwMux.RLock() // 读锁

	fmt.Println("Reading data:", data) // 打印数据

	time.Sleep(100 * time.Millisecond) // 模拟读操作

	rwMux.RUnlock() // 释放读锁
}

func write(wg *sync.WaitGroup) {
	defer wg.Done() // 减少计数器

	rwMux.Lock() // 写锁

	data++ // 更新数据

	fmt.Println("Writing data:", data) // 打印数据

	time.Sleep(100 * time.Millisecond) // 模拟写操作

	rwMux.Unlock() // 释放写锁
}

func main() {
	var wg sync.WaitGroup // 定义等待组

	for i := 0; i < 3; i++ {
		wg.Add(1) // 增加计数器

		go read(&wg) // 启动一个goroutine执行读取操作

		wg.Add(1) // 增加计数器

		go write(&wg) // 启动一个goroutine执行写入操作
	}

	wg.Wait() // 等待所有goroutine完成
}

输出:

Reading data: 0
Writing data: 1
Reading data: 1
Reading data: 1
Writing data: 2
Writing data: 3

3协程的应用场景

高并发任务:如 Web 服务器处理多个请求,使用 Goroutine 处理 HTTP 请求。

并行计算:分解任务到多个协程并行处理,提高计算效率。

I/O 密集型任务:如文件操作、数据库查询,协程让 I/O 操作非阻塞。

定时任务:使用协程运行周期性任务。

4注意事项

资源泄漏

  • 确保协程退出,避免资源耗尽。
  • 使用 context 提供超时或取消机制。

共享资源

  • 对共享资源使用互斥锁或其他同步原语保护。

死锁风险

  • 小心锁的使用顺序,避免多个协程互相等待。

协程数量控制

  • 避免大量协程导致内存耗尽。

Golang 的协程通过轻量、高效的实现,为开发者提供了强大的并发处理能力。结合同步原语(如 WaitGroup 和 Mutex)和 Go runtime 的调度能力,协程在处理高并发和复杂任务时表现出色。合理使用协程是 Go 并发编程的核心技能。下期风云再给大家详细介绍golang中的通道channel,解决协程之间的通讯,敬请期待。


http://www.kler.cn/a/452819.html

相关文章:

  • sentinel限流+其他
  • 【LuaFramework】服务器模块相关知识
  • Python Polars快速入门指南:LazyFrames
  • windows C++ TCP客户端
  • 【23种设计模式·全精解析 | 行为型模式篇】11种行为型模式的结构概述、案例实现、优缺点、扩展对比、使用场景、源码解析
  • Idea导入Springboot项目,无法正确加载yml文件,且不为绿色图标的解决办法
  • Qt之QML应用程序开发:给应用程序添加图标文件
  • 【FastAPI】日志
  • element ui--下拉根据拼音首字母过滤
  • 纯真社区版IP库CZDB数据格式使用教程
  • 05.HTTPS的实现原理-HTTPS的握手流程(TLS1.2)
  • 【大语言模型】ACL2024论文-34 你的模型能区分否定和隐含意义吗?通过意图编码器揭开挑战
  • 美食推荐系统|Java|SSM|JSP|
  • w~视觉~3D~合集5
  • 如何编写 Prompt
  • 笔记工具--MD-Markdown的语法技巧
  • OSI 网络 7 层模型
  • Let‘s encrypt 免费 SSL 证书安装
  • [Unity Shader][图形渲染]【游戏开发】 Shader数学基础8 - 齐次坐标
  • Docker部署Sentinel
  • vue 基础学习
  • 赛博错题本
  • android 登录界面编写
  • 在UE5中调用ImGui图形界面库
  • Mysql的MHA高可用及故障切换
  • 3.银河麒麟V10 离线安装Nginx