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

golang 14并发编程

Go 并发编程基于 1. 核心概念

  • 并发:同时处理多个任务,逻辑上的并行

  • Go 并发模型:基于 Goroutine 和 Channel 的 CSP(通信顺序进程)模型

  • 优势:轻量级、低开销、简化并发编程复杂度

  1. Goroutine 基础

  • 定义:Go 语言特有的轻量级线程,由 Go 运行时管理

  • 创建:使用 go 关键字启动 go function()

  • 特点:初始栈大小小(2KB),可动态伸缩,开销远小于 OS 线程

  • 调度:由 Go 调度器(GPM 模型)管理,M:N 映射到系统线程

  1. 同步机制

  • Channel:

    • 类型:无缓冲(同步)、有缓冲(异步)

    • 操作:发送ch <- val、接收val <- ch、关闭close(ch)

    • 用途:Goroutine 间通信和同步

    • 遍历:for val := range ch自动检测关闭

  • 共享内存:

    • Mutex:互斥锁,sync.Mutex提供Lock()Unlock()

    • RWMutex:读写锁,允许多读单写

    • WaitGroup:等待一组 Goroutine 完成,Add()Done()Wait()

    • Once:确保代码只执行一次

    • Cond:条件变量,用于等待特定条件

  1. 并发控制

  • 超时控制:time.After()结合 select

  • 取消操作:context.Context实现多 Goroutine 协作取消

  • 限制并发数:带缓冲 channel 实现信号量模式

  • 错误处理:通过 channel 收集错误或使用errgroup

  1. 调度原理(GPM 模型)

  • G(Goroutine):用户态线程,包含栈、程序计数器等

  • P(Processor):逻辑处理器,维护可运行 G 队列

  • M(Machine):绑定 OS 线程,执行 G

  • 工作窃取:P 的 G 队列空时,从其他 P 偷取 G 执行

  1. 常见模式

  • 生产者 - 消费者:通过 channel 传递数据

  • 扇出 - 扇入:多个生产者,一个消费者汇总结果

  • 管道模式:多个阶段串联处理数据

  • Worker Pool:固定数量的工作 Goroutine 处理任务

  1. 注意事项

  • 避免竞态条件:使用 channel 或锁保护共享资源

  • 防止内存泄漏:确保所有 Goroutine 能正常退出

  • 避免死锁:注意锁的获取顺序,channel 操作配对

  • 合理设置 Goroutine 数量:并非越多越好

  • 使用go test -race检测竞态条件

  1. 高级特性

  • 定时器:time.Tickertime.After

  • 原子操作:sync/atomic包提供的原子函数

  • 上下文:context包管理请求生命周期

  • 并行计算:利用runtime.NumCPU()设置并行度

Go 并发编程的核心思想是 "不要通过共享内存来通信,而要通过通信来共享内存",这一理念使 Go 在处理并发问题时更加简洁和安全。

  • 摘要

    该视频主要讲述了Go语言在并发编程方面的优势及其背后的原因。首先,指出了Go语言因并发编程简单且高效而受到青睐。随后,对比了多线程和多进程编程的缺点,如内存占用大和线程切换成本高。接着,介绍了用户级线程(如协程)的兴起,并指出Go语言虽未直接提供协程,但其goroutine机制实现了类似效果,具有内存占用小和切换快的特点,适合高并发场景。

  • 分段总结

    折叠

    00:01go语言并发编程概述

    1.go语言并发编程简单,容易写出高并发程序。 2.并发编程在其他语言中主要通过多线程实现,存在内存占用大和线程切换成本高的问题。 3.用户级线程(协程)内存占用小,切换快,不是由操作系统调度。 4.go语言没有设计线程,所有库和第三方库都支持go语言的协程。

    07:37go协程编程体验

    1.在go语言的main函数中启动一个协程,只需在函数前加关键字go。 2.主协程一旦挂掉,所有子协程也会挂掉,因为程序退出时协程也随之退出。 3.使用sleep函数防止主协程退出,确保子协程有时间运行。 4.在主协程中打印信息,观察协程的异步执行情况。

    12:29匿名函数在协程中的应用

    1.匿名函数适用于临时运行的函数,定义在main函数内部。 2.使用匿名函数简化协程的定义和调用。

    15:19启动多个协程

    1.在for循环中启动多个协程,注意闭包和for循环变量的问题。 2.使用闭包时,确保变量不会被意外修改或重用。 3.启动大量协程时,注意内存占用和资源管理。

    16:48闭包和for循环问题

    1.闭包可以引用外部变量的值,但在for循环中需要注意变量重用的问题。 2.for循环中的变量在协程执行时可能已被修改,导致结果不可预测。 3.解决办法包括复制变量值或使用直传递方式传递变量。

  • 重点

    本视频暂不支持提取重点

一、并发编程 00:00
1. 课前说明 00:18
1)选择go语言的原因 00:25
  • 核心优势: Go语言因其并发编程简单且易于实现高并发程序而广受欢迎,这是许多开发者选择Go的主要原因。

  • 生态特点: Go语言从设计之初就专注于协程(goroutine),使得其并发编程生态统一且完善。

2)多线程编程及其存在的问题 00:49
  • 传统实现: 其他语言(如Python、Java、PHP)主要通过多线程实现并发编程。

  • 主要问题

    :

    • 内存消耗: 每个线程占用约2MB内存

    • 调度成本: 线程切换由操作系统完成,开销较大

3)多线程的内存占用和线程调度问题 02:00
  • 内存占用: Java等语言的线程内存占用是Go协程的1000倍(2MB vs 2KB)

  • 调度机制

    :

    • PHP等语言依赖操作系统调度

    • Java通过JVM间接调用操作系统调度

4)多线程的发展和用户级线程的出现 03:30
  • 发展背景: Web2.0时代对并发要求提高,传统多线程力不从心

  • 解决方案

    : 出现用户级线程(协程),具有多种叫法:

    • 绿程

    • 轻量级线程

    • 协程(最通用)

5)协程的叫法、特点及发展 03:51
  • 语言实现

    :

    • Python: asyncio

    • PHP: Swoole

    • Java: Netty

  • 核心优势

    :

    • 内存占用小: 仅2KB

    • 切换速度快: 函数级切换而非系统级

6)Go语言协程的特点和优势 05:46
  • 设计理念: Go作为后发语言,直接采用协程(goroutine)作为唯一并发模型

  • 生态统一: 所有第三方库都原生支持goroutine

  • 对比劣势: 其他语言因兼容多模型导致协程生态发展受限

7)Go语言协程的便利性 06:59
  • 语法简洁: 只需在函数调用前加go关键字

  • 无限制: 任何函数都可转为协程执行,不像Python需要特殊定义

2. 体验协程编程 07:34
1)定义函数并启动协程 08:03
  • 基本语法:

go funcName()
  • 匿名函数写法:

go func() {// 代码逻辑
}()
2)运行程序并观察结果 09:31
  • 常见问题: 主协程退出导致子协程未执行

  • 原因分析: Go采用"主死随从"机制,主协程结束会终止所有子协程

3)使用time的Sleep方法 10:52
  • 解决方案: 通过time.Sleep保持主协程存活

time.Sleep(2 * time.Second)
4)证明异步打印过程 11:22
  • 验证方法: 在主协程先打印内容,协程延迟打印

  • 执行顺序: 主协程代码立即执行,协程异步执行

5)协程在for循环中的运用 12:19
  • 典型场景: 在循环中启动多个协程

for i := 0; i < 100; i++ {go func() {// 协程逻辑}()
}
6)匿名函数写法 13:06
  • 优化建议: 对于只使用一次的协程逻辑,推荐匿名函数写法

  • 内存效率: 避免定义全局函数,减少命名空间污染

7)运行程序并观察结果 14:32
  • 执行特点: 协程执行顺序不确定,由调度器决定

  • 资源消耗: Go可轻松创建上百万协程,远胜传统线程

8)启动多个协程 15:21
  • 常见错误: 直接使用循环变量导致值异常

// 错误写法
go func() {fmt.Println(i) // i可能已被修改
}()
9)for循环的问题 16:37
  • 根本原因: 循环变量重用和闭包特性

  • 现象分析: 协程执行时可能获取到已被修改的循环变量值

10)值复制 20:44
  • 解决方案1: 临时变量拷贝

tmp := i
go func() {fmt.Println(tmp)
}()
  • 解决方案2: 参数传递(推荐)

go func(i int) {fmt.Println(i)
}(i)
  • 核心原理: 通过值传递确保每个协程获得独立的变量副本

二、知识小结
知识点核心内容考试重点/易混淆点难度系数
Go语言并发编程优势Go语言通过协程(goroutine)实现高并发,内存占用小(仅2KB),切换快(用户级调度)。对比传统多线程(如Java/Python)存在内存占用大(约2MB)、线程切换成本高(依赖操作系统调度)的缺点。协程与线程的区别:协程是用户级轻量级线程,切换由程序控制;线程依赖OS调度,成本高。⭐⭐
goroutine使用通过go关键字启动协程,支持任意函数异步执行(如go funcName())。匿名函数写法:go func() { ... }()。主协程退出导致子协程终止:需通过time.Sleep或同步机制(如WaitGroup)避免主协程提前退出。⭐⭐
并发编程的常见问题闭包变量捕获:for循环中直接使用迭代变量会导致协程共享同一变量(如打印重复值)。解决方案:1. 局部变量复制(temp := i);2. 函数参数值传递(func(i int) { ... }(i))。变量作用域与数据竞争:协程共享变量需注意同步(如mutex或channel)。⭐⭐⭐
协程生态优势Go语言原生支持协程,所有库均适配goroutine;其他语言(如Python/Java)需依赖第三方库(如asyncio/Netty),生态碎片化。生态唯一性:Go开发者无需选择线程/协程,避免混合编程的复杂性。
性能对比Go协程启动成本极低,可轻松创建百万级协程;传统线程(如Java)通常限制在数百个。调度效率:Go的GMP调度模型 vs 其他语言的OS线程调度。⭐⭐
  • 摘要

    该视频主要讲述了Go语言中的goroutine(协程)与传统多线程编程的区别,以及goroutine调度的简单原理。视频通过对比Python、Java等语言的线程调度模型,指出Go语言通过GMP机制实现轻量级线程管理,减少线程切换的代价和内存占用。Go在操作系统上申请少量线程,通过goroutine实现大量并发任务,避免了为每个任务实例化操作系统线程,从而提高了调度效率和性能。

  • 分段总结

    折叠

    00:01多线程编程与协程调度

    1.多线程编程的不同语言调度模型:如Python和Java的区别,Java依赖虚拟机调度,Python直接与操作系统交互。 2.操作系统线程调度:线程必须在操作系统上运行,涉及系统级的工作如寄存器准备和临时信息存储。 3.线程切换的代价:包括系统级工作的耗时和线程结构体的大小。

    04:18Go语言的GMP调度模型

    1.GMP调度模型:G代表goroutine,M代表操作系统线程,P代表处理器。 2.goroutine的数量:可以非常多,映射到线程池上的线程上。 3.调度器的作用:管理goroutine到线程的映射,实现公平调度。

    11:00调度器的优化

    1.多调度器:通过多个调度器实现并行调度,避免锁竞争。 2.处理器与线程的绑定:处理器绑定线程,实现高效的goroutine调度。 3.goroutine的挂起:当goroutine进行sleep操作或网络请求时,会被挂起,释放资源给其他goroutine。

  • 重点

    本视频暂不支持提取重点

一、go语言 00:02
1. 用户级线程与多线程编程的区别 00:26
  • 调度层级

    :

    • 传统线程:直接由操作系统内核调度,涉及系统调用和上下文切换

    • Go协程:用户空间调度,通过GMP模型实现轻量级调度

  • 资源消耗

    :

    • 传统线程:每个线程需要独立的栈空间(通常MB级)和内核资源

    • Go协程:栈空间初始仅2KB,且由Go运行时管理

  • 切换代价

    :

    • 传统线程:需要保存/恢复寄存器状态、内存映射等,耗时约1-2微秒

    • Go协程:仅需保存少量寄存器,切换时间约200纳秒

2. go语言的调度机制 04:17
1)GMP模型基础
  • 核心组件

    :

    • G(Goroutine):轻量级线程,包含栈、指令指针等信息

    • M(Machine):操作系统线程,实际执行单元

    • P(Processor):逻辑处理器,管理本地运行队列

  • 调度原理

    :

    • 通过P将大量G映射到少量M上执行

    • 默认P数量等于CPU核心数,可通过GOMAXPROCS调整

    • 每个P维护本地运行队列,减少全局锁竞争

2)工作窃取机制
  • 负载均衡

    :

    • 当P的本地队列为空时,会从全局队列或其他P的队列"窃取"G

    • 避免某些P过载而其他P空闲的情况

  • 调度触发点

    :

    • 系统调用阻塞时(如文件I/O)

    • 主动调用runtime.Gosched()

    • 通道操作阻塞

    • 垃圾回收标记阶段

