前端角度实现网站首页加载慢优化,河北建设执业信息网站,手机百度网站证书过期,wordpress 查看图片目录 一 协程的概述1.1 并行与并发1.2 线程1.3 新的思路1.4 Goroutine 二 第一个入门程序 一 协程的概述
我查看了网上的一些协程的资料#xff0c;发现每个人对协程的概念都不一样#xff0c;但是我认可的一种说法是#xff1a;协程就是一种轻量级的线程框架#xff08;K… 目录 一 协程的概述1.1 并行与并发1.2 线程1.3 新的思路1.4 Goroutine 二 第一个入门程序 一 协程的概述
我查看了网上的一些协程的资料发现每个人对协程的概念都不一样但是我认可的一种说法是协程就是一种轻量级的线程框架Kotlin在我之前学到Akka框架都是为了解决线程在高并发下能力不足的问题这里参考了一下大神的文章《并发之痛 ThreadGoroutineActor》也许你会有更深的理解。 文章地址并发之痛 ThreadGoroutineActor
1.1 并行与并发 并发concurrency 并发的关注点在于任务切分。举例来说你是一个创业公司的CEO开始只有你一个人你一人分饰多角一会做产品规划一会写代码一会见客户虽然你不能见客户的同时写代码但由于你切分了任务分配了时间片表现出来好像是多个任务一起在执行。并行parallelism 并行的关注点在于同时执行。还是上面的例子你发现你自己太忙了时间分配不过来于是请了工程师产品经理市场总监各司一职这时候多个任务可以同时执行了。所以总结下并发并不要求必须并行可以用时间片切分的方式模拟比如单核cpu上的多任务系统并发的要求是任务能切分成独立执行的片段。而并行关注的是同时执行必须是多核cpu要能并行的程序必须是支持并发的。本文大多数情况下不会严格区分这两个概念默认并发就是指并行机制下的并发。
1.2 线程
开始我们的程序是面向过程的数据结构func。后来有了面向对象对象组合了数结构和func我们想用模拟现实世界的方式抽象出对象有状态和行为。但无论是面向过程的func还是面向对象的func本质上都是代码块的组织单元本身并没有包含代码块的并发策略的定义。于是为了解决并发的需求引入了Thread线程的概念。 线程Thread
系统内核态更轻量的进程由系统内核进行调度同一进程的多个线程可共享资源
线程的出现解决了两个问题一个是GUI出现后急切需要并发机制来保证用户界面的响应。第二是互联网发展后带来的多用户问题。最早的CGI程序很简单将通过脚本将原来单机版的程序包装在一个进程里来一个用户就启动一个进程。但明显这样承载不了多少用户并且如果进程间需要共享资源还得通过进程间的通信机制线程的出现缓解了这个问题。线程的使用比较简单如果你觉得这块代码需要并发就把它放在单独的线程里执行由系统负责调度具体什么时候使用线程要用多少个线程由调用方决定但定义方并不清楚调用方会如何使用自己的代码很多并发问题都是因为误用导致的比如Go中的map以及Java的HashMap都不是并发安全的误用在多线程环境就会导致问题。另外也带来复杂度
竞态条件race conditions 如果每个任务都是独立的不需要共享任何资源那线程也就非常简单。但世界往往是复杂的总有一些资源需要共享比如前面的例子开发人员和市场人员同时需要和CEO商量一个方案这时候CEO就成了竞态条件。依赖关系以及执行顺序 如果线程之间的任务有依赖关系需要等待以及通知机制来进行协调。比如前面的例子如果产品和CEO讨论的方案依赖于市场和CEO讨论的方案这时候就需要协调机制保证顺序。
为了解决上述问题我们引入了许多复杂机制来保证
Mutex(Lock) Go里的sync包, Java的concurrent包通过互斥量来保护数据但有了锁明显就降低了并发度。semaphore 通过信号量来控制并发度或者作为线程间信号signal通知。volatile Java专门引入了volatile关键词来来降低只读情况下的锁的使用。compare-and-swap 通过硬件提供的CAS机制保证原子性atomic也是降低锁的成本的机制。
如果说上面两个问题只是增加了复杂度我们通过深入学习严谨的CodeReview全面的并发测试比如Go语言中单元测试的时候加上-race参数一定程度上能解决当然这个也是有争议的有论文认为当前的大多数并发程序没出问题只是并发度不够如果CPU核数继续增加程序运行的时间更长很难保证不出问题。但最让人头痛的还是下面这个问题 系统里到底需要多少线程 这个问题我们先从硬件资源入手考虑下线程的成本
内存线程的栈空间 每个线程都需要一个栈Stack空间来保存挂起suspending时的状态。Java的栈空间64位VM默认是1024k不算别的内存只是栈空间启动1024个线程就要1G内存。虽然可以用-Xss参数控制但由于线程是本质上也是进程系统假定是要长期运行的栈空间太小会导致稍复杂的递归调用比如复杂点的正则表达式匹配导致栈溢出。所以调整参数治标不治本。调度成本context-switch 我在个人电脑上做的一个非严格测试模拟两个线程互相唤醒轮流挂起线程切换成本大约6000纳秒/次。这个还没考虑栈空间大小的影响。国外一篇论文专门分析线程切换的成本基本上得出的结论是切换成本和栈空间使用大小直接相关。CPU使用率 我们搞并发最主要的一个目标就是我们有了多核想提高CPU利用率最大限度的压榨硬件资源从这个角度考虑我们应该用多少线程呢这个我们可以通过一个公式计算出来100/(155)*420用20个线程最合适。但一方面网络的时间不是固定的另外一方面如果考虑到其他瓶颈资源呢比如锁比如数据库连接池就会更复杂。
作为一个1岁多孩子的父亲认为这个问题的难度好比你要写个给孩子喂饭的程序需要考虑『给孩子喂多少饭合适』这个问题有以下回答以及策略
孩子不吃了就好了但孩子贪玩不吃了可能是想去玩了孩子吃饱了就好了废话你怎么知道孩子吃饱了孩子又不会说话逐渐增量长期观察然后计算一个平均值这可能是我们调整线程常用的策略但增量增加到多少合适呢孩子吃吐了就别喂了如果用逐渐增量的模式通过外部观察可能会到达这个边界条件。系统性能如果因为线程的增加倒退了就别增加线程了没控制好边界把孩子给给撑坏了 这熊爸爸也太恐怖了。但调整线程的时候往往不小心可能就把系统搞挂了
通过这个例子我们可以看出从外部系统来观察或者以经验的方式进行计算都是非常困难的。于是结论是 让孩子会说话吃饱了自己说自己学会吃饭自管理是最佳方案。 然并卵计算机不会自己说话如何自管理 但我们从以上的讨论可以得出一个结论
线程的成本较高内存调度不可能大规模创建应该由语言或者框架动态解决这个问题
线程池方案 Java1.5后Doug Lea的Executor系列被包含在默认的JDK内是典型的线程池方案。 线程池一定程度上控制了线程的数量实现了线程复用降低了线程的使用成本。但还是没有解决数量的问题线程池初始化的时候还是要设置一个最小和最大线程数以及任务队列的长度自管理只是在设定范围内的动态调整。另外不同的任务可能有不同的并发需求为了避免互相影响可能需要多个线程池最后导致的结果就是Java的系统里充斥了大量的线程池。
1.3 新的思路
从前面的分析我们可以看出如果线程是一直处于运行状态我们只需设置和CPU核数相等的线程数即可这样就可以最大化的利用CPU并且降低切换成本以及内存使用。但如何做到这一点呢 陈力就列不能者止 这句话是说能干活的代码片段就放在线程里如果干不了活需要等待被阻塞等就摘下来。通俗的说就是不要占着茅坑不拉屎如果拉不出来需要酝酿下先把茅坑让出来因为茅坑是稀缺资源。 要做到这点一般有两种方案
异步回调方案 典型如NodeJS遇到阻塞的情况比如网络调用则注册一个回调方法其实还包括了一些上下文数据对象给IO调度器linux下是libev调度器在另外的线程里当前线程就被释放了去干别的事情了。等数据准备好调度器会将结果传递给回调方法然后执行执行其实不在原来发起请求的线程里了但对用户来说无感知。但这种方式的问题就是很容易遇到callback hell因为所有的阻塞操作都必须异步否则系统就卡死了。还有就是异步的方式有点违反人类思维习惯人类还是习惯同步的方式。GreenThread/Coroutine/Fiber方案 这种方案其实和上面的方案本质上区别不大关键在于回调上下文的保存以及执行机制。为了解决回调方法带来的难题这种方案的思路是写代码的时候还是按顺序写但遇到IO等阻塞调用时将当前的代码片段暂停保存上下文让出当前线程。等IO事件回来然后再找个线程让当前代码片段恢复上下文继续执行写代码的时候感觉好像是同步的仿佛在同一个线程完成的但实际上系统可能切换了线程但对程序无感。
GreenThread
用户空间 首先是在用户空间避免内核态和用户态的切换导致的成本。由语言或者框架层调度更小的栈空间允许创建大量实例百万级别
几个概念
Continuation 这个概念不熟悉FP编程的人可能不太熟悉不过这里可以简单的顾名思义可以理解为让我们的程序可以暂停然后下次调用继续contine从上次暂停的地方开始的一种机制。相当于程序调用多了一种入口。Coroutine 是Continuation的一种实现一般表现为语言层面的组件或者类库。主要提供yieldresume机制。Fiber 和Coroutine其实是一体两面的主要是从系统层面描述可以理解成Coroutine运行之后的东西就是Fiber。
1.4 Goroutine Goroutine其实就是前面GreenThread系列解决方案的一种演进和实现。
首先它内置了Coroutine机制。因为要用户态的调度必须有可以让代码片段可以暂停/继续的机制。其次它内置了一个调度器实现了Coroutine的多线程并行调度同时通过对网络等库的封装对用户屏蔽了调度细节。最后提供了Channel机制用于Goroutine之间通信实现CSP并发模型Communicating Sequential Processes。因为Go的Channel是通过语法关键词提供的对用户屏蔽了许多细节。其实Go的Channel和Java中的SynchronousQueue是一样的机制如果有buffer其实就是ArrayBlockQueue。
Goroutine调度器 这个图一般讲Goroutine调度器的地方都会引用想要仔细了解的可以看看原博客。这里只说明几点
M代表系统线程P代表处理器核G代表Goroutine。Go实现了M:N的调度也就是说线程和Goroutine之间是多对多的关系。这点在许多GreenThread/Coroutine的调度器并没有实现。比如Java1.1版本之前的线程其实是GreenThread这个词就来源于Java但由于没实现多对多的调度也就是没有真正实现并行发挥不了多核的优势所以后来改成基于系统内核的Thread实现了。某个系统线程如果被阻塞排列在该线程上的Goroutine会被迁移。当然还有其他机制比如M空闲了如果全局队列没有任务可能会从其他M偷任务执行相当于一种rebalance机制。这里不再细说有需要看专门的分析文章。具体的实现策略和我们前面分析的机制类似。系统启动时会启动一个独立的后台线程不在Goroutine的调度线程池里启动netpoll的轮询。当有Goroutine发起网络请求时网络库会将fd文件描述符和pollDesc用于描述netpoll的结构体包含因为读/写这个fd而阻塞的Goroutine关联起来然后调用runtime.gopark方法挂起当前的Goroutine。当后台的netpoll轮询获取到epolllinux环境下的event会将event中的pollDesc取出来找到关联的阻塞Goroutine并进行恢复。
Goroutine是银弹么 Goroutine很大程度上降低了并发的开发成本是不是我们所有需要并发的地方直接go func就搞定了呢 Go通过Goroutine的调度解决了CPU利用率的问题。但遇到其他的瓶颈资源如何处理比如带锁的共享资源比如数据库连接等。互联网在线应用场景下如果每个请求都扔到一个Goroutine里当资源出现瓶颈的时候会导致大量的Goroutine阻塞最后用户请求超时。这时候就需要用Goroutine池来进行控流同时问题又来了池子里设置多少个Goroutine合适
二 第一个入门程序
https://github.com/Kotlin/kotlinx.coroutines
maven
dependencygroupIdorg.jetbrains.kotlinx/groupIdartifactIdkotlinx-coroutines-core/artifactIdversion1.7.3/version
/dependency测试案例
/*** description:* author: shu* createDate: 2023/8/10 20:32* version: 1.0*/
import kotlinx.coroutines.*OptIn(DelicateCoroutinesApi::class)
fun main() {GlobalScope.launch {// 在后台启动一个新协程并继续执行之后的代码delay(1000L)// 非阻塞式地延迟一秒println(World!)// 延迟结束后打印}println(Hello,)//主线程继续执行不受协程 delay 所影响Thread.sleep(2000L)
// 主线程阻塞式睡眠2秒以此来保证JVM存活
} 协程在 CoroutineScope 协程作用域的上下文中通过 launch、async 等协程构造器coroutine builder来启动哈哈第一个入门程序我们就写完了看起来还是特别简单这里推荐一个视频来了解携程
https://www.bilibili.com/video/BV1KJ41137E9凯哥