Go协程的运行机制以及并发模型

进程与线程

进程与线程都是os用来运行程序的基本单元。其中进程是正在执行的程序的实例,它包含了程序代码、数据、文件和系统资源等。进程是os资源分配的基本单元,每个进程都有自己独立的地址空间、文件描述符、网络连接、进程ID等系统资源。进程与进程之间有较好的隔离性,但是进程之间的通信困难,创建一个进程耗时且耗资源,因此多进程并不是高并发场景下的最佳选择。

由于多进程在并发条件下的不足,os抽象出一个轻量级的资源——线程。一个进程可以包含多个线程,这些线程共享进程的资源,包括内存,文件和网络描述符等。同时,每个线程都有独立的栈空间、程序计数器和线程本地存储等资源。线程是os资源调度的基本单位,它比进程更轻量级,可以被更快地创建和销毁,且线程间地切换开销比进程小,因此在多任务处理中,使用线程可以提高程序地并发性和性能。

线程与协程

协程一般被认为是轻量级地线程,os感知不到协程的存在,协程的管理依赖Go语言运行时自身提供的调度器。因此准确的说,Go语言中的协程是从属某一个线程的,只有协程和实际线程绑定,才有执行的机会。
下面从调度方式、上下文切换的速度、调度策略、栈的大小这4个方面分析一下线程和协程的不同之处。

  • 调度方式
    Go语言中的协程从属于某一个线程的,协程与线程是多对多的对应关系。Go语言调度器可以将多个协程调度到同一个线程中执行,一个协程也可以在多个线程中切换。
  • 上下文切换速度
    协程上下文切换的速度要快于线程,因为切换协程不必同时切换开发者态与os内核态,而且在Go语言中,切换协程只需要保留极少的状态和寄存器值,切换线程则会保留额外的寄存器值。在一般情况下,线程切换的速度大约为1~2微秒,Go语言中协程切换的速度比它快数倍,为0.2微秒左右。
  • 调度策略
    线程的调度在多数时间里是抢占式的,os调度器为了均衡每个线程的执行周期,会定时发出中断信号强制切换线程上下文。而Go语言中的协程在一般情况下是协作式调度的,当一个协程处理完自己的任务后,可以主动将执行权限让渡给其他协程。这意味着协程可以更好地在规定时间内完成自己的工作,而不会轻易被抢占。只有当一个协程运行了太长时间时,Go语言调度器才会强制抢占其任务的执行。
  • 栈的大小
    线程的栈的大小一般是在创建时指定的。为了避免出现栈溢出的情况,默认的栈较大(例如2MB),这意味着每创建1000个线程就需要消耗2GB的虚拟内存,大大限制了可以创建的线程的数量,而Go语言中的协程栈默认是2KB,所以在实践中,经常会看到成千上万的协程存在。
    另外,线程的栈在运行时不能更改,但是Go语言中的协程栈在Go运行时的帮助下会动态检测栈的大小,并动态地进行扩容,因此在实践中,我们可以将协程看作轻量的资源。

协程的数据争用