3)性能优化设计
  • 避免全局锁

    :

    • 采用分布式调度,每个P独立管理本地队列

    • 全局队列仅作为备用,使用原子操作减少锁竞争

  • 协作式调度

    :

    • 在函数调用边界插入调度点

    • 长时间运行的循环会被编译器插入抢占检查

  • 网络轮询器

    :

    • 将网络I/O与系统线程解耦

    • 使用epoll/kqueue等系统调用实现高效事件通知

4)与传统线程对比
  • 创建数量

    :

    • 传统线程:通常数百个即达极限

    • Goroutine:轻松创建数十万个

  • 内存占用

    :

    • 传统线程:默认栈大小2MB(Linux)

    • Goroutine:初始栈2KB,动态伸缩

  • 调度开销

    :

    • 传统线程:需要内核介入,完整上下文切换

    • Goroutine:用户态切换,仅保存必要寄存器

二、知识小结
知识点核心内容考试重点/易混淆点难度系数
Go Routine 概念用户级线程(协程),与操作系统线程的区别,轻量级并发模型协程 vs 线程:协程由 Go 运行时调度,线程由操作系统调度⭐⭐
线程调度模型对比Java(JVM 调度) vs Go(直接与操作系统交互)虚拟机调度 vs 直接交互:Java 依赖 JVM,Go 直接操作线程⭐⭐⭐
操作系统线程调度线程切换需操作系统介入(寄存器保存、上下文切换),高开销线程切换成本:系统调用、结构体初始化耗时⭐⭐⭐⭐
Go 调度器(GMP 模型)G(协程)、M(线程)、P(处理器) 三层调度,协程绑定到固定线程池M:N 映射:少量线程(如 CPU 核数)调度大量协程⭐⭐⭐⭐
调度器优化(锁竞争)多 P(处理器)分片调度,减少全局锁争用锁粒度:P 本地队列 vs 全局队列⭐⭐⭐⭐⭐
协程挂起与抢占协程因 Sleep/网络请求/系统调用挂起时,P 解绑并调度其他协程非抢占式 vs 抢占式:Go 1.14 前需主动让出 CPU⭐⭐⭐⭐
性能优势协程切换无系统调用/寄存器操作,代价远低于线程切换上下文切换成本:协程 ≈ 函数调用,线程 ≈ 系统调用⭐⭐⭐
  • 摘要

    该视频主要讲述了Go语言中的Weight Group包,它是一个用于管理并发Go Routines执行顺序的重要工具。通过Add方法添加需要等待的协程,使用Wait方法阻塞当前协程,直到所有添加的协程执行完成。视频还介绍了Weight Group的实现原理和使用方法,包括计数器、Down方法、defer关键字等,并强调了正确使用Weight Group的重要性,以避免潜在问题。通过该视频,观众可以更深入地了解Weight Group,并在编写Go语言并发程序时更加熟练地运用它。

  • 分段总结

    折叠

    00:01wait group介绍

    1.wait group是一个用于等待协程(goroutine)执行的包。 2.它可以解决主线程不要挂掉,子线程继续运行的问题。 3.与使用sleep相比,wait group更加优雅,且不需要知道具体等待时间。

    01:27wait group的使用

    1.使用wait group时,需要先声明一个wait group变量,不需要实例化。 2.wait group变量是一个struct,其状态不需要手动设置。 3.wait group提供了三个方法:add、wait和done。

    03:16add方法

    1.add方法用于添加需要监控的协程数量。 2.如果事先知道协程数量,可以直接写入;如果不知道,可以在每次启动协程时调用add方法。

    04:28wait方法

    1.wait方法用于阻塞主线程,直到所有需要监控的协程执行完成。 2.当监控的协程数量变为零时,wait方法返回。

    05:13done方法

    1.done方法用于通知wait group一个协程已经执行完成。 2.每个协程执行结束后都需要调用done方法。 3.add方法和done方法需要成对出现,确保所有协程都得到监控。

    06:29wait group的注意事项

    1.使用wait group时,需要确保add方法和done方法成对出现,防止死锁。 2.很多时候为了防止忘记调用done方法,会使用defer来确保在函数退出前调用done方法。

  • 重点

    本视频暂不支持提取重点

一、wait group 00:05
1. 问题引入 00:21
  • 问题背景: 在Go语言中,主协程(main goroutine)结束时所有子协程也会被强制终止,但需要确保子协程完成工作后再结束主协程

  • 传统方案缺陷: 使用time.Sleep()方法不优雅且无法准确预估需要等待的时间

  • 核心问题

    :

    • 子goroutine如何通知主goroutine自己已结束

    • 主goroutine如何知道所有子goroutine已完成

2. wait group介绍 01:51
1)wait group结构 02:46
  • 本质: sync包中的WaitGroup结构体,用于协程同步

  • 特点

    :

    • 无需显式初始化即可使用

    • 内部维护计数器状态

    • 禁止在首次使用后复制(noCopy机制)

2)wait group的方法 03:13
  • Add

    03:18

    • 功能: 设置需要等待的goroutine数量

    • 参数: 接受整数delta值,可为正负

    • 注意事项

      :

      • 正数delta应在Wait前调用

      • 计数器归零时释放所有阻塞的Wait

      • 计数器为负会引发panic

  • Wait

    04:25

    • 阻塞机制: 阻塞当前goroutine直到计数器归零

    • 使用场景: 主goroutine中调用,等待所有子goroutine完成

    • 替代方案: 相比time.Sleep更精确可靠

  • Done

    05:29

    • 作用: 将计数器减1,表示一个goroutine完成

    • 配套使用: 必须与Add成对出现

    • 最佳实践: 建议在goroutine开头使用defer wg.Done()确保执行

  • 应用案例

    06:14

    • 例题:wait group等待戈程序执行

      • 实现步骤

        :

        • 声明var wg sync.WaitGroup

        • wg.Add(100)设置监控数量

        • 每个goroutine执行wg.Done()

        • 主goroutine调用wg.Wait()

      • 注意事项

        :

        • Add数量与实际goroutine数量必须一致

        • 忘记调用Done会导致deadlock

        • 可重用但需保证前次Wait已完成

      • 执行流程

        :

        • 计数器从100开始递减

        • 归零时Wait解除阻塞

        • 打印"all done"表示全部完成

二、知识小结
知识点核心内容考试重点/易混淆点难度系数
Wait Group 的作用用于等待多个 goroutine 执行完成,确保主线程不提前退出Add 和 Done 必须成对使用,否则会导致死锁⭐⭐⭐
Wait Group 的方法Add(n):设置监控的 goroutine 数量Done():减少计数器(相当于 -1)Wait():阻塞直到计数器归零Done 必须在 goroutine 结束时调用,推荐使用 defer 确保执行⭐⭐⭐⭐
Wait Group 的典型用法1. 声明 var wg sync.WaitGroup2. wg.Add(n) 设置数量3. 在 goroutine 中使用 defer wg.Done()4. wg.Wait() 等待所有 goroutine 完成避免重复调用 Add,推荐在循环外统一设置数量⭐⭐⭐
常见错误1. 忘记调用 Done() 导致死锁2. Add 和 Done 数量不匹配3. 在 goroutine 内部调用 Add 导致竞态条件必须确保 Add 和 Done 数量一致⭐⭐⭐⭐
优化实践使用 defer wg.Done() 确保 goroutine 结束时自动减少计数器避免在主线程中使用 time.Sleep 等待 goroutine⭐⭐⭐
  • 摘要

    该视频主要讲述了在Go语言中如何使用互斥锁解决资源竞争问题。Go语言相比Java,锁机制较为简单,但处理共享资源时仍需注意。视频通过示例展示了在并发环境下,多个goroutine对共享变量total进行加减操作时出现的资源竞争问题,导致结果不可预知。随后,视频强调了使用互斥锁(mutex)来同步访问共享资源,确保数据一致性。

  • 分段总结

    折叠

    00:01Go语言锁的简介

    1.Go语言的锁比其他语言简单,特别是比Java简单。 2.Java的锁库非常多,包括synchronized、Lock、volatile等。 3.Go语言的锁功能较少,更推荐通过通信共享变量来解决问题。

    01:01互斥锁的使用

    1.互斥锁用于解决共享资源的竞争问题。 2.例子:假设有一个共享变量total,两个goroutine分别对total进行加法和减法操作。 3.不加锁的情况下,结果不可预知,因为多个goroutine同时访问和修改共享变量时存在竞争条件。

    11:37Mutex互斥锁的使用

    1.Mutex互斥锁用于确保同一时间只有一个goroutine可以访问共享资源。 2.使用方法:lock和unlock,确保同一把锁被同一把锁保护。 3.锁不能复制,复制后失去效果,因为锁的本质是修改状态值。

    13:39Atomic原子操作包的使用

    1.Atomic原子操作包用于将简单操作原子化,如加一或减一。 2.Atomic包中的Add方法可以接受int32或int64类型的参数,并进行原子操作。 3.使用Atomic包可以简化原子操作,提高性能。

  • 重点

    本视频暂不支持提取重点

一、通过mutex和atomic完成全局变量的原子操作 00:00
1. 使用互斥锁 01:03
  • 语言特点: Go语言的锁比其他语言(如Java)简单很多,不推荐通过共享变量方式通信

  • 设计理念: Go语言在设计时对锁的功能相对精简,更推荐使用channel进行通信

1)例题:资源竞争问题处理 01:08
  • 问题现象: 当多个goroutine同时操作共享变量total时,结果不可预期

  • 原因分析

    :

    • 非原子操作: 看似简单的total += 1实际包含3个步骤:加载值→计算→写入

    • 竞争条件: 不同goroutine的这三个步骤可能交叉执行,导致最终结果错误

  • 示例结果

    :

    • 单独运行加法或减法函数结果正确(±100000)

    • 并发运行时结果随机(如-41995、-98454等)

2)使用锁保证原子性 11:27
  • 实现方法

    :

    • 声明: var lock sync.Mutex

    • 加锁: lock.Lock()

    • 解锁: lock.Unlock()

  • 注意事项

    :

    • 同一把锁: 必须使用同一把锁保护同一资源

    • 不可复制: 锁复制后会失去效果,因为内部状态不会被共享

    • 及时释放: 必须配对使用Lock/Unlock,避免死锁

  • 效果: 将临界区代码串行化,保证最终结果为0

3)应用案例 13:28
  • 例题:锁的应用

    • 小数据量现象: 当循环次数较少(如10次)时可能看似正常

    • 本质原因: 并非没有竞争,而是执行速度太快未发生上下文切换

    • 正确做法: 无论数据量大小都应使用锁保护共享资源

4)使用atomic原子包 15:19
  • 例题:atomic包的应用

    15:54

    • 适用场景: 简单数值类型的原子操作

    • 实现方法

      :

      • 类型限制: 需使用int32/int64等特定类型

      • 原子操作: atomic.AddInt32(&total, 1)

    • 优势: 性能高于互斥锁

    • 局限性: 只能用于简单操作,无法保护代码块

    • 选择建议

      :

      • 简单数值操作优先使用atomic

      • 复杂逻辑或代码块保护使用mutex

二、知识小结
知识点核心内容考试重点/易混淆点难度系数
Go语言锁机制Go语言的锁相比Java更简单,主要提供sync.Mutex互斥锁,不推荐通过共享变量通信锁的不可复制性(复制后失去锁效果)⭐⭐
互斥锁使用通过sync.Mutex的Lock()和Unlock()方法保护共享资源,确保代码段原子性锁的释放时机(必须显式调用Unlock())⭐⭐⭐
资源竞争问题多协程对共享变量(如total)的非原子操作(加载→计算→写入)导致结果不可控竞态条件复现(小规模循环可能掩盖问题)⭐⭐⭐⭐
原子操作包atomic包提供AddInt32等方法实现无锁原子操作,适用于简单加减场景与互斥锁的选择(原子操作性能更高,但仅支持基础类型)⭐⭐
读写锁对比未展开讲解,但提及下一课将介绍读写锁(推测为sync.RWMutex)读写锁适用场景(读多写少时提升并发性)⭐⭐⭐
  • 摘要

    该视频主要讲述了读写锁的概念、与互斥锁的区别及其应用场景。读写锁用于优化读多写少的并发场景,通过允许多个读操作并发执行,同时保证写操作与读、写操作之间的互斥性,以提高性能。视频还提到锁会将并行代码串行化,影响性能,但读写锁通过区分读写操作,在保持数据一致性的同时,提高了系统的并发能力。

  • 分段总结

    折叠

    00:01读写锁的基本概念

    1.读写锁是一种用于并行编程的锁机制,用于平衡读操作和写操作之间的冲突。 2.读写锁允许多个读线程同时访问共享资源,但写线程互斥地访问资源。 3.读写锁的目标是提高并发性能,特别是在读多写少的场景下。

    00:30锁的性能影响

    1.锁的本质是将并行代码串行化,使用锁会影响性能。 2.锁的性能下降较为厉害,特别是在高并发环境下。 3.在并发编程中应尽量避免使用锁,但有时不得不使用。

    01:53读写锁的应用场景

    1.读写锁适用于读多写少的场景,如Web系统中。 2.读操作远远多于写操作,如商品详情页和库存信息。 3.读线程之间可以并发,但写线程和读线程之间必须互斥。

    05:45读写锁的使用方法

    1.读写锁定义了一个全局变量,包含写锁和读锁的方法。 2.写锁用于防止其他写线程和读线程获取资源。 3.读锁不会阻止其他读线程获取资源。 4.读写锁的使用包括加锁和解锁操作。

    11:15读写锁的防止读取特性

    1.写锁能够防止其他读线程获取资源,确保写操作的原子性。 2.通过示例代码演示了写锁如何阻止读线程获取资源。 3.读线程在写锁持有期间无法获取读锁。

