🍊Go并发编程
2024-4-11
| 2024-4-17
0  |  阅读时长 0 分钟
type
status
date
slug
summary
tags
category
icon
password
🍊
Go语言并发编程学习

目 录

并发基础

需要并发的原因

  • 并发能更客观地表现问题模型。例如,我们需要灵敏响应的图形用户界面,一方面程序还需要执行大量的运算或者IO密集操作,而我们需要让界面响应与运算同时执行。
  • 并发可以充分利用CPU核心的优势,提高程序的执行效率
  • 并发能够充分利用CPU与其他硬件设备固有的异步性

并发主流实现模型

  • 多进程:多进程是在操作系统层面进行并发的基本模式,同时也是开销最大的模式
    • 好处:简单、进程间互不影响
    • 坏处:系统开销大,因为所有的进程都是由内核管理的
  • 多线程:多线程在大部分操作系统上都属于系统层面的并发模式,也是我们使用最多的最有效的一种模式
    • 它比多进程的开销小很多,但是其开销依旧比较大,且在高并发模式下,效率会有影响。
  • 基于回调的非阻塞/异步IO:使用多线程模式会很快耗尽服务器的内存和CPU资源。而这种模式通过事件驱动的方式使用异步IO,使服务器持续运转,且尽可能地少用线程,降低开销,它目前在Node.js中得到了很好的实践。但是使用这种模式,编程比多线程要复杂,因为它把流程做了分割,对于问题本身的反应不够自然。
  • 协程:协程(Coroutine)本质上是一种用户态线程,不需要操作系统来进行抢占式调度,且在真正的实现中寄存于线程中,因此,系统开销极小,可以有效提高线程的任务并发性,而避免多线程的缺点。使用协程的优点是编程简单,结构清晰;缺点是需要语言的支持,如果不支持,则需要用户在程序中自行实现调度器。

goroutine

操作系统中存在多个抽象概念的执行体,例如操作系统自己掌管的进程、进程内的线程、以及进程内的协程(轻量级线程)。协程最大的优势在于其“轻量级”,可以轻松创建上百万个而不会导致系统资源衰竭。多数语言在语法层面不直接支持协程,而是通过库的方式支持,但是它们支持的功能不完整。Go语言在语言级别就支持轻量级线程—goroutine,并提供完整的功能。
notion image
❓ 为什么循环中使用goroutine调用Add函数没有在终端输出结果:Go语言是从main package并执行main函数开始的(与大多数语言相似)。上面的例子,主函数执行了6次Add函数就返回了,程序立刻退出并且不会等待所有的goroutine执行结束,因此没有输出。(与其他语言并发类似,需要sleep来等待线程完成)

并发单元的通信

并发单元间的通信是所有编程语言并发编程时遇到的最大问题。
Go语言的并发单元间通信主要通过goroutines和channels来实现,这是一种独特的并发模型,它显著地区别于C语言的线程和互斥量模型或Java的线程和锁模型。
Go语言的goroutines比线程更轻量级,可以轻易地创建和管理数以百万计的并发任务。与此同时,channels提供了一种在goroutines之间传递数据和同步执行的机制。通过channels,我们可以避免在并发编程中常见的竞态条件和死锁问题。
另一个显著的优点是Go语言的并发模型简洁易读。"Do not communicate by sharing memory; instead, share memory by communicating."(不要通过共享内存来通信,而应该通过通信来共享内存)这是Go语言并发模型的设计原则,它鼓励我们使用channels来显式地在goroutines之间传递数据,这使得代码更容易理解和维护。
总的来说,Go语言的并发模型在设计上更加注重代码的简洁和清晰,而且倾向于使用语言级别的解决方案来防止并发编程中的常见问题,这使得Go语言在处理并发编程时比C语言和Java等其他语言更有优势。

channel基本用法

channel声明

  • channel声明形式:var channel名 chan channel传递元素的数据类型

channel初始化

  • 使用make关键字声明并初始化一个channel

channel读写

  • channel写入和读取:
    • 向channel写入数据通常会导致程序阻塞,直到有其他goroutine从这个channel中读取数据
    • 如果channel之前没有写入数据,那么从channel中读取数据时也会导致程序阻塞

select用法

  • select用法:与switch的用法比较类似,但是select有比较多的限制,其中最大的限制就是每个case语句必须是一个IO操作。
notion image

channel缓冲机制

  • 缓冲机制:不带缓冲的channel在传递单个数据时可以接受,但是如果需要传递大量数据时就不合适了,因此需要缓冲机制,以为channel带上缓冲,从而达到消息队列的效果。
    • 在调用make()时将缓冲区大小作为第二个参数传入即可,比如上面这个例子就创建了一个大小为1024的int类型channel,即使没有读取方,写入方也可以一直往channel里写入,在缓冲区被填完之前都不会阻塞。
  • 从带缓冲的channel中读取数据与常规非缓冲channel完全一致的方法,但我们也可以使用range关键来实现更为简便的循环读取。

channel超时机制

  • 在并发编程时,最需要处理的就是超时问题,即向channel写数据时发现channel已满,或者从channel试图读取数据时发现channel为空。如果不正确处理这些情况,很可能会导致整个goroutine锁死。超时机制虽然可以解决死锁问题,但是也会带来新的问题。例如,在运行比较快的机器或者高速的网络上运行正常的程序,到了慢速的机器或者网络上运行就会出问题,从而出现结果不一致的现象。
  • Go语言没有提供直接的超时机制,但是可以利用select很方便地解决超时问题。因为select的特点就是只要其中一个case已经完成,程序就会继续执行下去,而不会考虑其他case的情况。
  • 在Go语言中,time.After函数用于在指定的时间间隔后发送一个值到通道,而select语句用于从多个通道中选择一个进行通信。当time.Afterselect搭配使用时,它们提供了一种强大而灵活的方式来处理超时和并发控制。
notion image
notion image
notion image

channel的传递

Go语言中channel是一种原生类型,就如map类型相似,因此channel本身在定义后也可以通过channel来传递。
notion image

单向channel

单向channel只能用于发送数据或接收数据。channel本身必然是同时支持读写的。如果一个channel仅支持读,那么这个channel必然是空的。如果channel只支持写,那么即便写入数据也没有意义,因为读不出来。
单向channel仅仅是对channel的一个限制,在某些地方有着自己独特的用处。比如,在将一个channel传递给一个函数时可以限制该channel为单向,以此来限制该函数对这个channel的操作。(类似于c++中的const指针)
单向channel的声明:var channel变量名 chan (chan) channel传递数据类型
单向channel如何初始化:由于channel是与map类似的原生类型,因此其也支持类型转换。基于类型转换可以实现单向channel的初始化
单向channel的一个函数例子

channel关闭

直接用Go内置的close函数即可
判断channel是否关闭

参考文献


    ☎️
    有关Go语言并发的任何问题,欢迎您在底部评论区留言,一起交流~ 🍊
     
    Go基础语法-IIAuthenticated Private Information Retrieval源码解析
    • Twikoo
    目录