在Go语言中,当两个以上协程同时访问相同的内存空间,并且至少有一个写操作时,可能出现并发安全问题,这种现象也叫做数据争用。而要解决数据争用问题,我们需要一些机制来保证某一时刻只有一个协程主席特定操作,比较传统的方案是锁,包括原子锁、互斥锁与读写锁。

  • 原子锁

    var count int64 =0
    func add(){atomic.AddInt64(&count,1)
    }
    func main(){go add()go add()
    }
    

    sync/atomic包中还有一个重要的功能——CompareAndSwap,它能够对比并替换元素值。在下面这个例子中,atomic.CompareAndSwapInt64会判断flag变量的值是否为0,如果是0,则将flag的值设置为1.这一系列操作都是原子性的,不会发生数据争用,也不会出现内存操作乱序问题。sync/atomic包中的原子操作能够构建起一种自旋锁,只有获取该锁,才能执行区域中的代码。

     var count int64 =0var flag int64 = 0func add(){for{if atomic.CompareAndSwapInt64(&flag,0,1){count++atomic.StoreInt64(&flag,0)return}}}func main(){go add()go add()}
    
  • 互斥锁
    通过原子操作构建起的自旋锁,虽然简单高效却不是万能的。例如,当某一个协程长时间霸占锁时,其他协程仍在继续抢占锁,这会导致CPU资源持续无意义地被浪费。同时,当许多协程同时获取锁时,可能有协程始终抢占不到锁。为了解决这种问题,os的锁接口提供了终止与唤醒的机制,这就避免了频繁自旋造成的浪费。不过,调用os级别的锁会锁住整个线程使之无法运行,另外所得抢占还涉及线程之间的上下文切换。Go语言借助协程实现了一种比传统os级别的锁更加轻量级别的互斥锁。

    var count int64=0
    var m sync.Mutex
    func add(){m.Lock()count++m.Unlock()
    }
    func main(){go add()go add()
    }
    

    这里,sync.Mutex构建起了互斥锁,在同一时刻,只会有一个获取了锁的协程会继续执行任务,其他的协程将陷入等待状态。借助协程的休眠与调度器的调度,这种锁会变得非常轻量。

  • 读写锁
    由于在同一时间内只能有一个协程获取互斥锁并执行操作,因此在多读少写的情况下,如果长时间没有写操作,读取到的会是完全相同的值,使用互斥锁就显得没有必要了,这时使用读写锁更加恰当。
    读写锁通过两种锁来实现,一种为读锁,一种为写锁。当进行读取操作时,需要加读锁,当进行写入操作时,需要加写锁。多个协程可以同时获得读锁并执行,但只能有一个协程获得写锁。如果此时有协程申请了写锁,那么该协程需要等待所有的读锁都被释放才能获取写锁并执行。如果当前的协程申请了读锁时已经存在写锁,那么需要等待写锁被释放再获取读锁并执行。

    type Stat struct{counters map[string]int64mutex sync.RWMutex
    }
    func (s *Stat)getCounter(name string) int64{s.mutex.RLock()defer s.mutex.RUnlock()return s.counters[name]
    }
    func (s *Stat) SetCounter(name string){s.mutex.Lock()defer s.mutex.Unlock()s.counters[name]++
    }
    