一、读写锁 00:13
1. 读写锁与互斥锁的区别 00:18
  • 本质区别:互斥锁(Mutex)将并行代码完全串行化,而读写锁(RWMutex)允许读操作并行,仅在写操作时串行。

  • 应用场景:适用于读多写少的场景(如商品详情页读取),读操作频率远高于写操作(比例可达90%读:10%写)。

  • 性能影响:读写锁通过允许并发读显著提升性能,相比互斥锁减少75%以上的锁竞争开销。

  • 设计原则

    :即使使用锁也应尽量保证并行,串行化是最后手段。读写锁实现了:

    • 读协程间:完全并发

    • 读写协程间:强制串行

    • 写协程间:完全互斥

2. 读写锁的使用 05:54
1)写协程实现 06:34
  • 加锁方式:使用Lock()方法获取写锁,会阻塞其他所有读写操作

  • 典型流程:

  • 注意事项

    • 写锁期间读操作会被阻塞

    • 必须使用defer确保锁释放

    • 写操作应尽量快速完成

2)读协程实现 08:43
  • 加锁方式:使用RLock()获取读锁,允许多个读操作并发

  • 典型流程:

  • 关键特性

    • 读锁不阻止其他读锁

    • 读锁会阻止写锁获取

    • 使用专门的RUnlock()释放

3)waitGroup同步 10:07
  • 初始化:var wg sync.WaitGroup

  • 计数器:wg.Add(6)对应6个协程

  • 结束标记:每个协程执行defer wg.Done()

  • 等待结束:主协程调用wg.Wait()

4)应用案例 11:28
  • 读写锁基础示例

    • 演示要点

      • 写锁获取后打印"get write lock"

      • 读锁每500ms打印"get read lock"

      • 使用time.Sleep控制执行顺序

  • 间隔打印示例

    12:51

    • 实现方法

      • 读协程:time.Sleep(500*time.Millisecond)

      • 写协程:time.Sleep(5*time.Second)

    • 现象观察

      • 前3秒并发读打印

      • 写锁获取后读打印停止

      • 写锁释放后恢复读打印

  • 多协程竞争示例

    14:18

    • 设计要点

      • 启动5个读协程+1个写协程

      • 写协程延迟3秒获取锁

      • 读协程循环获取读锁

    • 运行效果

      • 初始阶段:5个读协程交替打印

      • 写锁阶段:所有读打印暂停

      • 写锁释放:读打印恢复

二、知识小结
知识点核心内容考试重点/易混淆点难度系数
读写锁(RWLock)读写锁用于区分读操作和写操作的并发控制,允许多个读操作并行,但写操作必须互斥读写锁与互斥锁的区别:互斥锁完全串行化,读写锁在读多写少场景性能更优⭐⭐⭐
读写锁特性- 读锁不阻塞其他读锁,但阻塞写锁- 写锁阻塞所有读锁和写锁写锁的强互斥性:写操作期间禁止读操作,避免数据不一致(如商品价格变更场景)⭐⭐⭐⭐
读写锁实现(Go)使用sync.RWMutex:- RLock():加读锁- Lock():加写锁- Unlock()/RUnlock():释放锁方法命名易混淆:Lock()为写锁,RLock()为读锁,需注意区分⭐⭐
并发编程原则- 尽量避免锁- 必须锁时优先保证并行性- 串行化是最后手段锁的性能影响:锁将并行代码串行化,导致性能下降⭐⭐⭐
典型应用场景读多写少场景(如商品详情页、库存系统),读操作远多于写操作数据一致性要求:写操作期间禁止读操作(如支付时价格校验)⭐⭐⭐⭐
代码演示- 写协程:Lock()→修改数据→Unlock()- 读协程:RLock()→读取数据→RUnlock()时序控制问题:演示中通过time.Sleep强制协程执行顺序,实际需用通道同步⭐⭐⭐
  • 摘要

    该视频主要讲述了在Go语言中,无缓冲的channel在使用时可能会遇到的问题。无缓冲的channel在发送和接收数据时,如果没有匹配的操作,会导致阻塞。为了避免这种情况,需要单独起一个goroutine去消费数据,保证数据的正确传递。此外,视频还介绍了有缓冲的channel和无缓冲的channel的区别和应用场景,以及Go语言中happen before的机制如何保证数据的正确性。在使用channel时,需要注意无缓冲channel的使用,避免出现data lock的问题。

  • 分段总结

    折叠

    00:01并发编程中的同步机制

    1.同步机制在并发编程中非常重要,用于协调多个线程或协程(goroutine)之间的操作。 2.Go语言中,goroutine之间的通信机制采用channel。

    01:02Go语言的并发编程哲学

    1.Go语言的设计哲学是不要通过共享内存来通信,而是通过通信来实现内存共享。 2.这种设计哲学避免了多线程编程中常见的竞态条件和死锁问题。

    02:05其他编程语言中的通信方式

    1.在其他编程语言中,多线程编程通常通过全局变量或消息队列来进行通信。 2.全局变量:通过共享内存来通信,需要解决竞态条件问题。 3.消息队列:生产者-消费者模式,通过队列来发送和接收消息。

    03:32Go语言的channel机制

    1.channel提供了简单的语法糖,用于在goroutine之间发送和接收数据。 2.channel可以理解为其他语言中的消息队列,但使用起来更简单。

    04:12如何定义和初始化channel

    1.定义channel与定义变量类似,需要指定channel中放置的数据类型。 2.初始化channel可以使用make函数,指定channel的大小。 3.未初始化的channel默认为nil。

    07:33如何向channel中发送和接收数据

    1.使用<-符号向channel中发送数据或从channel中接收数据。 2.发送操作:将值放在<-符号的左侧,channel放在右侧。 3.接收操作:将channel放在<-符号的左侧,变量名放在右侧。

    09:19无缓冲channel的使用注意事项

    1.无缓冲channel的初始化值为0时,发送和接收操作都会阻塞。 2.无缓冲channel容易造成死锁,需要启动一个goroutine来消费数据。 3.happen before机制保证先写入数据的goroutine会先于其他goroutine执行。

  • 重点

    本视频暂不支持提取重点

一、通过channel进行goroutine之间的通信 00:09
1. go语言的设计哲学 01:11
  • 核心原则: 不要通过共享内存来通信,而要通过通信来实现内存共享

  • 与传统语言区别: 其他语言(PHP/Python/Java)多线程通信常用全局变量,而Go推荐使用channel

  • 设计优势: 提供语法糖使channel使用更简单,类似其他语言的消息队列机制但更易用

2. 定义channel 04:15
1)举例理解channel 04:43
  • 通信模型: 两个goroutine(g1和g2)通过建立的通道传递数据

  • 工作方式: 发送方将数据放入通道,接收方从通道取出数据

  • 类型限制: 必须指定通道传输的数据类型(如string),不像动态语言可传任意类型

2)定义有默认值的channel 05:52
  • 声明方式: var msg chan string

  • 默认值: 未初始化的channel值为nil

  • 初始化方法: 必须使用make函数初始化后才能使用

3)定义有初始值的channel 07:15
  • make语法: msg = make(chan string, 1)

  • 缓冲区大小: 第二个参数指定通道缓冲区容量

  • 底层实现: 使用环形数组实现

  • 符号意义解释

    07:48

    • 发送操作: msg <- "bobby" 表示将字符串放入通道

    • 接收操作: data := <- msg 表示从通道取出数据

    • 方向记忆: 箭头方向表示数据流动方向

  • 应用案例

    08:58

    • 例题:channel通信示例

      • 基本流程

        :

        • 创建带缓冲区的channel

        • 发送数据到channel

        • 从channel接收数据

      • 输出验证: 成功打印出"bobby"证明通信正常

4)channel的用法注意事项 10:20
  • 死锁风险: 无缓冲channel(make(chan string,0))在单goroutine中会导致死锁

  • 解决方案: 必须启动单独goroutine进行数据接收

  • happen before机制: Go保证goroutine启动先于channel操作执行

5)初始化渠道时参数的变动 11:17
  • 无缓冲channel: 缓冲区大小为0,发送会阻塞直到有接收者

  • 有缓冲channel: 缓冲区大于0(如1或10),可暂存未接收的数据

  • 选择原则: 根据通信场景决定是否需要缓冲区

二、知识小结
知识点核心内容考试重点/易混淆点难度系数
Go语言并发通信机制通过channel实现协程(goroutine)间通信,避免共享内存无缓冲channel的阻塞特性(需单独goroutine消费)⭐⭐⭐⭐
channel定义与初始化语法:var ch chan string = make(chan string, size),size=0为无缓冲默认值为nil,必须用make初始化⭐⭐
channel操作语法糖<-符号:ch <- data(发送)、data := <-ch(接收)方向决定操作类型(左送右取)⭐⭐
无缓冲channel同步通信,发送/接收均会阻塞,需配合goroutine使用死锁风险(单协程先写后读或先读后写)⭐⭐⭐⭐
有缓冲channel异步通信,缓冲区满时发送阻塞,空时接收阻塞缓冲区大小影响性能(需权衡)⭐⭐⭐
Go设计哲学“通过通信共享内存”(非共享内存通信),优先使用channel而非全局变量与其他语言(Java/PHP/Python)的消息队列模式对比⭐⭐⭐
happen before机制保障goroutine执行顺序,确保无缓冲channel的消费先于生产底层环形数组实现⭐⭐⭐⭐
常见死锁场景1. WaitGroup未调用Done;2. 无缓冲channel单协程操作需启动独立goroutine消费无缓冲channel⭐⭐⭐⭐
一、渠道分类应用场景分类 00:00
  • 无缓冲channel适用场景:适用于需要即时通知的场景,例如当B需要第一时间知道A是否已完成某个事件时。由于没有缓冲区,A一旦发出消息B就能立即接收。

  • 有缓冲channel适用场景:适用于生产者-消费者模式,例如爬虫场景中生产者不断放入URL,消费者持续读取URL进行抓取。缓冲区可以平衡生产消费速率差异。

二、Go中channel的应用场景 02:00
  • 消息传递与过滤:作为goroutine间的通信管道

  • 信号广播:实现系统级别的信号通知机制

  • 事件订阅与广播:建立事件驱动的响应模式

  • 任务分发:将任务分配给多个工作goroutine

  • 结果汇总:收集并整合多个goroutine的处理结果

  • 并发控制:通过channel实现并发量的限制

  • 同步与异步:协调不同goroutine的执行时序

  • 设计原则:遵循"不要通过共享内存来通信,而要通过通信来实现内存共享"的并发编程哲学,优先考虑使用channel而非全局变量实现goroutine间通信。

