协程(用户态线程)
文章目录
- 协程(用户态线程)
-
- 协程
- 对比线程
- 多线程一定快吗?
- 并发和上下文切换
- 协程的艺术
- 演示
- 总结
协程
首先什么是协程?有人认为进程下有线程,线程管理着协程.其实这并不对
协程是一种用户态线程.它比线程更加轻量并且协程对于操作系统是并不可见的.
也就是说操作系统看不见协程
同一时刻一个CPU只会执行一个协程.
比如交给协程去执行的代码你可以理解为一个个提交的任务
对比线程
那什么是线程?
线程是进程的一个实体,是CPU调度和分派的基本单位.线程是程序中一个单一的顺序控制流程。进程内有一个相对独立的、可调度的执行单元,是系统独立调度和分派CPU的基本单位指令运行时的程序的调度单位。
创建一个线程每个线程都有自己的TCB都有自己的堆栈.创建一个线程是有很大的花销的
也就是说一台服务器的资源总是有限的,你不可能无休止的去开线程(总有一天你会把内存占完)
默认创建一个线程的大小是1MB(取决于实现)也就是说4G的内存 最多只能创建4096个
但是很多时候我们创建的一个线程用不了这么多内存空间
线程的平均内存空间利用率是很低的
那可能有很多人写过这样的一个程序. 是一个基于Socket聊天的程序.大部分人是这样做的来一个Socket以后为其开一个线程工作.这样小程序的时候貌似没有什么问题
但是只要程序一大可能计算机资源就被分配光了. 更好的做法应该是像Reactor模式一样(有兴趣的可以去看一下).使用IO多路复用.反应器模式的效率会高很多
多线程一定快吗?
有的人一说到多线程就觉得多线程开越多程序用行越好.其实并不是的
多线程的创建和销毁都有一定的开销(更好的方式是线程池和任务队列)
有时候多线程并不如串行化快(一会说).
确实有些场景下多线程会比单线程快 比如:返回一个页面,这个页面可能是图文并茂.你经常看到的一个场景是文字先全部出来然后是图片加载出来.因为文字是很快的,而有时候图片是很大的.如果按顺序加载可能会造成一些不太友好的界面
再比如你使用归并排序或者Dijkstra的求最短路径算法.归并排序和Dijkstra是可以分开成一个个子任务的,每一个线程去执行一个个子任务彼此都是分开的不会产生共享不会产生并发的问题.这样的问题 如果你有很多的数据的话那么使用多线程的效率可能会高一些,但是数据比较小往往来说得到的效果就会很差
而且你有时候可能想尝试使用多线程去优化你的程序,你会发现还不如不使用呢
并发和上下文切换
现代OS可以并发和上下文切换密不可分
在操作系统中,CPU切换到另一个进程需要保存当前进程的状态并恢复另一个进程的状态:当前运行任务转为就绪(或者挂起、删除)状态,另一个被选定的就绪任务成为当前任务。上下文切换包括保存当前任务的运行环境,恢复将要运行任务的运行环境。
也就是说当你cpu分配给每个线程一些时间,当时间片使用完了就会切换到其他线程这一点也是会使得你多线程效率差的原因
当你进程特别多的时候频繁的上下文切换会浪费大量的时间.
协程的艺术
上文说到了协程是一种更轻量级的线程.为什么是更轻量级的? 因为协程创建的代价很小 线程比协程的创建需要的资源大了成百上千倍. 那这是怎么做到的呢? 协程只需要很小很小的空间. 其他的东西都是多个协程共享的.
这个时候可能会有人问了 共享的不就会出现并发问题了吗?不是的协程对cpu并不可见. 也就是cpu是看不见协程的
同一时刻一个cpu只会运行一个协程任务那么这样是不是就保证了安全性?
那为了更好的明白同一时刻cpu只执行一个协程 cpu对协程不可见的会用代码演示一下
演示
那么这里采用Golang语言演示.看不懂也没关系 你只需要明白输出,为什么这样输出就可以
那么我在这里说一下Golang语言的好处, Golang语言是最契合现代操作系统的语言.它真的非常棒.简单 易上手 高效 工具强大背景强大绝对是它的优势. 而且它那极具艺术的并发编程和接口才是最吸引我的. 可能就是因为Golang的并发使得它真的是一门非常适合网络编程的语言
下面这段代码很简单
主函数在循环打印0-1000 那么 worker函数在循环打印0-1000 后面还会带个abc
package main
import (
"fmt"
)
func main() {
str := "abc"
//创建一个管道可以使worker函数比main函数晚结束
//在java中main函数会等待其他线程结束才会结束 在Golang中不会
ch := make(chan int)
//go 是go语言的一个关键字,是使用一个协程执行worker函数
go worker(ch, str)
for i := 0; i < 1000; i++ {
fmt.Println(i)
}
//这里会阻塞等待有人往管道里写数据
<- ch
}
func worker(channel chan int, str string) {
for i := 0; i < 1000; i++ {
fmt.Println(i,str)
}
channel <- 1
}
下面是我的一个执行结果片段
可以看到这是并发执行的
那么有人可能问这和多线程有什么区别
你别急因为这里有多核CPU的存在 可能4核CPU4个协程就可以并发的工作
接下来加一行代码 把CPU的核数改成1
接下来是结果的一个执行片段
我来解释一下 这个时候只会有一个任务在执行 可以先到main这个任务先执行到 <-ch 这个管道阻塞等待别人写数据
这个时候就可以切换到另外一个协程执行完毕后程序才退出
如果下面这个程序 更改worker函数那么程序永远卡死
程序永远死循环
下面程序将CPU的核数改成2
那么下面部分输出结果
并且程序正常退出了
如果将main函数的最后一行注释掉 并且CPU的核数改为1 那么worker函数并没有进入
这就是cpu同一时刻只会执行一个协程
总结
那么为什么会有协程?
协程更加轻量(所需要的内存空间更小)
协程是用户态的 这也就是它为什么叫用户态的线程.内核态并不知道它的存在,也就是说没有上下文切换
使得协程变得效率更高