说一说协程

首先,我们了解一下进程,线程和协程三个概念之间的区别

进程,线程,协程区别

进程 拥有自己独立的堆和栈,既不共享堆,亦不共享栈,进程由操作系统调度。
线程 拥有自己独立的栈和共享的堆,共享堆,不共享栈,线程亦由操作系统调度。
协程 和线程一样共享堆,不共享栈,协程由程序员在协程的代码里显示调度。

进程,线程,协程

协程的优势

实现协程的语言,主要以python和go语言为主,当然java也有协程的第三方库,但生产环境使用的不多,典型的协程实现还是以go语言为代表的,下面我们以go语言来说明协程的优势

  • 内存消耗:每个 goroutine (协程) 默认占用内存远比 Java 、C 的线程少。
    我们知道,线程是有固定的栈的,基本都是2MB,当然,不同系统可能大小不太一样,但是的确都是固定分配的。这个栈用于保存局部变量,用于在函数切换时使用。但是对于goroutine这种轻量级的协程来说,一个大小固定的栈可能会导致资源浪费:比如一个协程里面只print了一个语句,那么栈基本没怎么用;当然,也有可能嵌套调用很深,那么可能也不够用。
    所以go采用了动态扩张收缩的策略:初始化为2KB,最大可扩张到1GB。

  • 切换调度开销方面,goroutine 远比线程小
    协程和线程的区别在于:线程切换需要陷入内核,然后进行上下文切换,而协程在用户态由协程调度器完成,不需要陷入内核,这代价就小了;另外,协程的切换时间点是由调度器决定的,而不是系统内核决定的。

多线程编程的槽点

线程,是操作系统的内核对象,多线程编程时,如果线程数过多,就会导致频繁的上下文切换,这些 cpu 时间是一个额外的耗费。所以在一些高并发的网络服务器编程中,使用一个线程服务一个 socket 连接是很不明智的。于是操作系统提供了基于事件模式的异步编程模型。用少量的线程来服务大量的网络连接和I/O操作。但是采用异步和基于事件的编程模型,复杂化了程序代码的编写,非常容易出错。因为线程穿插,也提高排查错误的难度。

协程,是在应用层模拟的线程,他避免了上下文切换的额外耗费,兼顾了多线程的优点。简化了高并发程序的复杂度。举个例子,一个高并发的网络服务器,每一个socket连接进来,服务器用一个协程来对他进行服务。代码非常清晰。而且兼顾了性能。

协程底层实现原理

协程(Coroutine)是在1963年由Melvin E. Conway USAF, Bedford, MA等人提出的一个概念,而且协程的概念是早于线程(Thread)提出的,它是一种非抢占式的线程调度。【参考 线程的调度 · 协同式调度

协程和线程的原理是一样的,当 a线程 切换到 b线程 的时候,需要将 a线程 的相关执行进度压入栈,然后将 b线程 的执行进度出栈,进入 b线程 的执行序列。协程只不过是在 应用层 实现这一点。但是,协程并不是由操作系统调度的,而且应用程序也没有能力和权限执行 cpu 调度。怎么解决这个问题?

答案是,协程是基于线程的。内部实现上,维护了一组数据结构和 n 个线程,真正的执行还是线程,协程执行的代码被扔进一个待执行队列中,由这 n 个线程从队列中拉出来执行。这就解决了协程的执行问题。那么协程是又是怎么切换的呢?

golang 对各种 io函数 进行了封装,这些封装的函数提供给应用程序使用,而其内部调用了操作系统的异步 io函数,当这些异步函数返回 busy 或 bloking 时,golang 利用这个时机将现有的执行序列压栈,让线程去拉另外一个协程的代码来执行,基本原理就是这样,利用并封装了操作系统的异步函数。包括 linux 的 epoll、select 和 windows 的 iocp、event 等。

尽管,在任务调度上,协程是弱于线程的。但是在资源消耗上,协程则是极低的。一个线程的内存在 MB 级别,而协程只需要 KB 级别。而且线程的调度需要内核态与用户的频繁切入切出,资源消耗也不小。

至此,我们把协程的基本特点归纳为:

  1. 协程调度机制无法实现公平调度
  2. 协程的资源开销是非常低的,一台普通的服务器就可以支持百万协程

Golang 协程的应用

我们知道,协程(coroutine)是Go语言中的轻量级线程实现,由Go运行时(runtime)管理。

go 关键字

go 关键字用来创建 goroutine (协程),是实现并发的关键。go 关键字的用法如下:

//go 关键字放在方法调用前新建一个 goroutine 并让他执行方法体
go GetThingDone(param1, param2);

//上例的变种,新建一个匿名方法并执行
go func(param1, param2) {
}(val1, val2)

//直接新建一个 goroutine 并在 goroutine 中执行代码块
go {
    //do someting...
}