三、知识小结
知识点核心内容考试重点/易混淆点难度系数
Channel类型分为有缓冲channel和无缓冲channel区分适用场景及底层机制⭐⭐
无缓冲channel适用于实时通知场景(如事件触发、同步等待)无缓冲导致发送与接收必须同步完成⭐⭐⭐
有缓冲channel适用于生产者-消费者模型(如爬虫URL队列)缓冲大小影响吞吐量与资源占用⭐⭐
Channel应用场景1. 消息传递/过滤2. 信号广播3. 事件订阅与广播4. 任务分发5. 结果汇总6. 并发控制7. 同步与异步并发控制和任务分发为高频考点⭐⭐⭐⭐
编程实践建议优先考虑使用channel解决通信、同步问题易混淆无缓冲channel与有缓冲channel的选择逻辑⭐⭐⭐
  • 摘要

    该视频主要讲述了在Go语言中如何使用channel进行数据的发送和接收,并重点介绍了如何通过for range语法来连续地消费channel中的数据。视频还演示了当channel中没有数据时,for range会阻塞等待,直到有新的数据被发送。此外,视频还提到了一种特殊的机制,即当channel被close后,for range会立即退出循环,这是Go语言与其他编程语言在处理队列时的一个显著区别。通过这种方法,生产者可以通知消费者停止消费数据,从而实现更灵活的数据处理流程。

  • 分段总结

    折叠

    00:01channel的介绍和使用

    1.介绍了channel的基本概念,包括如何发送和接收数据。 2.讨论了消费者和生产者如何从channel中源源不断地取数据。

    00:38range在channel中的应用

    1.介绍了range语法在channel中的应用,可以方便地遍历channel中的数据。 2.通过示例代码展示了如何使用range来不断从channel中取数据并消费。

    03:29for range的语法和用法

    1.详细讲解了for range的语法,包括如何从channel中获取值并进行打印。 2.通过示例代码展示了for range的简单用法和便捷性。

    04:14close方法在channel中的应用

    1.介绍了close方法在channel中的应用,用于关闭channel并通知for range退出循环。 2.通过示例代码展示了如何使用close方法来关闭channel,并讨论了关闭后是否可以继续从channel中取值的问题。

  • 重点

    本视频暂不支持提取重点

  • 摘要

    该视频主要讲述了Go语言中单向channel的概念、定义、初始化及其使用限制。首先解释了channel默认为双向,但可通过特定语法定义单向channel,分为只发送和只接收两种。接着展示了如何初始化单向channel,并指出即使原始channel为双向,也可通过赋值操作将其限制为单向。最后强调了单向channel的使用限制,即不能向只接收的channel发送数据,也不能从只发送的channel接收数据,且单向channel不能转回普通channel。

  • 分段总结

    折叠

    00:01单项Channel的定义和用途

    1.单项Channel允许我们限制对Channel的操作,使其只能发送或接收数据。 2.默认情况下,Channel是双向的,可以发送和接收数据。 3.在函数调用中,我们经常希望Channel只能单向使用,以避免数据竞争和错误。

    02:05单向Channel的定义和初始化

    1.单向Channel通过在Channel类型后添加箭头符号来定义,箭头方向表示数据的流动方向。 2.send only Channel只能发送数据,不能接收数据。 3.receive only Channel只能接收数据,不能发送数据。 4.初始化单向Channel时,可以直接将双向Channel赋值给单向Channel。

    06:14单向Channel在实践中的应用

    1.生产者(Producer)只能往Channel中写入数据,因此接受send only类型的Channel。 2.消费者(Consumer)只能从Channel中读取数据,因此接受receive only类型的Channel。 3.在main函数中,我们创建了一个双向Channel,并将其传递给生产者和消费者。 4.生产者向Channel中写入数据,消费者从Channel中读取数据。 5.通过sleep函数确保消费者有足够的时间来读取所有数据。

  • 重点

    本视频暂不支持提取重点

  • 摘要

    该视频主要讲述了如何使用Go语言的channel实现两个goroutine交叉打印数字和字母的面试题。首先,通过定义两个无缓冲的channel(一个用于数字,一个用于字母)来控制打印顺序。一个goroutine负责打印数字,每打印两个数字后,通过channel通知另一个goroutine打印字母。字母打印的goroutine在接收到通知后,打印一个字母,并再次通过channel通知数字打印的goroutine。这样循环进行,实现了数字和字母的交替打印。

  • 分段总结

    折叠

    00:01课程介绍和目标

    1.介绍课程内容,讲解Go语言Channel的一道常见面试题。 2.课程目标:使用Channel实现交叉打印序列,交替打印数字和字母。

    00:30题目描述和要求

    1.题目描述:使用两个Go routine交叉打印序列,一个打印数字,一个打印字母。 2.要求:实现效果为一二AB三四CD,交替打印数字和字母。

    01:03使用Channel进行通知

    1.使用Channel进行互相通知,实现交替打印过程。 2.先写一段打印数字的Go routine,使用Channel等待另一个Go routine的通知。

    01:55打印数字的Go routine

    1.打印数字的Go routine使用for循环,从1开始,每次打印两个数字。 2.使用Channel接收通知,打印完数字后发送通知给另一个Go routine。

    04:52打印字母的Go routine

    1.打印字母的Go routine使用for循环,从A开始,到Z结束。 2.使用Channel接收通知,打印完字母后发送通知给另一个Go routine。

    06:30主函数和并发控制

    1.主函数中定义两个Go routine,先启动打印数字的Go routine。 2.使用Channel进行交替触发,防止主Go routine退出。

    07:27运行结果和问题解决

    1.运行结果:出现panic,提示超过长度。 2.解决问题:添加长度判断,防止索引超出范围。

    08:47总结和推荐做法

    1.总结:Channel在并发编程中的重要性。 2.推荐做法:使用Go语言的Channel方式完成并发编程任务。

  • 重点

    本视频暂不支持提取重点

一、Go语言并发编程 00:04
1. 例题:交替打印数字字母 00:38
  • 题目解析

    • 需求分析:使用两个goroutine交替打印序列,一个打印数字(连续两个),一个打印字母(连续两个)

    • 示例输出:12AB34CD56EF78GH...2526YZ2728

    • 核心难点:实现goroutine间的同步控制

    • 解决方案:使用无缓冲channel进行通信协调

1)实现方案
  • 同步机制

    • channel声明:var number, letter = make(chan bool), make(chan bool)

    • 类型选择:可使用bool/int等简单类型作为信号量

    • 无缓冲特性:确保严格的同步顺序

  • 数字打印逻辑

    • 初始值:从1开始(i := 1)

    • 打印格式:fmt.Printf("%d%d", i, i+1)

    • 步进值:每次增加2(i += 2)

    • 流程控制:

2)字母打印实现
  • 关键实现

    • 字母序列:"ABCDEFGHIJKLMNOPQRSTUVWXYZ"

    • 边界检查:需添加if i >= len(str)判断防止越界

    • 索引处理:从0开始,每次取2个字符(str[i:i+2])

    • 流程控制:

3)主程序控制
  • 启动顺序

    • 先启动两个goroutine:go printNum()和go printLetter()

    • 初始触发:number <- true启动数字打印

  • 防止退出

    • 使用time.Sleep保持主goroutine运行

    • 实际项目中应使用sync.WaitGroup等更优雅的方式

2. channel的核心作用 08:15
  • 通信机制

    • 通知功能:通过发送/接收操作实现goroutine间的精确同步

    • 无缓冲优势:保证发送和接收的严格先后顺序,天然适合此类同步场景

  • 设计思想

    • 推荐做法:channel是Go语言并发编程的首选方案

    • 思维转变:不同于其他语言的锁机制,通过通信共享内存而非通过共享内存通信

  • 实际应用

    • 常见场景:生产者-消费者模型、工作池、任务分发等

    • 错误处理:需注意channel的关闭和panic处理

二、知识小结
知识点核心内容考试重点/易混淆点难度系数
Go语言channel应用使用两个goroutine交叉打印数字和字母序列无缓冲channel的同步机制⭐⭐⭐
并发控制通过channel实现goroutine间的交替执行channel阻塞与唤醒的触发条件⭐⭐⭐⭐
字符串处理字母序列的切片操作(string[i:i+2])数组越界检查的必要性⭐⭐
并发编程范式通信共享内存优于共享内存通信channel与锁机制的对比选择⭐⭐⭐⭐
错误处理goroutine中的panic预防(数组边界检查)defer-recover在并发场景的应用⭐⭐⭐
  • 摘要

    该视频主要讲述了Go语言中的select语句及其在并发编程中的应用。select语句类似于switch case语句,但主要用于处理多个channel的通信。当执行select语句时,Go语言会从就绪的channel中选择一个进行操作,从而避免了使用全局变量和循环检查带来的CPU消耗。此外,select语句在Go语言中的作用并非取代操作系统的select系统调用,而是与其紧密配合,为并发编程提供了更加灵活和高效的解决方案。通过select语句,开发人员可以方便地实现协程之间的通信和同步,提高程序的并发性能和可维护性。

  • 分段总结

    折叠

    00:01select语句介绍

    1.select语句用于并发编程,类似于switch case语句,但主要用于多个channel上。 2.select语法与操作系统或Linux中的网络IO select函数类似,但作用不同。 3.select语句用于选择已就绪的channel,常与channel一起使用。

    01:59select语句的执行方式

    1.select语句在执行时会选择当前已就绪的channel。 2.适用于在多个goroutine中等待某个goroutine完成,并获取完成的具体信息。 3.通过全局变量或共享变量来检测goroutine是否完成的方法较为简单但效率较低。

    04:43使用全局变量检测goroutine完成

    1.通过设置全局变量来检测goroutine是否完成。 2.使用for循环不断检查全局变量的状态。 3.为了减少CPU消耗,通常会添加短暂的sleep。

    07:35使用channel优化goroutine检测

    1.使用channel来优化goroutine完成的检测。 2.下一节课将详细讲解如何使用channel来完成更优雅的代码实现。

  • 重点

    本视频暂不支持提取重点

一、select语句 00:01
1. select目录 00:24
  • 目录创建:在课程中创建了名为"select"的目录用于演示select语句的使用

  • 文件结构:包含main.go和sel.go两个文件,分别用于不同场景的演示

2. select语句功能讲解 00:42
  • 语法类比:select语句的用法类似于switch case语句,但在功能上有本质区别

  • 核心用途:主要用于处理多个channel的并发操作

  • 系统级对比:与Linux系统中的select、poll、epoll机制类似但不相同,Go的select专门用于channel通信

3. 需求问题 02:15
  • 场景描述:需要监控两个goroutine的执行状态,当任意一个完成时立即获知

  • 具体需求

    • 两个goroutine执行时间不同(g1执行1秒,g2执行2秒)

    • 主goroutine需要实时感知任一子goroutine完成的状态

    • 需要知道具体是哪个goroutine完成了任务

4. 传统解决方法及问题 02:28
  • 实现方案

    • 使用全局布尔变量done标记完成状态

    • 配合sync.Mutex保证并发安全

    • 主goroutine通过for循环不断检查done状态

  • 代码示例:

5. 传统方法的弊端 07:07
  • 性能问题

    • 循环检查会消耗大量CPU资源

    • 即使添加sleep也会引入延迟

  • 设计问题

    • 使用共享变量违背Go语言并发设计哲学

    • 需要手动处理竞态条件,代码复杂度高

  • 改进方向

    • 应该使用channel替代共享变量

    • 通过select语句实现更优雅的解决方案

    • 符合Go语言"不要通过共享内存来通信,而应该通过通信来共享内存"的设计理念

二、知识小结
知识点核心内容考试重点/易混淆点难度系数
select语句Go语言中用于多通道操作的语法,类似switch case但专用于channel通信与Linux系统select/poll/epoll的区别(非替代关系)⭐⭐
并发控制需求场景监控多个goroutine完成状态(如:两个耗时不同的任务)传统全局变量方案(共享变量+轮询)的CPU消耗问题⭐⭐
空循环问题轮询检查全局变量导致CPU空转解决方案:添加time.Sleep降低资源占用(临时方案)
Go并发设计哲学反对共享变量,提倡通过channel通信实现同步全局变量方案与channel方案的优雅度对比⭐⭐⭐
代码示例两个goroutine(1秒/2秒耗时)通过全局变量done通知主线程关键缺陷:需手动加锁(sync.Mutex)保证线程安全⭐⭐
  • 摘要

    该视频主要讲述了在Go语言并发编程中如何使用channel来同步和传递信息。首先,强调了推荐在并发编程中使用channel来完成任务。接着,介绍了通过建立两个通道来实现g1和g2之间的同步,即g1和g2完成任务后往各自的通道中写入值。还讨论了使用空结构体作为通道传递信息的方式,因为空结构体不占用空间。然后,指出了channel是线程安全的,多个goroutine可以向同一个channel中写入值。最后,提到了当需要监控多个channel时,每个goroutine可以拥有自己的channel,而不是共享同一个channel。

  • 分段总结

    折叠

    00:01并发编程中的通道使用

    1.推荐使用goroutine和channel进行并发编程。 2.通过建立通道(channel)来同步goroutine的执行。 3.全局通道可用于多个goroutine之间的通信。

    01:04通道的数据类型

    1.布尔值:用于表示成功或失败,空间效率高。 2.空结构体:零空间占用,常用于通道通信。 3.通道是线程安全的,多个goroutine可以安全地往同一个通道写值。

    04:00多通道监控

    1.多个goroutine可能写入不同的通道。 2.select语句用于监控多个通道,选择可读的通道。 3.select会随机选择一个就绪的通道进行读取。

    11:20超时机制

    1.select语句支持超时机制,通过default子句实现。 2.default子句会在阻塞操作超时时执行。 3.使用timer可以更优雅地实现超时机制。

  • 重点

    本视频暂不支持提取重点

一、监控多个channel的改写 00:02
1. 使用通道 00:20
  • 通道优势:在Go语言并发编程中强烈推荐使用channel替代锁机制,通过建立两个通道分别用于g1和g2协程的通信

  • 实现方式:每个协程完成任务后往自己的通道写入值,主协程通过监控这些通道来获知任务完成情况

2. 空结构体 01:10
  • 通道类型选择:当只关心成功/失败状态时,可以使用空结构体struct{}作为通道类型

  • 特点

    • 零空间占用,不占用额外内存

    • 在多线程环境下是安全的,多个goroutine可以安全地向同一个channel写入值

  • 实例化:done := make(chan struct{}) 是空结构体通道的定义和实例化方式

3. select语句 02:32
1)select语句介绍
  • 功能类比:类似于switch case语句,但主要用于操作多个channel,类似于Linux中的select/poll/epoll机制

  • 基本用法:监控多个channel,当任一channel就绪时执行对应case分支

  • 阻塞特性:默认情况下会阻塞等待,直到某个case分支就绪

2)channel初始化 03:08
  • 必须初始化:channel使用前必须通过make初始化,否则会导致deadlock

  • 初始化方式:var done = make(chan struct{})是正确初始化方式

  • 错误示例:未初始化的channel会导致fatal error: all goroutines are asleep - deadlock!