Go并发控制库

  • sync.WaitGroup
    sync.WaitGroup能够协调多个协程之间的并发执行,它会等待多个协程执行完毕再继续执行后续代码。先来看下这样一个场景:在加载配置的过程中,我们希望多个协程可以同时加载不同的配置文件,同时希望这些协程都加载完毕程序才提供服务。这时,使用sleep函数进行休眠是一种低效的解决方案,更高效的方案是使用Go语言标准库中的sync.WaitGroup。
    sync.WaitGroup提供了3种方法:Add、Done和Wait。其中Add方法将等待的数量加1,Done方法将等待的数量减1,Wait方法则会陷入等待,直到等待的数量为0。因此,一般在开启协程前调用Add方法;然后开启多个工作协程,在每个协程结束时延迟调用Done方法,将等待的数量减1;在末尾调用Wait方法,该方法会陷入阻塞,等待所有协程执行完毕再继续执行后续代码。

    func worker(id int){//.....
    }
    func main(){var wg sync.WaitGroupfor i :=1;i<=5;i++{wg.Add(1)i :=igo func(){defer wg.Done()worker(i)}()}wg.Wait()
    }
    
  • sync.Once
    sync.Once可以保证某一个过程只执行一次,它在实践中被广泛使用,用于防止内存泄漏、资源重复关闭等异常情况。例如,我们希望再程序启动时仅加载一次配置、初始化一次日志组件。

    var(once sync.Once)
    func DbOnce()(*sql.DB,error){once.Do(func(){db,dbErr })
    }
    
  • sync.Map
    sync.Map是Go语言标准库提供的一种线程安全的map类型。与常规的map类型不同,sync.Map是并发安全的,可以在多个协程之间共享访问。
    sync.Map的使用非常简单,只需要使用sync.Map的内置方法进行读写操作即可。例如,可以使用Load方法读取某个Key对应的Value,使用Store方法存储Key-Value对,使用Delete方法删除指定的Key,等等。具体来说,sync.Map支持以下几个方法。

    • Load(key interface{})(interface{},bool):加载指定Key对应的Value。
    • Store(key, value interface{}):存储Key-Value对。
    • LoadOrStore(key,value interface{})(actual interface{},loaded bool):加载Key对应的Value,如果Key不存在,则存储Key-Value对,并返回(value,false);如果Key已经存在,则返回已经存在的Value,并返回(value,true)。
    • Delete(key interface{}):删除指定的Key-Value对。
    • Range(f func(key,value interface{})bool):遍历sync.Map中的所有Key-Value对,并对每个Key-Value对执行指定的函数f。如果函数f返回false,则Range方法会停止遍历。
    func main(){var m sync.Mapm.Store("foo","bar")m.Store("hello","world")val,ok := m.Load("foo")if ok {fmt.Println(val)//bar}newVal,loaded :=m.LoadOrStore("foo","baz")if loaded{fmt.Println(newVal)//bar}else{fmt.Println("Stored value for key 'foo'")}m.Range(func (key,value interface{})bool{fmt.Println("key: %v,value: %v\n",key,value)return true})
    }

    需要注意的是,由于sync.Map内部实现了一些复杂的算法,因此在性能上可能略逊于普通的map类型。另外,由于sync.Map中的key和value都是interface{}类型,因此在使用时需要进行类型断言。

  • sync.Cond
    sync.Cond是Go语言提供的一种类似条件变量的同步机制,它能够让协程陷入阻塞,直到某个条件发生再继续执行。sync.Cond包含了3个重要的API:Wait()、Signal()和Broadcast()。其中,Wait()表示等待条件的发生,会释放所持有的锁,并使当前协程陷入等待状态;Signal用于唤醒等待队列中的一个协程;而Broadcast会唤醒所有等待的协程。要注意的是,使用Wait之前必须调用Cond.L.Lock进行枷锁,结束后还需要调用Cond.L.UnLock()进行解锁。
    使用sync.Cond的正确方法是:协程A会用for循环判断是否满足条件,如果不满足则陷入休眠状态。协程B会在恰当的时候调用c.Broadcast()唤醒等待的协程。当协程被唤醒后,需要再次检查条件是否满足,如果不满足则需要重新陷入等待。
    使用sync.Cond可以实现某种程度上的解耦:消息的发出者不需要知道具体的判断条件,这样可以增强代码的可维护性和可扩展性。

    //协程A
    c.L.Lock()
    for !condition(){c.Wait()
    }
    ...
    c.L.Unlock()//协程B
    c.Broadcast()

    在实践中,并不经常使用sync.Cond,因为在很多场景下都可以使用更为强大的通道。

Go并发模式

前面讲了很多传统的同步模式,但是在实践中协调协程时,使用最多的还是通道。通道最厉害之处在于,在通道的过程中完成了数据所有权的转移,数据只可能在某一个协程中执行,这在无形中解决了并发安全的问题。

  • ping-pong模式
    收到数据的协程可以在不加锁的情况下对数据进行处理,而不必担心并发冲突。

    func main(){var Ball inttable :=make(chan int)go player(table)go player(table)table<-Balltime.Sleep(1*time.Second)<-table
    }
    func player(table chan int){for{ball :=<-tableball++time.Sleep(1*time.Second)table <- ball}
    }
    
  • fan-in模式
    多个协程把数据写入通道,但只有一个协程等待读取通道数据。

    func search(msg string)chan string{var ch = make(chan string)go func(){var i intfor{ch <- fmt.Sprintf("get %s %d",msg,i)i++time.Sleep(1*time.Second)}}()return ch
    }
    func main(){ch1 := search("jonson")ch2 := search("olaya")for{select{case msg := <-ch1:fmt.Println(msg)case msg := <-ch2:fmt.Println(msg)}}
    }
    
  • fan-out模式
    一个协程完成数据写入,多个协程争夺同一个通道中的数据。fan-out模式通常用来分配任务。例如,程序消费kafka等中间件的数据,多个协程会监听同一个通道中的数据,并在读取到数据后立即进行后续处理,处理完毕再继续读取,循环往复。

    func worker(tasksCh <- chan int,wg *sync.WaitGroup){defer wg.Done()for{task,ok:=<-tasksChif !ok{return}d := time.Duration(task)*time.Millisecondtime.Sleep(d)fmt.Println("processing task",task)}
    }func pool(wg *sync.WaitGroup,workers,tasks int){tasksCh := make(chan int)for i:=0;i<workers;i++{go worker(tasksCh,wg)}for i:=0;i<tasks;i++{tasksCh <- i}close(tasksCh)
    }func main(){var wg sync.WaitGroupwg.Add(36)go pool(&wg,36,50)wg.Wait()
    }
    
  • pipeline模式
    指由通道连接的一系列连续的阶段,以类似流的形式进行计算。每个阶段由一组执行特定任务的协程组成,通过通道获取上游传递过来的值,经过处理后,再将新的值发送给下游。

    func Generate(ch chan<- int){for i:=2;;i++{ch <- i}
    }
    func Filter(in <-chan int,out chan <- int,prime int){for{i := <-inif i%prime!=0{out <- i}}
    }
    func main(){ch := make(chan int)go Generate(ch)for i:=0;i<10000;i++{prime := <-chfmt.Println(prime)ch1 := make(chan int)go Filter(ch,ch1,prime)ch = ch1}
    }
    

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/49326.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【LeetCode:3098. 求出所有子序列的能量和 + 记忆化缓存】

&#x1f680; 算法题 &#x1f680; &#x1f332; 算法刷题专栏 | 面试必备算法 | 面试高频算法 &#x1f340; &#x1f332; 越难的东西,越要努力坚持&#xff0c;因为它具有很高的价值&#xff0c;算法就是这样✨ &#x1f332; 作者简介&#xff1a;硕风和炜&#xff0c;…

【北京迅为】《i.MX8MM嵌入式Linux开发指南》-第三篇 嵌入式Linux驱动开发篇-第四十七章 字符设备和杂项设备总结回顾

i.MX8MM处理器采用了先进的14LPCFinFET工艺&#xff0c;提供更快的速度和更高的电源效率;四核Cortex-A53&#xff0c;单核Cortex-M4&#xff0c;多达五个内核 &#xff0c;主频高达1.8GHz&#xff0c;2G DDR4内存、8G EMMC存储。千兆工业级以太网、MIPI-DSI、USB HOST、WIFI/BT…

connect-multiparty中间件用法以及实例--文件上传中间件(保姆级别教学)

connect-multiparty中间件的用法包括安装和引入、基本设置、路由应用、文件处理以及安全和优化等步骤。 connect-multiparty是一个专为Connect和Express框架设计的文件上传中间件&#xff0c;它基于multiparty库&#xff0c;用于处理多部分表单数据&#xff0c;尤其针对文件上传…

pytorch中的zero_grad()执行时机

在反向传播(backward())前执行即可 zero_grad() 用以清除优化器的梯度对张量执行backward(),以计算累积梯度执行optimizer.step(),优化器使用梯度更新参数当优化器更新完成,梯度即失去意义,即可以清除,为保证下一次梯度开始累积时为0,则在下一次执行反向传播前清除即可

sqlalchemy使用json_unquote函数的mysql like查询

sqlalchemy使用json_unquote函数的mysql like查询 在SQLAlchemy中使用json_unquote函数查询MySQL JSON字段可以通过使用func函数来实现。下面是一个示例,假设有一个名为users的表,其中包含一个名为data的JSON字段,我们想要查询该字段的内容: from sqlalchemy import crea…

Redis核心技术与实战学习笔记

Redis核心技术与实战学习笔记 最近想沉下心来看下redis&#xff0c;买了蒋德钧老师的《Redis 核心技术与实战》,这里记录一些学习笔记 希望能够坚持下去有想一起学习的童鞋&#xff0c;可以点击跳转到文章尾部获取学习资源,仅供学习不要用于任何商业用途!!! redis知识全景图 …

前端JS特效第50集:zyupload图片上传

zyupload图片上传&#xff0c;先来看看效果&#xff1a; 部分核心的代码如下(全部代码在文章末尾)&#xff1a; var operimg_id; var zoom_rate100; var zoom_timeout; function rotateimg(){var smallImg$("#"operimg_id);var numsmallImg.attr(curr_rotate);if(nu…

ESP8266用AT指令实现连接MQTT

1准备工作 硬件&#xff08;ESP8266&#xff09;连接电脑 硬件已经烧入了MQTT透传固件 2实现连接 2-1&#xff08;进入AT模式&#xff09; 打开串口助手发送如下指令 AT 2-2&#xff08;复位&#xff09; ATRST 2-3&#xff08;开启DHCP&#xff0c;自动获取IP&#x…

免费视频批量横版转竖版

简介 视频处理器 v1.3 是一款由是貔貅呀开发的视频编辑和处理工具&#xff0c;提供高效便捷的视频批量横转竖&#xff0c;主要功能&#xff1a; 导入与删除文件&#xff1a;轻松导入多个视频文件&#xff0c;删除不必要的文件。暂停与继续处理&#xff1a;随时暂停和继续处理。…

C# Math.Ceiling方法向上取整和Math.Floor方法向下取整

Math.Ceiling方法向上取整 用于对指定的双精度浮点值进行向上取整。这意味着它会返回大于或等于指定数字的最小整数。如果数字是整数&#xff0c;则Math.Ceiling将返回该整数本身。 double number1 3.13; double number2 5.0; double number3 -2.72;double result1 Math.…

Python学习笔记43:游戏篇之外星人入侵(四)

前言 在前面的文章中&#xff0c;我们已经对项目进行了简单的分析&#xff0c;并且已经编写好了基础的代码&#xff0c;接下来的工作就是进一步的分析游戏的业务功能&#xff0c;在基础代码之上&#xff0c;进行填充。 背景颜色 我们简单的创建窗口以后&#xff0c;除了命名…

php如何处理和表设计,不同商家的多商品订单,如何进行拆单和费用处理?

在处理不同商家的多商品订单时&#xff0c;拆单和费用处理是一个复杂但重要的任务。在PHP中进行订单处理和表设计。 数据库表设计 用户表 (users) idnameemail等等 商家表 (vendors) idnamecontact_info等等 商品表 (products) idnamepricevendor_id (外键&#xff0c;关联商…

设置使用小米google play和APK的下载使用

我们常常遇到从google play无法下载apk文件&#xff0c;被迫从APKcombo和APKpure两个网站下载安装文件&#xff0c;可是安装文件在手机google play服务框架未开启时即使安装好了&#xff0c;也没法用。也需要把google play服务框架安装好&#xff0c;下面分别介绍&#xff1a; …

Mac 中安装内网穿透工具ngrok

ngrok 是什么&#xff1f; Ngrok 是一个网络工具&#xff0c;主要用于在网络中创建从公共互联网到私有或本地网络中运行的web服务的安全隧道。它充当了一个反向代理&#xff0c;允许外部用户通过公共可访问的URL访问位于防火墙或私有网络中的web应用程序或服务。Ngrok 特别适用…

Three.js 官方文档学习笔记

Address&#xff1a;Three.js中文网 (webgl3d.cn) Author&#xff1a;方越 50041588 Date&#xff1a;2024-07-19 第一个3D案例—创建3D场景 创建3D场景对象Scene&#xff1a; const scene new THREE.Scene(); 创建一个长方体几何对象Geometry&#xff1a; const geomet…

实验八: 彩色图像处理

目录 一、实验目的 二、实验原理 1. 常见彩色图像格式 2. 伪彩色图像 3. 彩色图像滤波 三、实验内容 四、源程序和结果 (1) 主程序(matlab (2) 函数FalseRgbTransf (3) 函数hsi2rgb (4) 函数rgb2hsi (5) 函数GrayscaleFilter (6) 函数RgbFilter 五、结果分析 1. …

某数据泄露防护(DLP)系统NetSecConfigAjax接口SQL注入漏洞复现 [附POC]

文章目录 某数据泄露防护(DLP)系统NetSecConfigAjax接口SQL注入漏洞复现 [附POC]0x01 前言0x02 漏洞描述0x03 影响版本0x04 漏洞环境0x05 漏洞复现1.访问漏洞环境2.构造POC3.复现某数据泄露防护(DLP)系统NetSecConfigAjax接口SQL注入漏洞复现 [附POC] 0x01 前言 免责声明:请…

硬盘取证(电子数据取证)

硬盘取证是电子数据取证的一个重要分支&#xff0c;涉及对硬盘驱动器&#xff08;包括传统硬盘HDD、固态硬盘SSD等&#xff09;进行调查&#xff0c;以收集、保存、分析和呈现与法律案件或安全事件有关的电子证据。硬盘取证的目标是确保收集的证据在法庭上具有可接受性和可靠性…

【Emacs有什么优点,用Emacs写程序真的比IDE更方便吗?】

&#x1f3a5;博主&#xff1a;程序员不想YY啊 &#x1f4ab;CSDN优质创作者&#xff0c;CSDN实力新星&#xff0c;CSDN博客专家 &#x1f917;点赞&#x1f388;收藏⭐再看&#x1f4ab;养成习惯 ✨希望本文对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提出…

springcloud接入skywalking作为应用监控

下载安装包 需要下载SkyWalking APM 和 Java Agent 链接: skywalking 安装 下载JDK17&#xff08;可不配置环境变量&#xff09; 目前skywalking 9.0及以上版本基本都不支持JDK8&#xff0c;需要JDK11-21&#xff0c;具体版本要求在官网查看。 我这里使用的是skywalking9.…