并发编程是当前软件领域中不可忽视的一个关键概念 。随着CPU等硬件的不断发展,我们都渴望让我们的程序运行速度更快、更快 。而Go语言在语言层面天生支持并发,充分利用现代CPU的多核优势 , 这也是Go语言能够广泛流行的一个重要原因 。
在JAVA中 , 要支持高并发有几种方案可供选择 。首先 , 我们可以通过开启多部署节点集群来增加高并发处理能力 , 通过增加机器硬件来实现 。其次,我们可以在单节点上开启多线程来处理请求 。然而,即使在单节点内创建线程也是非常耗费资源的 。因此,通常情况下我们会使用线程池来管理线程的创建和销毁 。然而,有一个公式你可能会很熟悉 , 即核心线程数等于CPU核数的一半加一 。这意味着我们并不是线程创建得越多 , 对于我们的Java程序就越好 。
在我们明确了问题的痛点之后 , 我们可以进一步探究一下Go语言是如何解决这些问题,并且将高并发作为Go语言的一项特色功能 。
goroutine我们在Java中开启线程的方式是直接创建一个Thread对象 。然而,在Go语言中,如果我们想要实现异步处理,我们可以使用"go"关键字来开启一个goroutine协程 。协程的最大优势在于其轻量级,可以轻松创建上百万个协程而不会导致系统资源的耗尽 , 而线程和进程通常最多也不能超过1万个 。举个例子:
go f()// 创建一个新的 goroutine 运行函数f
在Go语言中,我们可以非常简单地使用关键字"go"来开启一个协程,从而实现异步处理函数f 。只需在函数f的调用前面加上"go"关键字,就能使得该函数在一个独立的协程中异步执行 。
不仅可以使用"go"关键字来开启一个协程异步执行具名函数 , 还可以使用"go"关键字来开启一个协程异步执行匿名函数 。
go func(){// ...}()
今天我们的重点不在这里,而是要讨论为什么Go语言适合处理高并发的情况 。我们都知道,操作系统的CPU最小调度单位是线程 , 然而Go语言却使用了协程的概念 。那么问题来了 , Go语言是如何将这些协程交给CPU来处理的呢?如果无法将它们交给CPU处理,那么就算再创建多少协程也无法运行代码 。在这里 , 我们就需要了解一下Go语言的调度器 , 也就是GPM调度模型 。
GPM调度模型可以借鉴一下以下图例 , 总的来说,我们可以像线程池一样,无论创建了多少协程,都需要将它们放入队列中 。然后,剩下的任务就交给调度器来处理 。
文章插图
图片
【你是否想知道如何应对高并发?Go语言为你提供了答案!】其中:
- G:使用关键字"go"加上一个函数调用可以创建一个goroutine(简称G) 。每次调用"go f()"都会创建一个新的G,其中包含要执行的函数f以及相关的上下文信息 。
- 全局队列(Global Queue)是用来存放等待运行的 G(Goroutine)的地方 。
- P 是指 goroutine 执行所需的物理资源,每个 P 最多可以承载 GOMAXPROCS 个 goroutine 的执行 。
- P 的本地队列是类似于全局队列的,它存放了等待运行的G,并且数量限制在256个以内 。每当新建一个G时,优先将其加入到P的本地队列中,如果本地队列已满,则会批量移动部分G到全局队列中 。
- 为了使线程能够执行任务,需要通过获取调度器(P)来获取任务(G) 。线程首先尝试从调度器的本地队列获取任务 , 如果本地队列为空,则线程会尝试从全局队列或其他调度器的本地队列获取任务 。一旦线程获取到任务,就会执行任务,并在任务执行完毕后再次从调度器获取下一个任务,持续重复这个过程 。
channel单纯地将函数并发执行是没有意义的,因为函数与函数之间需要进行数据交换,才能真正体现并发执行函数的意义 。
虽然可以利用共享内存进行数据交换,但是在不同的 goroutine 中使用共享内存容易导致竞态问题的出现 。为了确保数据交换的正确性,许多并发模型都需要通过使用互斥量对内存进行加锁来解决这个问题 。然而 , 这种做法往往会带来性能问题,因为加锁操作会引入额外的开销 。
推荐阅读
- pdf怎么解密你知道吗?
- 中国四大名鸡,你吃过哪几种?
- 如何判断一个人心眼很坏?若有以下十个特征,不值得你深交
- 建议关闭!手机不输密码,也能转走你的钱
- 手机频繁提示升级更新?教你一招,彻底关闭它
- 微信语音听不见了,怎么恢复?我来教你一招轻松找回来
- 如何降低小米手机发热的概率?一文带你了解
- 手机屏幕太大单手不好操作怎么办?教你一键改成小屏模式,真实用
- 手机为何“偷窥”你的浏览行为?原来是这些开关没关闭
- 迷你世界如何寻找天然矿洞,迷你世界冒险模式怎么寻找精铁矿石