4. 多个goroutine写同一个channel 04:45
  • 常见场景:不同goroutine可能写入各自的channel而非共享同一个channel

  • 实现方式:每个goroutine接收自己的channel参数,写入时使用各自的channel

  • 优势:避免共享channel可能带来的复杂性和竞争条件

5. select语句介绍 06:46
  • 核心功能:同时监控多个channel,任一channel就绪即可触发相应操作

  • 语法结构:

  • 应用场景:适用于需要等待多个异步操作中任意一个完成的场景

6. select语句注意事项 08:38
1)例题:channel随机选择 09:08
  • 选择机制

    • 当多个case同时就绪时,select会随机选择一个执行

    • 不是按照代码书写顺序选择,而是随机选择

  • 设计目的:防止某个channel长期获得执行机会而导致其他channel"饥饿"

  • 验证方法:通过缓冲channel预先写入数据,多次运行观察选择顺序的随机性

7. select语句的默认值 12:45
1)例题:select默认值应用 13:03
  • default作用:当所有case都未就绪时,立即执行default分支

  • 典型应用:实现非阻塞的channel操作

  • 限制:单纯的default会导致CPU空转,通常需要配合sleep使用

2)例题:select默认值应用 14:20
  • 优雅实现:使用time.NewTimer创建定时器channel实现超时控制

  • 代码结构:

  • 优势:相比循环检查更高效,避免CPU空转

  • 注意事项:超时后应及时return或break,避免继续执行

二、结束 16:32
三、知识小结
知识点核心内容考试重点/易混淆点难度系数
channel使用在Go语言并发编程中推荐使用channel,通过建立g1和g2两个通道实现通信channel必须初始化,否则会导致deadlock⭐⭐
空结构体用于channel通信时节省空间(零空间占用),是常用写法空结构体定义与实例化的区别
channel特性多线程安全,多个goroutine可安全地向同一个channel写值与锁机制的区别⭐⭐
select语句监控多个channel,选择首先就绪的channel执行多个channel就绪时随机执行(防饥饿机制)⭐⭐⭐
超时处理1. 使用default避免阻塞2. 更优雅方案:time.NewTimer生成带超时的channeldefault方案需配合循环使用,timer方案可直接return⭐⭐⭐⭐
select应用场景1. 多channel监控2. 函数超时控制(结合timer)超时时间设置与业务逻辑的平衡⭐⭐⭐
防饥饿机制select随机执行就绪channel的设计目的:防止某个goroutine持续独占资源类比锁机制的饥饿问题⭐⭐
  • 摘要

    该视频主要讲述了在并发编程中,context(上下文)的重要性以及使用它的好处。通过模拟一个CPU监控程序的需求,演示了如何每隔一段时间去监控CPU信息。然后,通过打印信息的方式展示了程序的执行效果。接着,讨论了如何退出程序,提出了使用共享变量的方式,但强调了使用消息和管道的更好方式。最后,通过修改代码,演示了如何使用context(上下文)来管理程序的执行流程。

  • 分段总结

    折叠

    00:01Context概述

    1.Context是并发编程中非常重要的知识点,可以理解为上下文。 2.Context用于解决并发编程中的信息传递问题。

    00:44Context的使用场景

    1.Context广泛应用于监控、超时控制、取消操作等场景。 2.通过Context传递取消信号、超时信息、值等。

    01:02CPU监控示例

    1.通过goroutine监控CPU信息,每隔两秒打印一次。 2.使用共享变量stop来控制监控程序的退出。

    06:17使用Select改进代码

    1.使用select来等待CPU信息和取消信号。 2.通过channel传递取消信号,实现优雅退出。

    11:23Context的引入

    1.Go语言提出Context来解决信息传递问题。 2.Context是一个interface,可以任意实现并传递。

    12:37Context的树形结构

    1.Context是一个树形结构,父context可以派生出子context。 2.子context可以接收父context的取消信号。

    15:04Context的函数

    1.withCancel:返回一个cancel方法和新的context。 2.withTimeout:返回一个带有超时时间的context。 3.withValue:返回一个带有值的context。

    16:07函数参数中添加Ctx

    1.函数参数中第一个参数通常为ctx,用于传递信息。 2.通过ctx传递取消信号、超时信息、值等。

    18:13Context的超时控制

    1.使用withTimeout函数创建带有超时时间的context。 2.通过调用cancel方法来触发超时。

  • 重点

    本视频暂不支持提取重点

一、并发编程 00:01
1. 渐进式讲解 00:36
  • 教学方式: 采用渐进式讲解方法,先讲解为什么需要使用context,再讲解其好处,最后讲解具体使用方式

  • 与传统差异: context设计与现有编程语言理念有很大差异,需要详细讲解帮助理解

  • 资料对比: 区别于网上资料直接讲解使用方式,本课程从需求出发逐步引入

2. 例题:Go监测CPU信息 01:02
1)基础实现
  • 需求场景: 实现一个持续监控CPU信息的goroutine

  • 实现方式

    :

    • 使用time.Sleep(2*time.Second)模拟每2秒监控一次

    • 通过fmt.Println打印"cpu的信息"代替实际监控代码

    • 使用sync.WaitGroup防止主goroutine提前退出

  • 问题发现: 由于使用无限循环,程序无法正常退出

2)共享变量改进
  • 改进需求: 实现主动退出监控程序的功能

  • 实现方式

    :

    • 定义全局布尔变量stop控制循环退出

    • 主goroutine在6秒后设置stop=true

    • 监控goroutine检查stop变量决定是否退出

  • 注意事项

    :

    • 需要添加defer wg.Done()

    • 建议使用读写锁保护共享变量(留作作业)

3)通道改进方案
  • 优化方向: 使用channel代替共享变量

  • 实现要点

    :

    • 创建stop := make(chan struct{})通道

    • 使用select监听通道和默认操作

    • 收到stop信号时打印"退出cpu监控"并return

    • default分支执行监控逻辑

  • 代码结构:

4)参数传递优化
  • 最佳实践: 通过参数传递stop通道

  • 改进方式

    :

    • 将stop通道定义移到main函数

    • 修改cpuInfo函数签名为func cpuInfo(stop chan struct{})

    • 主goroutine通过stop <- struct{}{}发送退出信号

  • 优势

    :

    • 代码更优雅

    • 符合Go语言并发模式最佳实践

  • 实际应用: 这种模式在开源项目中大量存在

3. Context功能讲解 11:24
1)Context接口 11:56
  • 接口特性:Context是一个interface,只要实现其方法的结构体都可以作为Context传递

  • 核心方法

    • Deadline:返回context的截止时间

    • Done:返回一个channel,当context被取消时会关闭

    • Value:用于获取context中存储的值

  • 设计优势:接口设计使得可以灵活传递各种实现,不依赖具体类型

  • 传值规范:context中的值应该用于请求范围的数据传递,而不是作为函数的可选参数

  • 键设计原则:键应该定义为未导出类型以避免包间冲突

2)WithCancel函数 12:55
  • 基本用法

    • 创建:ctx, cancel := context.WithCancel(context.Background())

    • 取消:调用cancel()函数即可取消context

  • 父子关系

    • 树形结构:Context形成树形结构,可以基于父context派生新的context

    • 传递性:父context取消会导致所有子context也被取消

  • 最佳实践

    • 参数位置:函数第一个参数尽量使用context.Context类型

    • 命名规范:参数名通常简写为ctx

  • 监控实现:通过select监听ctx.Done()通道实现优雅退出

  • 资源释放:调用cancel函数会释放相关资源,应在操作完成后立即调用

3)WithTimeout和WithValue函数 21:04
  • 三大派生函数

    • WithCancel:创建可手动取消的context

    • WithTimeout:创建带超时自动取消的context

    • WithValue:创建可传递值的context

  • 超时控制:WithTimeout可以设置goroutine的最大运行时间

  • 值传递:WithValue用于在context链中传递请求范围的值

  • 层级关系:context可以形成多级父子关系

  • 取消传播:父context取消会级联取消所有子context

  • 实际应用:这种设计非常适合需要跨多个goroutine传递取消信号的场景

二、知识小结
知识点核心内容考试重点/易混淆点难度系数
context概念并发编程中的上下文管理机制,用于协程间通信和控制与常规编程语言设计理念的差异⭐⭐⭐⭐
共享变量方案通过布尔变量stop控制协程退出需配合读写锁保证线程安全⭐⭐
channel方案使用select监听管道实现优雅退出default分支处理与非阻塞逻辑⭐⭐⭐
context基础interface设计(Deadline/Done/Err/Value方法)WithCancel返回新context和cancel函数⭐⭐⭐⭐
树形结构特性父子context的派生关系(Background作为根节点)父节点cancel会触发子节点连锁反应⭐⭐⭐⭐⭐
代码规范函数首个参数建议添加ctx参数业务参数与控制逻辑的分离设计⭐⭐
WithCancel创建可取消的context对象cancel函数无参数调用触发Done信号⭐⭐⭐⭐
传递性机制调用父context的cancel会级联通知所有子context内部通过链表结构实现传播⭐⭐⭐⭐⭐
  • 摘要

    该视频主要讲述了context在编程中的功能和应用,特别是如何通过context实现主动取消和超时控制。视频介绍了两种超时控制方法:主动超时和通过设定时间后的自动取消。同时,视频还提到了context的with value和with deadline用法,并强调了context在业务代码中的零修改特性。整体来看,该视频深入浅出地解释了context在编程中的重要作用,为观众提供了实用的编程知识和技巧。

  • 分段总结

    折叠

    00:01context的主动取消功能

    1.通过vis cancel可以主动取消context,实现主动取消功能。 2.主动取消适用于需要手动触发取消的情况,如满足特定条件或事件触发。

    00:28context的超时功能

    1.context的with timeout功能可以实现超时自动取消。 2.设置超时时间后,如果在规定时间内未完成操作,context将自动取消。 3.超时功能适用于需要自动取消的情况,如定时任务或操作超时。

    03:54context的值传递功能

    1.with value用于向context中传递值,如请求ID或链路追踪ID。 2.值可以在多个接口之间传递,用于记录请求来源或追踪请求链路。 3.with value的使用使得代码更加简洁和可维护。

    04:52链路追踪与context

    1.链路追踪用于记录请求的来源和调用链路。 2.通过context传递请求ID,可以在整个请求链路上追踪请求。 3.with value功能使得链路追踪的实现更加简单和高效。

  • 重点

    本视频暂不支持提取重点

一、context功能 00:01
1. 取消功能 00:05
  • 主动取消机制:通过context.WithCancel创建可取消的context,调用返回的cancel函数即可主动终止关联的goroutine

  • 代码实现:

  • 应用场景:当满足特定业务条件时需要手动终止goroutine执行

2. 超时功能 00:14
1)代码运行 02:06
  • 自动超时:使用context.WithTimeout设置6秒超时后,监控goroutine会自动退出

  • 执行效果:每2秒打印一次"cpu的信息",6秒后自动打印"退出cpu监控"

2)withTimeout函数详解 02:22
  • 函数签名:WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)

  • 参数说明

    • 第一个参数为父context

    • 第二个参数为超时时间(如6*time.Second)

  • 优势:相比手动sleep+cancel,自动超时机制更简洁可靠

3)withTimeout原理 02:34
  • 底层实现:实际调用WithDeadline,在当前时间基础上加上超时时间

  • 核心机制:内部通过timerCtx结构体实现定时取消功能

  • 取消触发:当time.Until(d) <= 0时自动调用cancel

3. withValue功能 03:46
1)代码分析 04:14
  • 传值机制:通过context.WithValue包装context传递键值对

  • 典型应用:传递traceID实现全链路追踪

  • 打印trace ID

    07:50

    • 取值方法:通过ctx.Value(key)获取存储的值

    • 代码示例:

    • 设计优势:不影响原有context的取消/超时功能,实现业务无关数据的透明传递

二、结束 09:50
三、知识小结
知识点核心内容考试重点/易混淆点难度系数
Context的主动取消功能通过WithCancel实现手动取消Context,适用于需要根据条件主动终止的场景区分主动取消与超时自动取消的实现差异⭐⭐
超时自动取消使用WithTimeout设置超时时间(如6秒),到期自动触发取消,无需手动调用理解WithTimeout内部基于WithDeadline实现⭐⭐⭐
指定时间点取消WithDeadline在固定时间点触发取消(如12:00),与WithTimeout本质相同但参数为时间戳时间参数格式需注意(time.Time类型)⭐⭐
上下文传值WithValue传递链路追踪ID等非业务数据,通过ctx.Value(key)获取值仅传递请求级数据,避免滥用导致代码耦合⭐⭐⭐⭐
源码关联性WithTimeout内部调用WithDeadline,两者均依赖cancelCtx实现取消功能源码中baseContext和propagateCancel机制⭐⭐⭐⭐
实际应用场景电商链路追踪:通过WithValue传递traceID,统一日志记录请求来源业务代码零修改兼容多种Context模式⭐⭐⭐

以下是 Go 并发编程的 30 道八股文题(难度递增)和 20 道场景题(涵盖基础到高级应用),系统覆盖 Go 并发的核心概念、机制及实践技巧。

