协程_协程详解 - CSDN
协程 订阅
协程与子例程一样,协程(coroutine)也是一种程序组件。相对子例程而言,协程更为一般和灵活,但在实践中使用没有子例程那样广泛。协程源自 Simula 和 Modula-2 语言,但也有其他语言支持。 展开全文
协程与子例程一样,协程(coroutine)也是一种程序组件。相对子例程而言,协程更为一般和灵活,但在实践中使用没有子例程那样广泛。协程源自 Simula 和 Modula-2 语言,但也有其他语言支持。
信息
外文名
coroutine
类    型
程序组件
中文名
协程
相似程序
子例程
协程简介
协程更适合于用来实现彼此熟悉的程序组件,如合作式多任务,迭代器,无限列表和管道。 协程最初在1963年被提出。
收起全文
精华内容
参与话题
  • 什么是协程

    千次阅读 2019-03-01 15:50:27
    一、什么是协程? 对操作系统来说: 1)线程是最小的【执行单元】 2)进程是最小的【资源管理单元】 二、Java中线程具有5种状态 1、初始化 2、可运行 3、运行中 4、阻塞 ...

    一、什么是协程?

               对操作系统来说:

                 1)线程是最小的【执行单元】

                 2)进程是最小的【资源管理单元】

               

    二、Java中线程具有5种状态

               1、初始化

               2、可运行

               3、运行中

               4、阻塞

               5、销毁

    线程不同状况之间的转化是谁来实现的呢?

       是JVM吗?

          并不是。JVM需要通过操作系统内核中的【TCB】(Thread Control Block)模块来改变线程的状态,这一过程需要【耗费一定的CPU资源】。

     

    三、进程和线程的痛点?

              1、线程之间是如何进行【协作】的呢?

                    ①. 最经典的例子就是【生产者/消费者模式】

                    ②. 若干个生产者线程向队列中写入数据
                          若干个消费者线程从队列中消费数据。

                      

    如何用java语言实现生产者/消费者模式呢?

    public class Producer extends Thread{
    
       private static final int MAX_QUEUE_SIZE = 5;
    
       private final Queue sharedQueue;
    
        public Producer (Queue sharedQueue) {
            super();
            this.sharedQueue = sharedQueue;
        }
    
        @Override
        public void run() {
            for(int i = 0; i < 100; i++){
               synchronized(sharedQueue){
                  while(sharedQueue.size() >= MAX_QUEUE_SIZE){
                      System.out.println("队列满了,等待消费");
                      try {
                          sharedQueue.wait();
                      } catch (InterruptedException e) {
                          e.printStackTrace();
                      }
                  }
                   sharedQueue.add(i);
                   System.out.println("进行生产 : " + i);
                   sharedQueue.notify();
               }
    
            }
        }
    }
    
     
    

     
     
       public class Consumer extends Thread{
    
        private final Queue sharedQueue;
    
        public Consumer (Queue sharedQueue) {
            super();
            this.sharedQueue = sharedQueue;
        }
    
        @Override
        public void run() {
           while(true){
              synchronized(sharedQueue){
                  while(sharedQueue.size() == 0){
                      System.out.println("队列空了,等待生产");
                      try {
                          sharedQueue.wait();
                      } catch (InterruptedException e) {
                          e.printStackTrace();
                      }
                  }
                  int number = (int) sharedQueue.poll();
                  System.out.println("进行消费 : "+ number );
                  sharedQueue.notify();
              }
    
           }
        }
    }
    
     
    

     
     
        public class ProducerConsumerTest {
    
        public  static  void main(String  args[]) {
            final Queue<Integer> sharedQueue = new  LinkedList();
            Thread producer = new Producer(sharedQueue);
            Thread consumer = new Consumer (sharedQueue);
            producer.start();
            consumer.start();
        }
    }
    
     
    

    ------------------------------------输出结果:
    进行生产 : 0
    进行生产 : 1
    进行生产 : 2
    进行生产 : 3
    进行生产 : 4
    队列满了,等待消费
    进行消费 : 0
    进行消费 : 1
    进行消费 : 2
    进行消费 : 3
    进行消费 : 4
    队列空了,等待生产
    进行生产 : 5
    进行生产 : 6
    进行生产 : 7
    进行生产 : 8
    进行生产 : 9
    队列满了,等待消费
    进行消费 : 5
    进行消费 : 6
    进行消费 : 7
    进行消费 : 8
    进行消费 : 9
    队列空了,等待生产
    进行生产 : 10
    进行生产 : 11
    进行生产 : 12
    进行生产 : 13
    进行生产 : 14
    队列满了,等待消费
    进行消费 : 10
    进行消费 : 11
    进行消费 : 12
    进行消费 : 13
    进行消费 : 14
    队列空了,等待生产
    进行生产 : 15
    进行生产 : 16
    进行生产 : 17
    进行生产 : 18
    进行生产 : 19
    队列满了,等待消费
    进行消费 : 15
    进行消费 : 16
    进行消费 : 17
    进行消费 : 18
    进行消费 : 19
    队列空了,等待生产
    进行生产 : 20
    进行生产 : 21
    进行生产 : 22
    进行生产 : 23
    进行生产 : 24
    队列满了,等待消费
    进行消费 : 20
    进行消费 : 21
    进行消费 : 22
    进行消费 : 23
    进行消费 : 24
    队列空了,等待生产
    进行生产 : 25
    进行生产 : 26
    进行生产 : 27
    进行生产 : 28
    进行生产 : 29
    队列满了,等待消费
    进行消费 : 25
    进行消费 : 26
    进行消费 : 27
    进行消费 : 28
    进行消费 : 29
    队列空了,等待生产
    进行生产 : 30
    进行生产 : 31
    进行生产 : 32
    进行生产 : 33
    进行生产 : 34
    队列满了,等待消费
    进行消费 : 30
    进行消费 : 31
    进行消费 : 32
    进行消费 : 33
    进行消费 : 34
    队列空了,等待生产
    进行生产 : 35
    进行生产 : 36
    进行生产 : 37
    进行生产 : 38
    进行生产 : 39
    队列满了,等待消费
    进行消费 : 35
    进行消费 : 36
    进行消费 : 37
    进行消费 : 38
    进行消费 : 39
    队列空了,等待生产
    进行生产 : 40
    进行生产 : 41
    进行生产 : 42
    进行生产 : 43
    进行生产 : 44
    队列满了,等待消费
    进行消费 : 40
    进行消费 : 41
    进行消费 : 42
    进行消费 : 43
    进行消费 : 44
    队列空了,等待生产
    进行生产 : 45
    进行生产 : 46
    进行生产 : 47
    进行生产 : 48
    进行生产 : 49
    队列满了,等待消费
    进行消费 : 45
    进行消费 : 46
    进行消费 : 47
    进行消费 : 48
    进行消费 : 49
    进行生产 : 50
    进行生产 : 51
    进行生产 : 52
    进行生产 : 53
    进行生产 : 54
    队列满了,等待消费
    进行消费 : 50
    进行消费 : 51
    进行消费 : 52
    进行消费 : 53
    进行消费 : 54
    队列空了,等待生产
    进行生产 : 55
    进行生产 : 56
    进行生产 : 57
    进行生产 : 58
    进行生产 : 59
    队列满了,等待消费
    进行消费 : 55
    进行消费 : 56
    进行消费 : 57
    进行消费 : 58
    进行消费 : 59
    队列空了,等待生产
    进行生产 : 60
    进行生产 : 61
    进行生产 : 62
    进行生产 : 63
    进行生产 : 64
    队列满了,等待消费
    进行消费 : 60
    进行消费 : 61
    进行消费 : 62
    进行消费 : 63
    进行消费 : 64
    队列空了,等待生产
    进行生产 : 65
    进行生产 : 66
    进行生产 : 67
    进行生产 : 68
    进行生产 : 69
    队列满了,等待消费
    进行消费 : 65
    进行消费 : 66
    进行消费 : 67
    进行消费 : 68
    进行消费 : 69
    队列空了,等待生产
    进行生产 : 70
    进行生产 : 71
    进行生产 : 72
    进行生产 : 73
    进行生产 : 74
    队列满了,等待消费
    进行消费 : 70
    进行消费 : 71
    进行消费 : 72
    进行消费 : 73
    进行消费 : 74
    队列空了,等待生产
    进行生产 : 75
    进行生产 : 76
    进行生产 : 77
    进行生产 : 78
    进行生产 : 79
    队列满了,等待消费
    进行消费 : 75
    进行消费 : 76
    进行消费 : 77
    进行消费 : 78
    进行消费 : 79
    队列空了,等待生产
    进行生产 : 80
    进行生产 : 81
    进行生产 : 82
    进行生产 : 83
    进行生产 : 84
    队列满了,等待消费
    进行消费 : 80
    进行消费 : 81
    进行消费 : 82
    进行消费 : 83
    进行消费 : 84
    队列空了,等待生产
    进行生产 : 85
    进行生产 : 86
    进行生产 : 87
    进行生产 : 88
    进行生产 : 89
    队列满了,等待消费
    进行消费 : 85
    进行消费 : 86
    进行消费 : 87
    进行消费 : 88
    进行消费 : 89
    队列空了,等待生产
    进行生产 : 90
    进行生产 : 91
    进行生产 : 92
    进行生产 : 93
    进行生产 : 94
    队列满了,等待消费
    进行消费 : 90
    进行消费 : 91
    进行消费 : 92
    进行消费 : 93
    进行消费 : 94
    队列空了,等待生产
    进行生产 : 95
    进行生产 : 96
    进行生产 : 97
    进行生产 : 98
    进行生产 : 99
    进行消费 : 95
    进行消费 : 96
    进行消费 : 97
    进行消费 : 98
    进行消费 : 99
    队列空了,等待生产

    这段代码做了下面几件事:
      1.定义了一个生产者类,一个消费者类。
      2.生产者类循环100次,向同步队列当中插入数据。
      3.消费者循环监听同步队列,当队列有数据时拉取数据。
      4.如果队列满了(达到5个元素),生产者阻塞。
      5.如果队列空了,消费者阻塞。

     

    上面的代码正确地实现了生产者/消费者模式,但是却并不是一个高性能的实现。
    为什么性能不高呢?原因如下: 

       1. 涉及到同步锁

       2. 涉及到线程阻塞状态和可运行状态之间的切换

       3. 涉及到线程上下文的切换

       以上涉及到的任何一点,都是非常耗费性能的操作。

    这个时候【协程】就登场了

     

    四、到底什么是【协程】?

            协程,英文Coroutines,是一种比【线程】更加【轻量级】的存在。

            正如一个进程可以拥有多个线程一样,一个线程也可以拥有多个协程。

            

          最重要的是,【协程】不是被操作系统【内核】所管理的,而完全是由【程序】所控制(也就是在【用户态】执行)

          这样带来的好处就是:

              性能得到了很大的提升,不会像线程切换那样消耗资源。

     

    五、既然协程这么好,它到底是怎么来使用的呢?

              由于Java的原生语法中并没有实现协程(某些开源框架实现了协程,但是很少被使用)

              协程的开销远远小于线程的开销。

     

    六、协程的应用

    Java语言
       如上文所说,Java语言并没有对协程的原生支持,但是某些开源框架模拟出了协程的功能,有兴趣的小伙伴可以看一看Kilim框架的源码:
      https://github.com/kilim/kilim

     

    展开全文
  • 协程概述

    千次阅读 多人点赞 2018-06-04 21:21:14
    协程不是进程,也不是线程,它就是一个函数,一个特殊的函数——可以在某个地方挂起,并且可以重新在挂起处继续运行。所以说,协程与进程、线程相比,不是一个维度的概念。一个进程可以包含多个线程,一个线程也可以...
    协程不是进程,也不是线程,它就是一个函数,一个特殊的函数——可以在某个地方挂起,并且可以重新在挂起处继续运行。所以说,协程与进程、线程相比,不是一个维度的概念。
    一个进程可以包含多个线程,一个线程也可以包含多个协程,也就是说,一个线程内可以有多个那样的特殊函数在运行。但是有一点,必须明确,一个线程内的多个协程的运行是串行的。如果有多核CPU的话,多个进程或一个进程内的多个线程是可以并行运行的,但是一个线程内的多个协程却绝对串行的,无论有多少个CPU(核)。这个比较好理解,毕竟协程虽然是一个特殊的函数,但仍然是一个函数。一个线程内可以运行多个函数,但是这些函数都是串行运行的。当一个协程运行时,其他协程必须挂起。
    一 协程与进程、线程的比较
    虽然说,协程与进程、线程相比不是一个维度的概念,但是有时候,我们仍然需要将它们做一番比较,具体如下:
    1 协程既不是进程,也不是线程,协程仅仅是一个特殊的函数,协程跟他们就不是一个维度。
    2 一个进程可以包含多个线程,一个线程可以包含多个协程。
    3 一个线程内的多个协程虽然可以切换,但是这多个协程是串行执行的,只能在这一个线程内运行,没法利用CPU多核能力。
    4 协程与进程一样,它们的切换都存在上下文切换问题。
    表面上,进程、线程、协程都存在上下文切换的问题,但是三者上下文切换又有明显不同,见下表:
    二 协程的使用场景
    一个线程内的多个协程是串行执行的,不能利用多核,所以,显然,协程不适合计算密集型的场景。协程适合I/O 阻塞型。
    I/O本身就是阻塞型的(相较于CPU的时间世界而言)。就目前而言,无论I/O的速度多快,也比不上CPU的速度,所以一个I/O相关的程序,当其在进行I/O操作时候,CPU实际上是空闲的。
    我们假设这样的场景,如下图:1个线程有5个I/O的事情(子程序)要处理。如果我们绝对的串行化,那么当其中一个I/O阻塞时,其他4个I/O并不能得到执行,因为程序是绝对串行的,5个I/O必须一个一个排队等待处理,当一个I/O阻塞时,其它4个也得等着。
    而协程能比较好地处理这个问题,当一个协程(特殊子进程)阻塞时,它可以切换到其他没有阻塞的协程上去继续执行,这样就能得到比较高的效率,如下图所示:
    上面举的例子是5个I/O处理,如果每秒500个,5万个或500万个呢?已经达到了“I/O密集型”的程度,而“I/O密集型”确实是协程无法应付的,因为它没有利用多核的能力。这个时候的解决方案就是“多进程+协程”了。
    所以说,I/O阻塞时,利用协程来处理确实有优点(切换效率比较高),但是我们也需要看到其不能利用多核的这个缺点,必要的时候,还需要使用综合方案:多进程+协程。
    Neutorn可以归类为I/O阻塞型,所以在Neutorn的代码中处处可见协程的相关代码。
    展开全文
  • 浅谈我对协程的理解

    万次阅读 多人点赞 2014-12-03 17:09:44
    最近在研究网络服务框架方面的东西,发现了一个神奇的东西-协程。 一句话说明什么是线程:协程是一种用户态的轻量级线程。 一句话并不能完全概括协程的全部,但是起码能让我们对协程这个概念有一个基本的印象。 从...

    我心中的协程

    最近在研究网络服务框架方面的东西,发现了一个神奇的东西-协程。
    一句话说明什么是线程:协程是一种用户态的轻量级线程

    一句话并不能完全概括协程的全部,但是起码能让我们对协程这个概念有一个基本的印象。
    从硬件发展来看,从最初的单核单CPU,到单核多CPU,多核多CPU,似乎已经到了极限了,但是单核CPU性能却还在不断提升。server端也在不断的发展变化。如果将程序分为IO密集型应用和CPU密集型应用,二者的server的发展如下:
    IO密集型应用: 多进程->多线程->事件驱动->协程
    CPU密集型应用:多进程-->多线程

    如果说多进程对于多CPU,多线程对应多核CPU,那么事件驱动和协程则是在充分挖掘不断提高性能的单核CPU的潜力。
    以下的讨论如无特别说明,不考虑cpu密集型应用。

    异步 vs 同步

    无论是线程还是进程,使用的都是同步进制,当发生阻塞时,性能会大幅度降低,无法充分利用CPU潜力,浪费硬件投资,更重要造成软件模块的铁板化,紧耦合,无法切割,不利于日后扩展和变化。不管是进程还是线程,每次阻塞、切换都需要陷入系统调用(system call),先让CPU跑操作系统的调度程序,然后再由调度程序决定该跑哪一个进程(线程)。多个线程之间在一些访问互斥的代码时还需要加上锁,这也是导致多线程编程难的原因之一。

    现下流行的异步server都是基于事件驱动的(如nginx)。事件驱动简化了编程模型,很好地解决了多线程难于编程,难于调试的问题。异步事件驱动模型中,把会导致阻塞的操作转化为一个异步操作,主线程负责发起这个异步操作,并处理这个异步操作的结果。由于所有阻塞的操作都转化为异步操作,理论上主线程的大部分时间都是在处理实际的计算任务,少了多线程的调度时间,所以这种模型的性能通常会比较好。

    总的说来,当单核cpu性能提升,cpu不在成为性能瓶颈时,采用异步server能够简化编程模型,也能提高IO密集型应用的性能。

    协程 vs 线程

    之前说道,协程是一种用户级的轻量级线程。协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈。因此:

    协程能保留上一次调用时的状态(即所有局部状态的一个特定组合),每次过程重入时,就相当于进入上一次调用的状态,换种说法:进入上一次离开时所处逻辑流的位置。

    在并发编程中,协程与线程类似,每个协程表示一个执行单元,有自己的本地数据,与其它协程共享全局数据和其它资源。目前主流语言基本上都选择了多线程作为并发设施,与线程相关的概念是抢占式多任务(Preemptive multitasking),而与协程相关的是协作式多任务

    不管是进程还是线程,每次阻塞、切换都需要陷入系统调用(system call),先让CPU跑操作系统的调度程序,然后再由调度程序决定该跑哪一个进程(线程)。
    而且由于抢占式调度执行顺序无法确定的特点,使用线程时需要非常小心地处理同步问题,而协程完全不存在这个问题(事件驱动和异步程序也有同样的优点)。

    我们在自己在进程里面完成逻辑流调度,碰着i\o我就用非阻塞式的。那么我们即可以利用到异步优势,又可以避免反复系统调用,还有进程切换造成的开销,分分钟给你上几千个逻辑流不费力。这就是协程。

    协程 vs 事件驱动

    以nginx为代表的事件驱动的异步server正在横扫天下,那么事件驱动模型会是server端模型的终点吗?
    我们可以深入了解下,事件驱动编程的模型。
    事件驱动编程的架构是预先设计一个事件循环,这个事件循环程序不断地检查目前要处理的信息,根据要处理的信息运行一个触发函数。其中这个外部信息可能来自一个目录夹中的文件,可能来自键盘或鼠标的动作,或者是一个时间事件。这个触发函数,可以是系统默认的也可以是用户注册的回调函数。

    事件驱动程序设计着重于弹性以及异步化上面。许多GUI框架(如windows的MFC,Android的GUI框架),Zookeeper的Watcher等都使用了事件驱动机制。未来还会有其他的基于事件驱动的作品出现。

    基于事件驱动的编程是单线程思维,其特点是异步+回调。
    协程也是单线程,但是它能让原来要使用异步+回调方式写的非人类代码,可以用看似同步的方式写出来。它是实现推拉互动的所谓非抢占式协作的关键。

    总结

    协程的好处:

    • 跨平台
    • 跨体系架构
    • 无需线程上下文切换的开销
    • 无需原子操作锁定及同步的开销
    • 方便切换控制流,简化编程模型
    • 高并发+高扩展性+低成本:一个CPU支持上万的协程都不是问题。所以很适合用于高并发处理。

    缺点:

    • 无法利用多核资源:协程的本质是个单线程,它不能同时将 单个CPU 的多个核用上,协程需要和进程配合才能运行在多CPU上.当然我们日常所编写的绝大部分应用都没有这个必要,除非是cpu密集型应用。
    • 进行阻塞(Blocking)操作(如IO时)会阻塞掉整个程序:这一点和事件驱动一样,可以使用异步IO操作来解决
    展开全文
  • C/C++协程学习笔记

    万次阅读 多人点赞 2018-08-09 08:18:30
    协程,又称微线程,纤程。英文名Coroutine。 协程的概念很早就提出来了,但直到最近几年才在某些语言(如Lua)中得到广泛应用。 子程序,或者称为函数,在所有语言中都是层级调用,比如A调用B,B在执行过程中又...

    协程,又称微线程,纤程。英文名Coroutine。

    协程的概念很早就提出来了,但直到最近几年才在某些语言(如Lua)中得到广泛应用。

    子程序,或者称为函数,在所有语言中都是层级调用,比如A调用B,B在执行过程中又调用了C,C执行完毕返回,B执行完毕返回,最后是A执行完毕。

    所以子程序调用是通过栈实现的,一个线程就是执行一个子程序。

    子程序调用总是一个入口,一次返回,调用顺序是明确的。而协程的调用和子程序不同。

    协程看上去也是子程序,但执行过程中,在子程序内部可中断,然后转而执行别的子程序,在适当的时候再返回来接着执行。

    注意,在一个子程序中中断,去执行其他子程序,不是函数调用,有点类似CPU的中断。比如子程序A、B:

    def A():
        print '1'
        print '2'
        print '3'
    
    def B():
        print 'x'
        print 'y'
        print 'z'

    假设由协程执行,在执行A的过程中,可以随时中断,去执行B,B也可能在执行过程中中断再去执行A,结果可能是:

    1
    2
    x
    y
    3
    z

    但是在A中是没有调用B的,所以协程的调用比函数调用理解起来要难一些。

    看起来A、B的执行有点像多线程,但协程的特点在于是一个线程执行,那和多线程比,协程有何优势?

    最大的优势就是协程极高的执行效率。因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显。

    第二大优势就是不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。

    因为协程是一个线程执行,那怎么利用多核CPU呢?最简单的方法是多进程+协程,既充分利用多核,又充分发挥协程的高效率,可获得极高的性能。

    Python对协程的支持还非常有限,用在generator中的yield可以一定程度上实现协程。虽然支持不完全,但已经可以发挥相当大的威力了。

    来看例子:

    传统的生产者-消费者模型是一个线程写消息,一个线程取消息,通过锁机制控制队列和等待,但一不小心就可能死锁。

    如果改用协程,生产者生产消息后,直接通过yield跳转到消费者开始执行,待消费者执行完毕后,切换回生产者继续生产,效率极高:

     

    import time
    
    def consumer():
        r = ''
        while True:
            n = yield r
            if not n:
                return
            print('[CONSUMER] Consuming %s...' % n)
            time.sleep(1)
            r = '200 OK'
    
    def produce(c):
        c.next()
        n = 0
        while n < 5:
            n = n + 1
            print('[PRODUCER] Producing %s...' % n)
            r = c.send(n)
            print('[PRODUCER] Consumer return: %s' % r)
        c.close()
    
    if __name__=='__main__':
        c = consumer()
        produce(c)

    执行结果:

    [PRODUCER] Producing 1...
    [CONSUMER] Consuming 1...
    [PRODUCER] Consumer return: 200 OK
    [PRODUCER] Producing 2...
    [CONSUMER] Consuming 2...
    [PRODUCER] Consumer return: 200 OK
    [PRODUCER] Producing 3...
    [CONSUMER] Consuming 3...
    [PRODUCER] Consumer return: 200 OK
    [PRODUCER] Producing 4...
    [CONSUMER] Consuming 4...
    [PRODUCER] Consumer return: 200 OK
    [PRODUCER] Producing 5...
    [CONSUMER] Consuming 5...
    [PRODUCER] Consumer return: 200 OK

    注意到consumer函数是一个generator(生成器),把一个consumer传入produce后:

    1. 首先调用c.next()启动生成器;

    2. 然后,一旦生产了东西,通过c.send(n)切换到consumer执行;

    3. consumer通过yield拿到消息,处理,又通过yield把结果传回;

    4. produce拿到consumer处理的结果,继续生产下一条消息;

    5. produce决定不生产了,通过c.close()关闭consumer,整个过程结束。

    整个流程无锁,由一个线程执行,produce和consumer协作完成任务,所以称为“协程”,而非线程的抢占式多任务。

     

    C/C++ 协程

    首先需要声明的是,这里不打算花时间来介绍什么是协程,以及协程和线程有什么不同。如果对此有任何疑问,可以自行 google。与 Python 不同,C/C++ 语言本身是不能天然支持协程的。现有的 C++ 协程库均基于两种方案:利用汇编代码控制协程上下文的切换,以及利用操作系统提供的 API 来实现协程上下文切换。典型的例如:

    • libco,Boost.context:基于汇编代码的上下文切换
    • phxrpc:基于 ucontext/Boost.context 的上下文切换
    • libmill:基于 setjump/longjump 的协程切换

    一般而言,基于汇编的上下文切换要比采用系统调用的切换更加高效,这也是为什么 phxrpc 在使用 Boost.context 时要比使用 ucontext 性能更好的原因。关于 phxrpc 和 libmill 具体的协程实现方式,以后有时间再详细介绍。

    libco 协程的创建和切换

    在介绍 coroutine 的创建之前,我们先来熟悉一下 libco 中用来表示一个 coroutine 的数据结构,即定义在 co_routine_inner.h 中的 stCoRoutine_t:

    struct stCoRoutine_t
    {
        stCoRoutineEnv_t *env;  // 协程运行环境
        pfn_co_routine_t pfn;   // 协程执行的逻辑函数
        void *arg;              // 函数参数
        coctx_t ctx;            // 保存协程的下文环境 
        ...
        char cEnableSysHook;    // 是否运行系统 hook,即非侵入式逻辑
        char cIsShareStack;     // 是否在共享栈模式
        void *pvEnv;
        stStackMem_t* stack_mem;  // 协程运行时的栈空间
        char* stack_sp;           // 用来保存协程运行时的栈空间
        unsigned int save_size;
        char* save_buffer;
    };

    我们暂时只需要了解表示协程的最简单的几个参数,例如协程运行环境,协程的上下文环境,协程运行的函数以及运行时栈空间。后面的 stack_spsave_size 和 save_buffer 与 libco 共享栈模式相关,有关共享栈的内容我们后续再说

    协程创建和运行

    由于多个协程运行于一个线程内部的,因此当创建线程中的第一个协程时,需要初始化该协程所在的环境 stCoRoutineEnv_t,这个环境是线程用来管理协程的,通过该环境,线程可以得知当前一共创建了多少个协程,当前正在运行哪一个协程,当前应当如何调度协程:

    struct stCoRoutineEnv_t
    {
        stCoRoutine_t *pCallStack[ 128 ];  // 记录当前创建的协程
        int iCallStackSize;                // 记录当前一共创建了多少个协程
        stCoEpoll_t *pEpoll;               // 该线程的协程调度器
    
        // 在使用共享栈模式拷贝栈内存时记录相应的 coroutine
        stCoRoutine_t* pending_co;
        stCoRoutine_t* occupy_co;
    };

    上述代码表明 libco 允许一个线程内最多创建 128 个协程,其中 pCallStack[iCallStackSize-1] 也就是栈顶的协程表示当前正在运行的协程。当调用函数 co_create 时,首先检查当前线程中的 coroutine env 结构是否创建。这里 libco 对于每个线程内的 stCoRoutineEnv_t 并没有使用 thread-local 的方式(例如gcc 内置的 __threadphxrpc采用这种方式)来管理,而是预先定义了一个大的数组,并通过对应的 PID 来获取其协程环境。:

    static stCoRoutineEnv_t* g_arrCoEnvPerThread[204800]
    stCoRoutineEnv_t *co_get_curr_thread_env()
    {
    	return g_arrCoEnvPerThread[ GetPid() ];
    }

    初始化 stCoRoutineEnv_t 时主要完成以下几步:

    1. 为 stCoRoutineEnv_t 申请空间并且进行初始化,设置协程调度器 pEpoll
    2. 创建一个空的 coroutine,初始化其上下文环境( 有关 coctx 在后文详细介绍 ),将其加入到该线程的协程环境中进行管理,并且设置其为 main coroutine。这个 main coroutine 用来运行该线程主逻辑。

    当初始化完成协程环境之后,调用函数 co_create_env 来创建具体的协程,该函数初始化一个协程结构 stCoRoutine_t,设置该结构中的各项字段,例如运行的函数 pfn,运行时的栈地址等等。需要说明的就是,如果使用了非共享栈模式,则需要为该协程单独申请栈空间,否则从共享栈中申请空间。栈空间表示如下:

    struct stStackMem_t
    {
        stCoRoutine_t* occupy_co;  // 使用该栈的协程
        int stack_size;            // 栈大小
        char* stack_bp;            // 栈底指针,栈从高地址向低地址增长
        char* stack_buffer;        // 栈底
    };

    使用 co_create 创建完一个协程之后,将调用 co_resume 来将该协程激活运行:

    void co_resume( stCoRoutine_t *co )
    {
        stCoRoutineEnv_t *env = co->env;
        // 获取当前正在运行的协程的结构
        stCoRoutine_t *lpCurrRoutine = env->pCallStack[ env->iCallStackSize - 1 ];
        if( !co->cStart )
        {
            // 为将要运行的 co 布置上下文环境
            coctx_make( &co->ctx,(coctx_pfn_t)CoRoutineFunc,co,0 );
            co->cStart = 1;
        }
        env->pCallStack[ env->iCallStackSize++ ] = co;  // 设置co为运行的线程
        co_swap( lpCurrRoutine, co );  
    }

    函数 co_swap 的作用类似于 Unix 提供的函数 swapcontext:将当前正在运行的 coroutine 的上下文以及状态保存到结构 lpCurrRoutine 中,并且将 co 设置成为要运行的协程,从而实现协程的切换。co_swap 具体完成三项工作:

    1. 记录当前协程 curr 的运行栈的栈顶指针,通过 char c; curr_stack_sp=&c 实现,当下次切换回 curr时,可以从该栈顶指针指向的位置继续,执行完 curr 后可以顺利释放该栈。
    2. 处理共享栈相关的操作,并且调用函数 coctx_swap 来完成上下文环境的切换。注意执行完 coctx_swap之后,执行流程将跳到新的 coroutine 也就是 pending_co 中运行,后续的代码需要等下次切换回 curr 时才会执行。
    3. 当下次切换回 curr 时,处理共享栈相关的操作。

    对应于 co_resume 函数,协程主动让出执行权则调用 co_yield 函数。co_yield 函数调用了 co_yield_env,将当前协程与当前线程中记录的其他协程进行切换:

    void co_yield_env( stCoRoutineEnv_t *env )
    {
        stCoRoutine_t *last = env->pCallStack[ env->iCallStackSize - 2 ];
        stCoRoutine_t *curr = env->pCallStack[ env->iCallStackSize - 1 ];
    
        env->iCallStackSize--;
        co_swap( curr, last);
    }

    前面我们已经提到过,pCallStack 栈顶所指向的即为当前正在运行的协程所对应的结构,因此该函数将 curr 取出来,并将当前正运行的协程上下文保存到该结构上,并切换到协程 last 上执行。接下来我们以 32-bit 的系统为例来分析 libco 是如何实现协程运行环境的切换的。

    协程上下文的创建和切换

    libco 使用结构 struct coctx_t 来表示一个协程的上下文环境:

    struct coctx_t
    {
    #if defined(__i386__)
        void *regs[ 8 ];
    #else
        void *regs[ 14 ];
    #endif
        size_t ss_size;
        char *ss_sp;
    };

    可以看到,在 i386 的架构下,需要保存 8 个寄存器信息,以及栈指针和栈大小,究竟这 8 个寄存器如何保存,又是如何使用,需要配合后续的 coctx_swap 来理解。我们首先来回顾一下 Unix-like 系统的 stack frame layout,如果不能理解这个,那么剩下的内容就不必看了。

                                                                     

     

    结合上图,我们需要知道关键的几点:

    1. 函数调用栈是调用者和被调用者共同负责布置的。Caller 将其参数从右向左反向压栈,再将调用后的返回地址压栈,然后将执行流程交给 Callee。
    2. 典型的编译器会将 Callee 函数汇编成为以 push %ebp; move %ebp, %esp; sub $esp N; 这种形式开头的汇编代码。这几句代码主要目的是为了方便 Callee 利用 ebp 来访问调用者提供的参数以及自身的局部变量(如下图)。
    3. 当调用过程完成清除了局部变量以后,会执行 pop %ebp; ret,这样指令会跳转到 RA 也就是返回地址上面执行。这一点也是实现协程切换的关键:我们只需要将指定协程的函数指针地址保存到 RA 中,当调用完 coctx_swap 之后,会自动跳转到该协程的函数起始地址开始运行

    了解了这些,我们就来看一下协程上下文环境的初始化函数 coctx_make

    int coctx_make( coctx_t *ctx, coctx_pfn_t pfn, const void *s, const void *s1 )
    {
    	char *sp = ctx->ss_sp + ctx->ss_size - sizeof(coctx_param_t);
    	sp = (char*)((unsigned long)sp & -16L);
    
    	coctx_param_t* param = (coctx_param_t*)sp ;
    	param->s1 = s;
    	param->s2 = s1;
    
    	memset(ctx->regs, 0, sizeof(ctx->regs));
    	ctx->regs[ kESP ] = (char*)(sp) - sizeof(void*);
    	ctx->regs[ kEIP ] = (char*)pfn;
    	return 0;
    }

    这段代码应该比较好理解,首先为函数 coctx_pfn_t 预留 2 个参数的栈空间并对其到 16 字节,之后将实参设置到预留的栈上空间中。最后在 ctx 结构中填入相应的,其中记录 reg[kEIP] 返回地址为函数指针 pfn,记录 reg[kESP] 为获得的栈顶指针 sp 减去一个指针长度,这个减去的空间是为返回地址 RA 预留的。当调用 coctx_swap 时,reg[kEIP] 会被放到返回地址 RA 的位置,待 coctx_swap 执行结束,自然会跳转到函数 pfn 处执行。

    coctx_swap(ctx1, ctx2) 在 coctx_swap.S 中实现。这里可以看到,该函数并没有使用 push %ebp; move %ebp, %esp; sub $esp N; 开头,因此栈空间分布中不会出现 ebp 的位置。coctx_swap 函数主要分为两段,其首先将当前的上下文环境保存到 ctx1 结构中:

        leal 4(%esp), %eax     // eax = old_esp + 4                                             
        movl 4(%esp), %esp     // 将 esp 的值设为 &ctx1(即ctx1的地址)        
        leal 32(%esp), %esp    // esp = (char*)&ctx1 + 32            
                                                  
        pushl %eax         //  ctx1->regs[EAX] = %eax 
        pushl %ebp         //  ctx1->regs[EBP] = %ebp
        pushl %esi         //  ctx1->regs[ESI] = %esi
        pushl %edi         //  ctx1->regs[EDI] = %edi
        pushl %edx         //  ctx1->regs[EDX] = %edx
        pushl %ecx         //  ctx1->regs[ECX] = %ecx
        pushl %ebx         //  ctx1->regs[EBX] = %ebx
        pushl -4(%eax)     //  ctx1->regs[EIP] = RA, 注意:%eax-4=%old_esp   

    这里需要注意指令 leal 和 movl 的区别。leal 将 eax 的值设置成为 esp 的值加 4,而 movl 将 esp 的值设为 esp+4 所指向的内存上的值,也就是参数 ctx1 的地址。之后该函数将 ctx2 中记录的上下文恢复到 CPU 寄存器中,并跳转到其函数地址处运行:

        movl 4(%eax), %esp //  将 esp 的值设为 &ctx2(即ctx2的地址)
        popl %eax          // %eax = ctx1->regs[EIP],也就是 &pfn
        popl %ebx          // %ebx = ctx1->regs[EBP]
        popl %ecx          // %ecx = ctx1->regs[ECX]
        popl %edx          // %edx = ctx1->regs[EDX]
        popl %edi          // %edi = ctx1->regs[EDI]
        popl %esi          // %esi = ctx1->regs[ESI]
        popl %ebp          // %ebp = ctx1->regs[EBP]
        popl %esp          // %esp = ctx1->regs[ESP],即(char*)(sp) - sizeof(void*)
        pushl %eax         // RA = %eax = &pfn,注意此时esp已经指向了新的esp
    	
        xorl %eax, %eax    // reset eax
        ret

    上面的代码看起来可能有些绕:

    1. 首先 line 1 将 esp 设置为参数 ctx2 的地址,后续的 popl 操作均在 ctx2 的内存空间上执行。
    2. line 2-9 将 ctx2->regs[] 中的内容恢复到相应的寄存器中。还记得在前面 coctx_make 中设置了 regs[EIP] 和 regs[ESP] 吗?这里刚好就对应恢复了相应的值。
    3. 当执行完 line 9 之后,esp 已经指向了 ctx2 中新的栈顶指针,由于在 coctx_make 中预留了一个指针长度的 RA 空间,line 10 刚好将新的函数指针 &pfn 设置到该 RA 上。
    4. 最后执行 ret 指令时,函数流程将跳到 pfn 处执行。这样,整个协程上下文的切换就完成了。

    如何使用 libco

    我们首先以 libco 提供的例子 example_echosvr.cpp 来介绍应用程序如何使用 libco 来编写服务端程序。 在 example_echosvr.cpp 的 main 函数中,主要执行如下几步:

    1. 创建 socket,监听在本机的 1024 端口,并设置为非阻塞;
    2. 主线程使用函数 readwrite_coroutine 创建多个读写协程,调用 co_resume 启动协程运行直到其挂起。这里我们忽略掉无关的多进程 fork 的过程;
    3. 主线程继续创建 socket 接收协程 accpet_co,同样调用 co_resume 启动协程直到其挂起;
    4. 主线程调用函数 co_eventloop 实现事件的监听和协程的循环切换;

    函数 readwrite_coroutine 在外层循环中将新创建的读写协程都加入到队列 g_readwrite 中,此时这些读写协程都没有具体与某个 socket 连接对应,可以将队列 g_readwrite 看成一个 coroutine pool。当加入到队列中之后,调用函数 co_yield_ct 函数让出 CPU,此时控制权回到主线程。

    主线程中的函数 co_eventloop 监听网络事件,将来自于客户端新进的连接交由协程 accept_co 处理,关于 co_eventloop 如何唤醒 accept_co 的细节我们将在后续介绍。accept_co 调用函数 accept_routine 接收新连接,该函数的流程如下:

    1. 检查队列 g_readwrite 是否有空闲的读写 coroutine,如果没有,调用函数 poll 将该协程加入到 Epoll 管理的定时器队列中,也就是 sleep(1000) 的作用;
    2. 调用 co_accept 来接收新连接,如果接收连接失败,那么调用 co_poll 将服务端的 listen_fd 加入到 Epoll 中来触发下一次连接事件;
    3. 对于成功的连接,从 g_readwrite 中取出一个读写协程来负责处理读写;

    再次回到函数 readwrite_coroutine 中,该函数会调用 co_poll 将新建立的连接的 fd 加入到 Epoll 监听中,并将控制流程返回到 main 协程;当有读或者写事件发生时,Epoll 会唤醒对应的 coroutine ,继续执行 read 函数以及 write 函数。

    上面的过程大致说明了控制流程是如何在不同的协程中切换,接下来我们介绍具体的实现细节,即如何通过 Epoll 来管理协程,以及如何对系统函数进行改造以满足 libco 的调用。

    通过 Epoll 管理和唤醒协程

    Epoll 监听 FD

    上一章节中介绍了协程可以通过函数 co_poll 来将 fd 交由 Epoll 管理,待 Epoll 的相应的事件触发时,再切换回来执行 read 或者 write 操作,从而实现由 Epoll 管理协程的功能。co_poll 函数原型如下:

    int co_poll(stCoEpoll_t *ctx, struct pollfd fds[], 
                nfds_t nfds, int timeout_ms)

    stCoEpoll_t 是为 libco 定制的 Epoll 相关数据结构,fds 是 pollfd 结构的文件句柄,nfds 为 fds 数组的长度,最后一个参数表示定时器时间,也就是在 timeout 毫秒之后触发处理这些文件句柄。这里可以看到,co_poll 能够同时将多个文件句柄同时加入到 Epoll 管理中。我们先看 stCoEpoll_t 结构:

    struct stCoEpoll_t
    {
        int iEpollFd;  // Epoll 主 FD
        static const int _EPOLL_SIZE = 1024 * 10;  // Epoll 可以监听的句柄总数
    
        struct stTimeout_t *pTimeout;  // 时间轮定时器
        struct stTimeoutItemLink_t *pstTimeoutList;  // 已经超时的时间
        struct stTimeoutItemLink_t *pstActiveList;   // 活跃的事件
        co_epoll_res *result;  // Epoll 返回的事件结果
    };
    

    以 stTimeout_ 开头的数据结构与 libco 的定时器管理有关,我们在后面介绍。co_epoll_res 是对 Epoll 事件数据结构的封装,也就是每次触发 Epoll 事件时的返回结果,在 Unix 和 MaxOS 下,libco 将使用 Kqueue 替代 Epoll,因此这里也保留了 kevent 数据结构。

    struct co_epoll_res
    {
        int size;
        struct epoll_event *events;  // for linux epoll
        struct kevent *eventlist;    // for Unix or MacOs kqueue
    };

    co_poll 实际是对函数 co_poll_inner 的封装。我们将 co_epoll_inner 函数的结构分为上下两半段。在上半段中,调用 co_poll 的协程 CC 将其需要监听的句柄数组 fds 都加入到 Epoll 管理中,并通过函数 co_yield_env 让出 CPU;当 main 协程的事件循环 co_eventloop 中触发了 CC 对应的监听事件时,会恢复 CC的执行。此时,CC 将开始执行下半段,即将上半段添加的句柄 fds 从 epoll 中移除,清理残留的数据结构,下面的流程图简要说明了控制流的转移过程:

                                                      

    有了上面的基本概念,我们来看具体的实现细节。co_poll 首先在内部将传入的文件句柄数组 fds 转化为数据结构 stPoll_t,这一步主要是为了方便后续处理。该结构记录了 iEpollFdndfsfds 数组,以及该协程需要执行的函数和参数。有两点需要说明的是:

    1. 对于每一个 fd,为其申请一个 stPollItem_t 来管理对应 Epoll 事件以及记录回调参数。libco 在此做了一个小的优化,对于长度小于 2 的 fds 数组,直接在栈上定义相应的 stPollItem_t 数组,否则从堆中申请内存。这也是一种比较常见的优化,毕竟从堆中申请内存比较耗时;
    2. 函数指针 OnPollProcessEvent 封装了协程的切换过程。当传入指定的 stPollItem_t 结构时,即可唤醒对应于该结构的 coroutine,将控制权交由其执行;

    co_poll 的第二步,也是最关键的一步,就是将 fd 数组全部加入到 Epoll 中进行监听。协程 CC 会将每一个 epoll_event 的 data.ptr 域设置为对应的 stPollItem_t 结构。这样当事件触发时,可以直接从对应的 ptr中取出 stPollItem_t 结构,然后唤醒指定协程。

    如果本次操作提供了 Timeout 参数,co_poll 还会将协程 CC 本次操作对应的 stPoll_t 加入到定时器队列中。这表明在 Timeout 定时触发之后,也会唤醒协程 CC 的执行。当整个上半段都完成后,co_poll 立即调用 co_yield_env 让出 CPU,执行流程跳转回到 main 协程中。

    从上面的流程图中也可以看出,当执行流程再次跳回时,表明协程 CC 添加的读写等监听事件已经触发,即可以执行相应的读写操作了。此时 CC 首先将其在上半段中添加的监听事件从 Epoll 中删除,清理残留的数据结构,然后调用读写逻辑。

    定时器实现

    协程 CC 在将一组 fds 加入 Epoll 的同时,还能为其设置一个超时时间。在超时时间到期时,也会再次唤醒 CC 来执行。libco 使用 Timing-Wheel 来实现定时器。关于 Timing-Wheel 算法,可以参考,其优势是 O(1) 的插入和删除复杂度,缺点是只有有限的长度,在某些场合下不能满足需求。

                                                          

    回过去看 stCoEpoll_t 结构,其中 *pTimeout 代表时间轮,通过函数 AllocateTimeout 初始化为一个固定大小(60 * 1000)的数组。根据 Timing-Wheel 的特性可知,libco 只支持最大 60s 的定时事件。而实际上,在添加定时器时,libco 要求定时时间不超过 40s。成员 pstTimeoutList 记录在 co_eventloop 中发生超时的事件,而 pstActiveList 记录当前活跃的事件,包括超时事件。这两个结构都将在 co_eventloop 中进行处理。

    下面我们简要分析一下加入定时器的实现:

    int AddTimeout( stTimeout_t *apTimeout, stTimeoutItem_t *apItem, 
        unsigned long long allNow )
    {
        if( apTimeout->ullStart == 0 )  // 初始化时间轮的基准时间
        {
            apTimeout->ullStart = allNow;
            apTimeout->llStartIdx = 0;  // 当前时间轮指针指向数组0
        }
        // 1. 当前时间不可能小于时间轮的基准时间
        // 2. 加入的定时器的超时时间不能小于当前时间
        if( allNow < apTimeout->ullStart || apItem->ullExpireTime < allNow )
        {
            return __LINE__;
        }
    
        int diff = apItem->ullExpireTime - apTimeout->ullStart;
        if( diff >= apTimeout->iItemSize )  // 添加的事件不能超过时间轮的大小
        {
            return __LINE__;
        }
        // 插入到时间轮盘的指定位置
        AddTail( apTimeout->pItems + 
            (apTimeout->llStartIdx + diff ) % apTimeout->iItemSize, apItem );
    
        return 0;
    }

    定时器的超时检查在函数 co_eventloop 中执行。

    EPOLL 事件循环

    main 协程通过调用函数 co_eventloop 来监听 Epoll 事件,并在相应的事件触发时切换到指定的协程执行。有关 co_eventloop 与 应用协程的交互过程在上一节的流程图中已经比较清楚了,下面我们主要介绍一下 co_eventloop 函数的实现:

    上文中也提到,通过 epoll_wait 返回的事件都保存在 stCoEpoll_t 结构的 co_epoll_res 中。因此 co_eventloop 首先为 co_epoll_res 申请空间,之后通过一个无限循环来监听所有 coroutine 添加的所有事件:

    for(;;)
    {
        int ret = co_epoll_wait( ctx->iEpollFd,result,stCoEpoll_t::_EPOLL_SIZE, 1 );
        ...
    }

    对于每一个触发的事件,co_eventloop 首先通过指针域 data.ptr 取出保存的 stPollItem_t 结构,并将其添加到 pstActiveList 列表中;之后从定时器轮盘中取出所有已经超时的事件,也将其全部添加到 pstActiveList 中,pstActiveList 中的所有事件都作为活跃事件处理。

    对于每一个活跃事件,co_eventloop 将通过调用对应的 pfnProcess 也就是上图中的OnPollProcessEvent 函数来切换到该事件对应的 coroutine,将流程跳转到该 coroutine 处执行。

    最后 co_eventloop 在调用时也提供一个额外的参数来供调用者传入一个函数指针 pfn。该函数将会在每次循环完成之后执行;当该函数返回 -1 时,将会终止整个事件循环。用户可以利用该函数来控制 main 协程的终止或者完成一些统计需求。

     

    展开全文
  • 什么是协程

    万次阅读 多人点赞 2019-03-25 17:34:15
    来自:程序员小灰(微信号:chengxuyuanxiaohui) ————— 第二天 ————— ———————————— 什么是进程和线程 ...进程拥有代码和打开的文件资源、数据资...
  • 协程

    千次阅读 2019-02-25 22:13:18
    import time import queue def consumer(name): print('---&amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;gt;ready to eat humburger...... new_humberger = yield # # 没有send就挂起来,等相当于阻塞。... pr..
  • 协程(一)协程的定义与分类

    千次阅读 2016-09-30 10:25:26
    协程,又称微线程和纤程等,据说源于 Simula 和 Modula-2 语言(我没有深究,有错请指正),现代编程语言基本上都有支持,比如 Lua、ruby 和最新的 Google Go,当然也还有最近很让我惊艳的 falcon。协程是用户空间线
  • Unity 停止(终止)协程

    万次阅读 2017-01-03 19:30:41
    测试一下停止协程是如何使用,准备如下脚本,并将该脚本挂在场景中任何一个物体上测试发现: 再次运行场景,接着就立即点击鼠标左键,测试结果如下: FR:海涛高软(QQ技术群:386476712)
  • 协程和线程的区别

    万次阅读 2018-06-30 16:10:17
    协程(协同程序): 同一时间只能执行某个协程。开辟多个协程开销不大。协程适合对某任务进行分时处理。线程: 同一时间可以同时执行多个线程。开辟多条线程开销很大。线程适合多任务同时处理。1.协...
  • 3分钟了解C#中的协程

    万次阅读 2018-06-27 15:23:43
    协程:协同程序。协程的使用前提:直接或间接的继承MonoBehavior才可以使用。不足:会产生外的开销。协程方法与普通方法的区别:被调用时:普通方法被调用时,原来执行的部分保留现场,停止执行,然后去执行要调用的...
  • 协程的概念很早以前就被提出,很多语言也实现了协程,例如Erlang,Lua。不过我首次接触协程是在学习golang的过程中,当真正使用协程的时候就被它的优雅和高效所折服,这也埋下了自己对协程好奇的种子。随着接触许多...
  • Unity协程和线程的区别

    万次阅读 2017-02-21 11:52:02
    先简要说下结论: ...协程(协同程序): 同一时间只能执行某个协程。开辟多个协程开销不大。协程适合对某任务进行分时处理。 线程: 同一时间可以同时执行多个线程。开辟多条线程开销很大。线程适合多
  • C++协程及其原理

    千次阅读 2020-04-19 15:35:20
    C++协程及其原理 博客搬家,原地址:https://langzi989.github.io/2017/10/06/C++协程实现/ 协程的几种实现方式及原理 协程又可以称为用户线程,微线程,可以将其理解为单个进程或线程中的多个用户态线程,这些微线程...
  • golang限制协程数量

    万次阅读 2016-12-27 18:59:38
    虽然golang中协程开销很低,但是在一些情况下还是有必要限制一下协程的开启数,比如爬虫中的下载协程,因为受到带宽限制,开的多了也没有效果。本来想在网上找找有没协程池,类似其它语言线程池这样的东西,可以限制...
  • Golang 协程控制关闭

    千次阅读 2017-05-17 18:32:37
    部分代码参考:https://zhuanlan.zhihu.com/p/26695984   这边文章的的 package main import ( "context" "fmt" "time" ) func main() { ctx := context.Background() ctx, cancel := context.... go P
  • 协程与线程的区别

    万次阅读 2019-03-25 10:39:44
    概念  1、进程 进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。每个进程都有自己的独立内存空间,不同进程通过进程间通信来通信。...
  • Unity~协程IEnumerator(详解)

    千次阅读 2018-09-04 15:31:18
    using System.Collections; using System.Collections.Generic; using UnityEngine; public class XIeChengScript : MonoBehaviour { float timer = 0; void Start () { //开启协程 StartCo...
  • 关于go协程的调度和执行顺序

    千次阅读 2018-09-06 14:58:12
    无论是java还是go,开启线程和协程的时候,执行的顺序都是一样的。 例子1:   func funcB() {  println("funcB") } func funcA() {  println("funcA") }   func main() { funcA()...
  • Unity3D开发之协程

    千次阅读 2019-06-20 15:02:54
    协程,其实是unity做的一种类似线程的东西,但不是线程。协程是运行在主线程上,并不可以并行运算,因此他可以去处理逻辑问题。 一.打开协程及关闭协程 // Use this for initialization void Start() { ...
  • unity3d协程方法停止不了

    千次阅读 2015-07-23 19:10:56
    使用 StartCoroutine(DoSomething());...开启协程,发现使用 StopCoroutine(DoSomething()); 停止不了,看API原来是因为: 请注意只有StartCoroutine使用一个字符串方法名时才能用StopCoroutine停用之.
1 2 3 4 5 ... 20
收藏数 47,282
精华内容 18,912
关键字:

协程