在一个函数调用前加上go关键字,这次调用就会在一个新的goroutine中并发执行。当被调用的函数返回时,这个goroutine也自动结束。需要注意的是,如果这个函数有返回值,那么这个返回值会被丢弃。

先看一下下面的程序代码:

func Add(x, y int) {
    z := x + y
    fmt.Println(z)
}
 
func main() {
    for i:=0; i<10; i++ {
        go Add(i, i)
    }
}

执行上面的代码,会发现屏幕什么也没打印出来,程序就退出了。

对于上面的例子,main()函数启动了10个goroutine,然后返回,这时程序就退出了,而被启动的执行 Add() 的 goroutine 没来得及执行。我们想要让 main() 函数等待所有 goroutine 退出后再返回,但如何知道 goroutine 都退出了呢?这就引出了多个goroutine之间通信的问题。

在工程上,有两种最常见的并发通信模型:共享内存消息传递【参考:并发编程模型的分类 】;Go 语言主要使用消息机制 channel 来作为通信模型

channel

消息机制认为每个并发单元是自包含的、独立的个体,并且都有自己的变量,但在不同并发单元间这些变量不共享。每个并发单元的输入和输出只有一种,那就是消息。

channel 是 Go 语言在语言级别提供的 goroutine 间的通信方式,我们可以使用 channel 在多个 goroutine 之间传递消息。channel是进程内的通信方式,因此通过 channel 传递对象的过程和调用函数时的参数传递行为比较一致,比如也可以传递指针等。channel 是类型相关的,一个 channel 只能传递一种类型的值,这个类型需要在声明 channel 时指定。


channel

CSP模型

要想理解 channel 要先知道 CSP 模型。CSP 是 Communicating Sequential Process 的简称,中文可以叫做通信顺序进程,是一种并发编程模型,由 Tony Hoare 于 1977 年提出。简单来说,CSP 模型由并发执行的实体(线程或者进程)所组成,实体之间通过发送消息进行通信,这里发送消息时使用的就是通道,或者叫 channel。CSP 模型的关键是关注 channel,而不关注发送消息的实体。Go 语言实现了 CSP 部分理论,goroutine 对应 CSP 中并发执行的实体,channel 也就对应着 CSP 中的 channel。

channel典型用法

  • channel的声明形式为:
    var chanName chan ElementType

  • 声明一个传递int类型的channel:
    var ch chan int

  • 使用内置函数 make() 定义一个channel:
    ch := make(chan int)

  • 在channel的用法中,最常见的包括写入和读出:

// 将一个数据value写入至channel,这会导致阻塞,直到有其他goroutine从这个channel中读取数据
ch <- value

// 从channel中读取数据,如果channel之前没有写入数据,也会导致阻塞,直到channel中被写入数据为止
value := <-ch

默认情况下,channel的接收和发送都是阻塞的,除非另一端已准备好。

  • 我们还可以创建一个带缓冲的channel:
ch := make(chan int, 1024)

// 从带缓冲的channel中读数据
for i:=range ch {
  ...
}

此时,创建一个大小为1024的int类型的channel,即使没有读取方,写入方也可以一直往channel里写入,在缓冲区被填完之前都不会阻塞。

无缓冲channel
有缓冲channel
  • 可以关闭不再使用的channel:
    close(ch)
    应该在生产者的地方关闭channel,如果在消费者的地方关闭,容易引起panic;

一个非阻塞简单示例

阻塞的意思是调用方在被调用的代码返回之前必须一直等待,不能处理别的事情。而非阻塞调用则不用等待,调用之后立刻返回。那么返回值如何获取呢?
Node.js 使用的是回调的方式,Golang 使用的是 channel。

/**
 * 每次调用方法会新建一个 channel : resultChan,
 * 同时新建一个 goroutine 来发起 http 请求并获取结果。
 * 获取到结果之后 goroutine 会将结果写入到 resultChan。
 */
func UnblockGet(requestUrl string) chan string {
    resultChan := make(chan string)
    go func() {
        request := httplib.Get(requestUrl)
        content, err := request.String()
        if err != nil {
            content = "" + err.Error()
        }
        resultChan <- content
    } ()
    return resultChan
}



fmt.Println(time.Now())
resultChan1 := UnblockGet("http://127.0.0.1/test.php?i=1")
resultChan2 := UnblockGet("http://127.0.0.1/test.php?i=2")

fmt.Println(<-resultChan1)
fmt.Println(<-resultChan1)
fmt.Println(time.Now())

上面两个 http 请求是在两个 goroutine 中并行的。总的执行时间小于 两个请求时间和。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,214评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,307评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,543评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,221评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,224评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,007评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,313评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,956评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,441评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,925评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,018评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,685评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,234评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,240评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,464评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,467评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,762评论 2 345

推荐阅读更多精彩内容