一、八股文题(30 题)

基础篇(1-10 题)
  1. 问题:Goroutine 是什么?与操作系统线程有何区别? 答案

    • Goroutine 是 Go 语言特有的轻量级执行单元,由 Go 运行时管理而非操作系统。

    • 区别:

      • 内存占用:Goroutine 初始栈约 2KB(可动态伸缩),线程通常几 MB。

      • 调度开销:Goroutine 由 Go 调度器在用户态调度,切换成本远低于线程的内核态切换。

      • 数量上限:单机可轻松支持数万 Goroutine,而线程数量通常限制在数千。

  2. 问题:如何创建 Goroutine?启动后如何确保主程序等待其执行完成? 答案

    • 创建:使用go关键字启动,如go func() { ... }()

    • 等待方法:

      • 使用sync.WaitGroup:调用Add(n)设置计数,Goroutine 中调用Done(),主程序调用Wait()阻塞等待。

      • 使用 Channel:通过 channel 传递信号,主程序接收信号后继续执行。

  3. 问题:Channel 的基本概念是什么?有哪几种类型?各自特点是什么? 答案

    • 概念:Channel 是 Goroutine 间通信的管道,用于传递数据和同步。

    • 类型及特点:

      • 无缓冲 Channel:ch := make(chan int),发送和接收操作同步(需配对),一方未准备好则阻塞。

      • 有缓冲 Channel:ch := make(chan int, n),缓冲区未满可发送,未空可接收,缓冲区满 / 空时阻塞。

  4. 问题sync.WaitGroup的作用是什么?使用时需注意哪些问题? 答案

    • 作用:等待一组 Goroutine 完成执行,实现主程序与多个子 Goroutine 的同步。

    • 注意事项:

      • Add(n)需在启动 Goroutine 前调用,确保计数正确。

      • Done()需在 Goroutine 退出前调用(通常用defer)。

      • 不可复制WaitGroup实例(内部有状态)。

      • 避免Add传入负数或Done调用次数超过Add

  5. 问题:什么是竞态条件(Race Condition)?如何检测和避免? 答案

    • 定义:多个 Goroutine 并发访问共享资源且至少有一个是写操作时,因执行顺序不确定导致结果不可预测的情况。

    • 检测:go test -race命令启用竞态检测。

    • 避免:

      • 使用 Channel 传递数据(CSP 模型)。

      • 使用sync.Mutexsync.RWMutex保护共享资源。

      • 使用原子操作(sync/atomic)。

  6. 问题sync.Mutexsync.RWMutex的区别是什么?分别适用于什么场景? 答案

    • sync.Mutex:互斥锁,同一时间只允许一个 Goroutine 访问资源(读 / 写互斥)。

    • sync.RWMutex:读写锁,允许多个读操作并发执行,但写操作与所有读 / 写操作互斥。

    • 适用场景:

      • Mutex:读写频率相近或写操作频繁的场景。

      • RWMutex:读操作远多于写操作的场景(如缓存)。

  7. 问题:如何优雅地关闭 Channel?关闭后的 Channel 有哪些特性? 答案

    • 关闭方式:使用close(ch),仅发送方应关闭 Channel,避免重复关闭。

    • 关闭后特性:

      • 发送操作会触发 panic。

      • 接收操作会立即返回零值和false(第二个返回值)。

      • 可通过for val := range ch遍历,关闭后自动退出循环。

  8. 问题select语句的作用是什么?在并发编程中有何用途? 答案

    • 作用:同时监听多个 Channel 的读写操作,当其中一个可操作时执行对应分支。

    • 用途:

      • 实现非阻塞的 Channel 操作(配合default分支)。

      • 超时控制(结合time.After())。

      • 处理多个数据源的并发事件。

      • 优雅退出(结合退出信号 Channel)。

  9. 问题:Goroutine 的调度模型(GPM)由哪几部分组成?各自的作用是什么? 答案

    • G(Goroutine):用户态线程,包含栈、程序计数器等执行状态。

    • P(Processor):逻辑处理器,维护可运行的 G 队列,关联一个 M。

    • M(Machine):绑定操作系统线程,执行 P 队列中的 G。

    • 工作原理:P 作为 G 和 M 的中间层,实现 M:N 调度,P 的数量默认等于 CPU 核心数(GOMAXPROCS)。

  10. 问题runtime.GOMAXPROCS(n)的作用是什么?如何合理设置其值? 答案

    • 作用:设置可同时执行用户级代码的逻辑处理器(P)数量,控制并行度。

    • 设置原则:

      • 默认值为 CPU 核心数,通常无需修改。

      • CPU 密集型任务:不超过 CPU 核心数。

      • I/O 密集型任务:可适当增大(如 2*CPU 核心数),利用等待 I/O 时的空闲资源。

中级篇(11-20 题)
  1. 问题:什么是上下文(context.Context)?其主要用途是什么? 答案

    • 定义:context包提供的上下文类型,用于在 Goroutine 间传递截止时间、取消信号和请求范围的值。

    • 用途:

      • 控制 Goroutine 生命周期(取消、超时)。

      • 在请求链路中传递元数据(如请求 ID、认证信息)。

      • 协调多个 Goroutine 的退出。

  2. 问题context包中的WithCancelWithTimeoutWithDeadline有何区别?如何使用? 答案

    • WithCancel(parent):创建可手动取消的上下文,调用返回的cancel()函数触发取消。

    • WithTimeout(parent, timeout):创建超时自动取消的上下文,timeout后自动触发。

    • WithDeadline(parent, deadline):创建指定时间点取消的上下文,deadline到达后触发。

    • 使用:将上下文作为函数参数传递,通过ctx.Done()通道监听取消信号。

  3. 问题:如何限制并发执行的 Goroutine 数量?请说明实现思路。 答案

    • 常用方法:使用带缓冲的 Channel 作为信号量。

    • 实现步骤:

      1. 创建容量为N的 Channel(sem := make(chan struct{}, N))。

      2. 启动 Goroutine 前,向 Channel 发送信号(sem <- struct{}{})。

      3. Goroutine 结束后,从 Channel 接收信号(<-sem)释放名额。

    • 效果:同一时间最多有N个 Goroutine 执行。

  4. 问题sync.Once的作用是什么?其实现原理是什么? 答案

    • 作用:确保某个函数只执行一次,即使被多个 Goroutine 调用。

    • 原理:内部通过互斥锁和原子操作实现,第一次调用Do(f)时执行f,后续调用直接返回。

    • 用途:初始化单例、加载配置等只需执行一次的操作。

  5. 问题:什么是工作池(Worker Pool)模式?如何实现一个简单的 Worker Pool? 答案

    • 模式:创建固定数量的 Worker Goroutine,从任务队列中获取任务并执行,实现任务的并发处理和资源控制。

    • 实现:

      1. 创建任务 Channel 和结果 Channel。

      2. 启动N个 Worker,循环从任务 Channel 接收任务并处理。

      3. 向任务 Channel 发送任务,从结果 Channel 收集结果。

  6. 问题time.Tickertime.After的区别是什么?分别适用于什么场景? 答案

    • time.Ticker:周期性触发事件,返回的C通道会定期发送当前时间,需手动调用Stop()释放资源。

    • time.After(d):延迟d时间后发送一次当前时间到返回的通道,一次性使用,无需手动停止。

    • 场景:

      • Ticker:定期任务(如心跳检测、定时刷新)。

      • After:单次超时控制(如select中的超时分支)。

  7. 问题:如何在多个 Goroutine 间收集错误?有哪些常用方法? 答案

    • 方法 1:使用带缓冲的错误 Channel,每个 Goroutine 将错误发送到该 Channel,主程序统一接收。

    • 方法 2:使用golang.org/x/sync/errgroup,等待所有 Goroutine 完成并返回第一个错误。

    • 方法 3:结合context,第一个错误发生时取消上下文,通知其他 Goroutine 退出。

  8. 问题:什么是 "扇出 - 扇入"(Fan-out Fan-in)模式?其优势是什么? 答案

    • 定义:扇出(Fan-out)指多个 Goroutine 并发处理不同任务;扇入(Fan-in)指将多个 Goroutine 的结果汇总到一个 Channel。

    • 优势:充分利用多核 CPU,提高处理效率,适用于数据并行处理场景(如批量任务处理)。

  9. 问题sync.Cond的作用是什么?与Mutex相比有何不同? 答案

    • 作用:条件变量,用于等待某个条件成立,允许 Goroutine 在条件不满足时阻塞,满足时被唤醒。

    • 区别:Mutex用于互斥访问共享资源,Cond用于协调多个 Goroutine 的执行顺序(基于条件)。

    • 方法:Wait()(等待条件)、Signal()(唤醒一个等待 Goroutine)、Broadcast()(唤醒所有等待 Goroutine)。

  10. 问题:如何避免 Goroutine 泄漏?常见的泄漏场景有哪些? 答案

    • 泄漏场景:

      • Goroutine 阻塞在未关闭的 Channel 读写操作上。

      • 无限循环且无退出条件的 Goroutine。

      • 未正确处理context取消信号的 Goroutine。

    • 避免方法:

      • 确保 Channel 操作配对,必要时使用selectdefault避免阻塞。

      • 为 Goroutine 设置退出条件(如context.Done())。

      • 使用带缓冲 Channel 限制阻塞风险。

高级篇(21-30 题)
  1. 问题:Go 调度器的 "工作窃取"(Work Stealing)机制是什么?有何作用? 答案

    • 机制:当一个 P 的可运行 G 队列为空时,会从其他 P 的队列尾部或全局队列中 "窃取"Goroutine 执行。

    • 作用:平衡各 P 的负载,避免 CPU 资源闲置,提高整体并发效率。

  2. 问题sync/atomic包提供了哪些原子操作?适用于什么场景? 答案

    • 常用操作:AddInt64LoadInt64StoreInt64SwapInt64CompareAndSwapInt64等。

    • 场景:简单的计数器、状态标记等,比互斥锁更高效,但仅适用于单一变量的操作。

    • 注意:原子操作针对的是变量的内存地址,需传递指针。

  3. 问题:什么是 "管道"(Pipeline)模式?如何用 Goroutine 和 Channel 实现? 答案

    • 模式:将任务分解为多个阶段,每个阶段由 Goroutine 处理,阶段间通过 Channel 连接,数据流式传递。

    • 实现:

      1. 每个阶段是一个函数,接收输入 Channel,返回输出 Channel。

      2. 前一阶段的输出作为后一阶段的输入。

      3. 示例:数据生成 → 过滤 → 转换 → 输出。

  4. 问题:如何实现一个并发安全的单例模式(Singleton)? 答案

    • 方法 1:使用

      sync.Once

      确保初始化代码只执行一次。

      go

      var (instance *Singletononce     sync.Once
      )func GetInstance() *Singleton {once.Do(func() {instance = &Singleton{} // 初始化})return instance
      }
    • 方法 2:使用双重检查锁定(配合原子操作),但实现较复杂,推荐使用sync.Once

  5. 问题:Goroutine 的栈是如何动态增长的?与其他语言的协程有何不同? 答案

    • 增长机制:Goroutine 初始栈为 2KB,当栈空间不足时,Go 运行时会分配新的更大栈(通常翻倍),并将旧栈数据复制到新栈。

    • 不同点:多数语言的协程栈大小固定或需预先指定,而 Go 的动态栈可按需增长(上限通常为 1GB),更灵活且内存效率高。

  6. 问题context包中的值传递(WithValue)有何注意事项?为什么不推荐传递大量数据? 答案

    • 注意事项:

      • 键类型应定义为自定义类型(避免命名冲突)。

      • 传递的数据应是请求范围的元数据(如认证信息),而非业务数据。

      • 数据是 immutable(只读)的,避免修改。

    • 不推荐大量数据:context值传递是通过链表查找,效率低;且会导致函数签名污染,不利于代码维护。

  7. 问题:什么是 "非阻塞"Channel 操作?如何实现? 答案

    • 定义:在 Channel 操作(发送 / 接收)无法立即完成时,不阻塞当前 Goroutine,而是执行备选逻辑。

    • 实现:使用

      select

      语句的

      default

      分支:

      go

      // 非阻塞发送
      select {
      case ch <- val:// 发送成功
      default:// 发送失败(缓冲区满)
      }// 非阻塞接收
      select {
      case val := <-ch:// 接收成功
      default:// 接收失败(缓冲区空)
      }
  8. 问题:如何调试 Go 并发程序?有哪些工具和方法? 答案

    • 工具:

      • go test -race:检测竞态条件。

      • pprof:分析 CPU、内存使用和 Goroutine 状态。

      • trace:生成执行轨迹,分析调度、锁竞争等。

      • fmt.Println/ 日志:简单调试,输出 GoroutineID 和状态。

    • 方法:复现问题时缩小范围,使用隔离测试验证并发逻辑,利用工具定位瓶颈。

  9. 问题:Go 1.14 引入的异步抢占式调度解决了什么问题?其原理是什么? 答案

    • 解决问题:之前的协作式调度可能导致长时间运行的 Goroutine 独占 P,其他 Goroutine 饥饿。

    • 原理:通过信号中断长时间运行(超过 10ms)的 Goroutine,将其抢占并放入全局队列,让其他 Goroutine 有机会执行。支持函数调用和循环中抢占。

  10. 问题:并发编程中,如何平衡性能和可读性?有哪些最佳实践? 答案

    • 最佳实践:

      1. 优先使用 Channel 通信,减少共享内存(遵循 CSP 模型)。

      2. 封装并发逻辑,对外暴露简洁接口(隐藏同步细节)。

      3. 控制 Goroutine 数量,避免无限制创建。

      4. 使用context管理生命周期,确保资源可回收。

      5. 编写并发测试,用-race检测问题,性能测试验证优化效果。

二、场景题(20 题)

基础应用(1-5 题)
  1. 场景:使用sync.WaitGroup实现主程序等待 5 个 Goroutine 完成,每个 Goroutine 打印自己的编号。 答案

    go

    package mainimport ("fmt""sync"
    )func main() {var wg sync.WaitGroupwg.Add(5) // 等待5个Goroutinefor i := 0; i < 5; i++ {go func(id int) {defer wg.Done() // 完成时递减计数fmt.Printf("Goroutine %d 完成\n", id)}(i)}wg.Wait() // 等待所有完成fmt.Println("所有Goroutine执行完毕")
    }
  2. 场景:使用无缓冲 Channel 实现两个 Goroutine 间的同步,交替打印数字(1,2,1,2...)。 答案

    go

    package mainimport "fmt"func main() {ch1 := make(chan struct{}) // 用于通知Goroutine1可以打印ch2 := make(chan struct{}) // 用于通知Goroutine2可以打印// Goroutine1:打印1go func() {for i := 0; i < 5; i++ {<-ch1          // 等待通知fmt.Print("1 ")ch2 <- struct{}{} // 通知Goroutine2}}()// Goroutine2:打印2go func() {for i := 0; i < 5; i++ {<-ch2          // 等待通知fmt.Print("2 ")ch1 <- struct{}{} // 通知Goroutine1}}()ch1 <- struct{}{} // 启动第一个打印// 等待最后一次打印完成(简单处理,实际可加WaitGroup)<-ch2fmt.Println("\n完成")
    }
  3. 场景:使用sync.Mutex实现一个并发安全的计数器,支持递增和获取当前值。 答案

    go

    运行

    package mainimport ("fmt""sync"
    )type Counter struct {mu    sync.Mutexvalue int
    }func (c *Counter) Increment() {c.mu.Lock()defer c.mu.Unlock()c.value++
    }func (c *Counter) Value() int {c.mu.Lock()defer c.mu.Unlock()return c.value
    }func main() {var counter Countervar wg sync.WaitGroupwg.Add(1000)// 1000个Goroutine并发递增for i := 0; i < 1000; i++ {go func() {defer wg.Done()counter.Increment()}()}wg.Wait()fmt.Println("最终计数:", counter.Value()) // 预期1000
    }
  4. 场景:使用带缓冲 Channel 限制并发数为 3,同时启动 10 个任务,观察执行顺序。 答案

    go

    package mainimport ("fmt""sync""time"
    )func main() {const concurrency = 3sem := make(chan struct{}, concurrency) // 信号量,限制并发数var wg sync.WaitGroupfor i := 0; i < 10; i++ {wg.Add(1)go func(taskID int) {defer wg.Done()sem <- struct{}{}        // 获取信号量(满则等待)defer func() { <-sem }() // 释放信号量fmt.Printf("任务 %d 开始\n", taskID)time.Sleep(100 * time.Millisecond) // 模拟任务执行fmt.Printf("任务 %d 完成\n", taskID)}(i)}wg.Wait()fmt.Println("所有任务完成")
    }
  5. 场景:使用select语句实现一个超时控制,若 500ms 内未收到 Channel 消息则打印超时。 答案

    go

    package mainimport ("fmt""time"
    )func main() {ch := make(chan string)// 模拟可能超时的操作go func() {// 取消注释以测试超时情况// time.Sleep(600 * time.Millisecond)ch <- "操作完成"}()// 超时控制select {case msg := <-ch:fmt.Println("收到消息:", msg)case <-time.After(500 * time.Millisecond):fmt.Println("操作超时")}
    }
中级应用(6-15 题)
  1. 场景:实现一个工作池(Worker Pool),包含 3 个 Worker,处理 10 个任务并收集结果。 答案

    go

    package mainimport ("fmt""sync"
    )// 任务函数
    func processTask(task int) int {return task * 2 // 简单处理:翻倍
    }func main() {const (numWorkers = 3numTasks   = 10)tasks := make(chan int, numTasks)results := make(chan int, numTasks)var wg sync.WaitGroup// 启动Workerwg.Add(numWorkers)for i := 0; i < numWorkers; i++ {go func(workerID int) {defer wg.Done()for task := range tasks {fmt.Printf("Worker %d 处理任务 %d\n", workerID, task)result := processTask(task)results <- result}}(i)}// 发送任务go func() {for i := 0; i < numTasks; i++ {tasks <- i}close(tasks) // 任务发送完毕,关闭通道}()// 等待所有Worker完成并关闭结果通道go func() {wg.Wait()close(results)}()// 收集结果for result := range results {fmt.Printf("收到结果: %d\n", result)}fmt.Println("所有任务处理完毕")
    }
  2. 场景:使用context.WithCancel实现一个 Goroutine 树的取消,当根上下文取消时,所有子 Goroutine 都退出。 答案

    go

    package mainimport ("context""fmt""time"
    )// 子Goroutine
    func childGoroutine(ctx context.Context, name string) {for {select {case <-ctx.Done():fmt.Printf("%s 收到取消信号,退出\n", name)returncase <-time.After(500 * time.Millisecond):fmt.Printf("%s 正在运行\n", name)}}
    }func main() {// 创建可取消的上下文ctx, cancel := context.WithCancel(context.Background())// 启动子Goroutinego childGoroutine(ctx, "子Goroutine 1")go childGoroutine(ctx, "子Goroutine 2")// 5秒后取消time.Sleep(2 * time.Second)fmt.Println("主程序发起取消")cancel()// 等待子Goroutine退出time.Sleep(1 * time.Second)fmt.Println("主程序退出")
    }
  3. 场景:实现 "扇出 - 扇入" 模式,多个 Goroutine 并发计算切片中元素的平方,然后汇总结果。 答案

    go

    package mainimport ("fmt""sync"
    )// 计算平方的函数,返回结果通道
    func square(nums []int) <-chan int {out := make(chan int)go func() {defer close(out)for _, n := range nums {out <- n * n}}()return out
    }// 扇入:合并多个结果通道
    func merge(cs ...<-chan int) <-chan int {var wg sync.WaitGroupout := make(chan int)// 为每个通道启动一个Goroutinewg.Add(len(cs))for _, c := range cs {go func(ch <-chan int) {defer wg.Done()for n := range ch {out <- n}}(c)}// 所有通道处理完毕后关闭outgo func() {wg.Wait()close(out)}()return out
    }func main() {nums := []int{1, 2, 3, 4, 5, 6, 7, 8}// 扇出:拆分任务到3个Goroutinec1 := square(nums[:3])c2 := square(nums[3:6])c3 := square(nums[6:])// 扇入:合并结果for res := range merge(c1, c2, c3) {fmt.Printf("%d ", res)}// 输出:1 4 9 16 25 36 49 64
    }
  4. 场景:使用sync.RWMutex优化一个高频读、低频写的缓存系统,提高并发读性能。 答案

    go

    package mainimport ("fmt""sync""time"
    )type Cache struct {mu    sync.RWMutexdata  map[string]string
    }func NewCache() *Cache {return &Cache{data: make(map[string]string),}
    }// Get 读取缓存(使用读锁)
    func (c *Cache) Get(key string) (string, bool) {c.mu.RLock()defer c.mu.RUnlock()val, ok := c.data[key]return val, ok
    }// Set 写入缓存(使用写锁)
    func (c *Cache) Set(key, val string) {c.mu.Lock()defer c.mu.Unlock()c.data[key] = val
    }func main() {cache := NewCache()var wg sync.WaitGroup// 模拟100个读操作for i := 0; i < 100; i++ {wg.Add(1)go func(i int) {defer wg.Done()key := fmt.Sprintf("key%d", i%5) // 重复读取5个keyval, ok := cache.Get(key)if ok {fmt.Printf("读取 %s: %s\n", key, val)} else {fmt.Printf("读取 %s: 未找到\n", key)}time.Sleep(10 * time.Millisecond)}(i)}// 模拟2个写操作for i := 0; i < 2; i++ {wg.Add(1)go func(i int) {defer wg.Done()key := fmt.Sprintf("key%d", i)val := fmt.Sprintf("val%d", i)cache.Set(key, val)fmt.Printf("写入 %s: %s\n", key, val)time.Sleep(50 * time.Millisecond)}(i)}wg.Wait()
    }
  5. 场景:使用time.Ticker实现一个每秒执行一次的定时任务,运行 5 次后停止。 答案

    go

    package mainimport ("fmt""time"
    )func main() {ticker := time.NewTicker(1 * time.Second)defer ticker.Stop() // 确保资源释放count := 0done := make(chan struct{})// 定时任务go func() {for {select {case <-ticker.C:count++fmt.Printf("定时任务执行第 %d 次\n", count)if count >= 5 {done <- struct{}{} // 通知完成return}}}}()<-done // 等待任务完成fmt.Println("定时任务已停止")
    }
  6. 场景:使用sync.Cond实现一个生产者 - 消费者模型,当队列满时阻塞生产者,空时阻塞消费者。 答案

    go

    package mainimport ("fmt""sync""time"
    )const maxQueueSize = 5type Queue struct {mu      sync.Mutexcond    *sync.Conditems   []intcount   int
    }func NewQueue() *Queue {q := &Queue{items: make([]int, 0, maxQueueSize),}q.cond = sync.NewCond(&q.mu)return q
    }// 生产者:添加元素
    func (q *Queue) Produce(item int) {q.mu.Lock()defer q.mu.Unlock()// 队列满则等待for len(q.items) == maxQueueSize {q.cond.Wait()}q.items = append(q.items, item)q.count++fmt.Printf("生产: %d, 队列长度: %d\n", item, len(q.items))q.cond.Broadcast() // 通知消费者
    }// 消费者:取出元素
    func (q *Queue) Consume() int {q.mu.Lock()defer q.mu.Unlock()// 队列空则等待for len(q.items) == 0 {q.cond.Wait()}item := q.items[0]q.items = q.items[1:]fmt.Printf("消费: %d, 队列长度: %d\n", item, len(q.items))q.cond.Broadcast() // 通知生产者return item
    }func main() {q := NewQueue()var wg sync.WaitGroup// 2个生产者wg.Add(2)for i := 0; i < 2; i++ {go func(prodID int) {defer wg.Done()for j := 0; j < 5; j++ {item := prodID*10 + jq.Produce(item)time.Sleep(100 * time.Millisecond)}}(i)}// 3个消费者wg.Add(3)for i := 0; i < 3; i++ {go func(consID int) {defer wg.Done()for j := 0; j < 3; j++ {q.Consume()time.Sleep(200 * time.Millisecond)}}(i)}wg.Wait()
    }
  7. 场景:使用context.WithTimeout为一个可能超时的 HTTP 请求设置 3 秒超时,并处理超时错误。 答案

    go

    package mainimport ("context""fmt""net/http""time"
    )func main() {// 创建3秒超时的上下文ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)defer cancel()// 创建请求req, err := http.NewRequestWithContext(ctx, "GET", "https://httpbin.org/delay/2", nil)if err != nil {fmt.Printf("创建请求失败: %v\n", err)return}// 发送请求client := &http.Client{}resp, err := client.Do(req)if err != nil {// 检查是否超时if ctx.Err() == context.DeadlineExceeded {fmt.Println("请求超时")return}fmt.Printf("请求失败: %v\n", err)return}defer resp.Body.Close()fmt.Printf("请求成功,状态码: %d\n", resp.StatusCode)
    }
  8. 场景:实现一个并发安全的 map,支持并发读写,并提供GetSetDelete方法。 答案

    go

    package mainimport ("fmt""sync"
    )// ConcurrentMap 并发安全的map
    type ConcurrentMap struct {mu sync.RWMutexm  map[string]interface{}
    }func NewConcurrentMap() *ConcurrentMap {return &ConcurrentMap{m: make(map[string]interface{}),}
    }// Get 读取值
    func (c *ConcurrentMap) Get(key string) (interface{}, bool) {c.mu.RLock()defer c.mu.RUnlock()val, ok := c.m[key]return val, ok
    }// Set 设置值
    func (c *ConcurrentMap) Set(key string, val interface{}) {c.mu.Lock()defer c.mu.Unlock()c.m[key] = val
    }// Delete 删除值
    func (c *ConcurrentMap) Delete(key string) {c.mu.Lock()defer c.mu.Unlock()delete(c.m, key)
    }func main() {cm := NewConcurrentMap()var wg sync.WaitGroup// 10个Goroutine并发写入for i := 0; i < 10; i++ {wg.Add(1)go func(i int) {defer wg.Done()key := fmt.Sprintf("key%d", i)cm.Set(key, i)fmt.Printf("设置 %s = %d\n", key, i)}(i)}// 10个Goroutine并发读取for i := 0; i < 10; i++ {wg.Add(1)go func(i int) {defer wg.Done()key := fmt.Sprintf("key%d", i%10)val, ok := cm.Get(key)if ok {fmt.Printf("读取 %s = %v\n", key, val)} else {fmt.Printf("读取 %s: 不存在\n", key)}}(i)}wg.Wait()
    }
  9. 场景:使用sync.Once确保配置文件只加载一次,即使被多个 Goroutine 同时调用。 答案

    go

    package mainimport ("fmt""sync"
    )type Config struct {AppName stringPort    int
    }var (config *Configonce   sync.Once
    )// LoadConfig 加载配置(确保只执行一次)
    func LoadConfig() *Config {once.Do(func() {// 模拟加载配置文件的耗时操作fmt.Println("加载配置文件...")config = &Config{AppName: "myapp",Port:    8080,}})return config
    }func main() {var wg sync.WaitGroupwg.Add(5)// 5个Goroutine同时调用LoadConfigfor i := 0; i < 5; i++ {go func(id int) {defer wg.Done()cfg := LoadConfig()fmt.Printf("Goroutine %d 获取配置: %+v\n", id, cfg)}(i)}wg.Wait()
    }
    // 输出:
    // 加载配置文件...
    // Goroutine X 获取配置: &{AppName:myapp Port:8080}
    // (其他Goroutine输出相同,但"加载配置文件..."只出现一次)
  10. 场景:实现一个管道(Pipeline)模式,分三个阶段处理数据:生成数字→过滤偶数→平方→输出结果。 答案

    go

    package mainimport "fmt"// 阶段1:生成数字
    func generate(nums ...int) <-chan int {out := make(chan int)go func() {defer close(out)for _, n := range nums {out <- n}}()return out
    }// 阶段2:过滤偶数
    func filter(in <-chan int) <-chan int {out := make(chan int)go func() {defer close(out)for n := range in {if n%2 == 0 { // 只保留偶数out <- n}}}()return out
    }// 阶段3:计算平方
    func square(in <-chan int) <-chan int {out := make(chan int)go func() {defer close(out)for n := range in {out <- n * n}}()return out
    }func main() {// 构建管道:生成 → 过滤 → 平方nums := generate(1, 2, 3, 4, 5, 6, 7, 8)evens := filter(nums)squares := square(evens)// 输出结果for res := range squares {fmt.Printf("%d ", res) // 输出:4 16 36 64}
    }
高级应用(16-20 题)
  1. 场景:使用errgroup.Group管理多个 Goroutine,一旦有一个 Goroutine 返回错误,立即取消所有其他 Goroutine。 答案

    go

    package mainimport ("context""fmt""golang.org/x/sync/errgroup""time"
    )func task(ctx context.Context, id int, fail bool) error {for {select {case <-ctx.Done():fmt.Printf("任务 %d 被取消: %v\n", id, ctx.Err())return ctx.Err()default:fmt.Printf("任务 %d 运行中\n", id)time.Sleep(500 * time.Millisecond)if fail {return fmt.Errorf("任务 %d 失败", id)}}}
    }func main() {g, ctx := errgroup.WithContext(context.Background())// 添加3个任务,其中第2个会失败g.Go(func() error {return task(ctx, 1, false)})g.Go(func() error {return task(ctx, 2, true) // 这个任务会失败})g.Go(func() error {return task(ctx, 3, false)})// 等待所有任务完成if err := g.Wait(); err != nil {fmt.Printf("执行出错: %v\n", err)} else {fmt.Println("所有任务成功完成")}
    }
  2. 场景:使用sync/atomic包实现一个无锁的并发计数器,比较与Mutex实现的性能差异。 答案

    go

    package mainimport ("fmt""sync""sync/atomic""time"
    )// 原子操作计数器
    type AtomicCounter struct {value int64
    }func (c *AtomicCounter) Increment() {atomic.AddInt64(&c.value, 1)
    }func (c *AtomicCounter) Value() int64 {return atomic.LoadInt64(&c.value)
    }// 互斥锁计数器
    type MutexCounter struct {mu    sync.Mutexvalue int64
    }func (c *MutexCounter) Increment() {c.mu.Lock()c.value++c.mu.Unlock()
    }func (c *MutexCounter) Value() int64 {c.mu.Lock()defer c.mu.Unlock()return c.value
    }// 性能测试函数
    func benchmarkCounter(counter interface{}, name string, wg *sync.WaitGroup, iterations int) {start := time.Now()for i := 0; i < iterations; i++ {wg.Add(1)go func() {defer wg.Done()switch c := counter.(type) {case *AtomicCounter:c.Increment()case *MutexCounter:c.Increment()}}()}wg.Wait()duration := time.Since(start)fmt.Printf("%s: 耗时 %v, 最终值: ", name, duration)switch c := counter.(type) {case *AtomicCounter:fmt.Println(c.Value())case *MutexCounter:fmt.Println(c.Value())}
    }func main() {const iterations = 100000var wg sync.WaitGroup// 测试原子计数器atomicCounter := &AtomicCounter{}benchmarkCounter(atomicCounter, "原子计数器", &wg, iterations)// 测试互斥锁计数器mutexCounter := &MutexCounter{}benchmarkCounter(mutexCounter, "互斥锁计数器", &wg, iterations)// 通常原子计数器性能更好(尤其高并发场景)
    }
  3. 场景:实现一个带有超时和重试机制的函数调用,当函数执行超时或失败时自动重试,最多重试 3 次。 答案

    go

    package mainimport ("context""fmt""time"
    )// 模拟可能失败或超时的函数
    func riskyOperation(ctx context.Context) (string, error) {// 随机模拟失败(50%概率)select {case <-time.After(100 * time.Millisecond):return "操作成功", nilcase <-time.After(200 * time.Millisecond):return "", fmt.Errorf("操作超时")}
    }// 带重试和超时的调用函数
    func withRetryAndTimeout(ctx context.Context, maxRetries int, timeout time.Duration) (string, error) {for i := 0; i < maxRetries; i++ {// 为每次尝试创建超时上下文ctx, cancel := context.WithTimeout(ctx, timeout)defer cancel()result, err := riskyOperation(ctx)if err == nil {return result, nil}// 检查是否是上下文取消或最后一次重试if ctx.Err() == context.Canceled || i == maxRetries-1 {return "", fmt.Errorf("第 %d 次尝试失败: %v", i+1, err)}fmt.Printf("第 %d 次尝试失败,重试...\n", i+1)time.Sleep(500 * time.Millisecond) // 重试间隔}return "", fmt.Errorf("达到最大重试次数")
    }func main() {ctx := context.Background()result, err := withRetryAndTimeout(ctx, 3, 150*time.Millisecond)if err != nil {fmt.Printf("最终失败: %v\n", err)return}fmt.Println("成功:", result)
    }
  4. 场景:使用context传递请求 ID,在多个 Goroutine 处理请求的链路中共享该 ID 用于日志追踪。 答案

    go

    package mainimport ("context""fmt""sync"
    )// 定义请求ID的键类型(避免命名冲突)
    type ctxKey string
    const requestIDKey ctxKey = "requestID"// 从上下文获取请求ID
    func getRequestID(ctx context.Context) string {id, ok := ctx.Value(requestIDKey).(string)if !ok {return "unknown"}return id
    }// 模拟处理步骤1
    func step1(ctx context.Context, wg *sync.WaitGroup) {defer wg.Done()fmt.Printf("[%s] 执行步骤1\n", getRequestID(ctx))// 模拟处理
    }// 模拟处理步骤2
    func step2(ctx context.Context, wg *sync.WaitGroup) {defer wg.Done()fmt.Printf("[%s] 执行步骤2\n", getRequestID(ctx))// 模拟处理
    }// 处理请求的函数
    func handleRequest(requestID string) {// 创建带请求ID的上下文ctx := context.WithValue(context.Background(), requestIDKey, requestID)var wg sync.WaitGroup// 启动多个Goroutine处理wg.Add(2)go step1(ctx, &wg)go step2(ctx, &wg)wg.Wait()fmt.Printf("[%s] 请求处理完成\n", requestID)
    }func main() {// 处理两个不同的请求go handleRequest("req-123")go handleRequest("req-456")// 等待完成time.Sleep(100 * time.Millisecond)
    }
    // 输出示例:
    // [req-123] 执行步骤1
    // [req-456] 执行步骤1
    // [req-123] 执行步骤2
    // [req-456] 执行步骤2
    // [req-123] 请求处理完成
    // [req-456] 请求处理完成
  5. 场景:实现一个并发的文件下载器,使用指定数量的 Goroutine 同时下载多个 URL,并记录每个下载的耗时。 答案

    go

    package mainimport ("fmt""net/http""sync""time"
    )// 下载任务
    type DownloadTask struct {URL    stringResult stringErr    errorTime   time.Duration
    }// 下载函数
    func download(url string) (string, error, time.Duration) {start := time.Now()resp, err := http.Get(url)if err != nil {return "", err, time.Since(start)}defer resp.Body.Close()duration := time.Since(start)return fmt.Sprintf("状态码: %d", resp.StatusCode), nil, duration
    }// 并发下载器
    func downloadConcurrent(urls []string, concurrency int) []DownloadTask {tasks := make(chan string, len(urls))results := make(chan DownloadTask, len(urls))var wg sync.WaitGroup// 填充任务for _, url := range urls {tasks <- url}close(tasks)// 启动下载Goroutinewg.Add(concurrency)for i := 0; i < concurrency; i++ {go func(workerID int) {defer wg.Done()for url := range tasks {result, err, duration := download(url)results <- DownloadTask{URL:    url,Result: result,Err:    err,Time:   duration,}}}(i)}// 等待所有下载完成并关闭结果通道go func() {wg.Wait()close(results)}()// 收集结果var allResults []DownloadTaskfor res := range results {allResults = append(allResults, res)}return allResults
    }func main() {urls := []string{"https://httpbin.org/get","https://httpbin.org/status/200","https://httpbin.org/delay/1","https://httpbin.org/delay/2","https://example.com",}results := downloadConcurrent(urls, 2) // 2个并发下载// 打印结果for _, res := range results {if res.Err != nil {fmt.Printf("下载 %s 失败: %v, 耗时: %v\n", res.URL, res.Err, res.Time)} else {fmt.Printf("下载 %s 成功: %s, 耗时: %v\n", res.URL, res.Result, res.Time)}}
    }

总结

以上题目全面覆盖了 Go 并发编程的核心知识点:

  • 八股文题从基础概念(Goroutine、Channel、WaitGroup)到高级特性(调度原理、原子操作、上下文),解析了 Go 并发模型的设计原理及最佳实践。

  • 场景题结合实际开发场景(工作池、管道模式、超时控制、并发安全数据结构),展示了不同复杂度下的并发编程技巧。

通过练习这些题目,可深入理解 Go 并发的核心思想("通过通信共享内存"),掌握在实际项目中设计高效、安全的并发程序的能力。

http://www.dtcms.com/a/360846.html

相关文章:

  • 20250828的学习笔记
  • Socket-TCP 上位机下位机数据交互框架
  • 深入理解 HTTP 与 HTTPS:区别以及 HTTPS 加密原理
  • UART-TCP双向桥接服务
  • Flutter WebAssembly (Wasm) 支持 - 实用指南Flutter WebAssembly (Wasm) 支持 - 实用指南
  • 解决爬虫IP限制:Selenium隧道代理完整解决方案
  • 聚焦智慧教育新趋势:AI+虚拟仿真技术加速未来学习转型
  • 算法面试题(上)
  • 【Java后端】Spring Boot 全局域名替换
  • Azure AI Search构建RAG的优化点
  • 接口自动化测试之设置断言思路
  • 大模型应用开发面试实录:LLM原理、RAG工程与多Agent场景化落地解析
  • mysql实例是什么?
  • 产品月报|睿本云8月产品功能迭代
  • Topaz Video AI:AI驱动的视频增强与修复工具
  • 嵌入式实时操作系统(二十五)-实时性
  • 从 “能用” 到 “好用”:生成式 AI 落地三大核心痛点与破局路径
  • nt5inf.hash排序后前后两个共五个和nti5nf.cat文件用asn.1editor打开后导出后部分内容的对比--重要
  • Unity中多线程与高并发下的单例模式
  • 结构体成员大小及内存对齐练习
  • Electron使用WebAssembly实现CRC-16 CCITT校验
  • 9.1C++——类中特殊的成员函数
  • 安卓悬浮球-3566-测试报告
  • vue社区网格化管理系统(代码+数据库+LW)
  • Adobe Acrobat打开pdf文件时闪退如何解决?
  • OpenCV-CUDA 图像处理
  • 论文阅读_TradingAgents多智能体金融交易框架
  • .net 微服务jeager链路跟踪
  • C++11 ——— lambda表达式
  • LeetCode 19: 删除链表的倒数第 N 个结点