协程
实现关键:栈、状态、上下文切换、调度器
协程(Coroutines)底层实现的原理通常涉及到以下四个关键概念:栈,状态,上下文切换和调度器。 这里我们主要讨论这些概念的基础知识,并不会过多涉及到特定编程语言的实现。
-
栈: 每个线程都有自己的栈,用于存储函数调用的上下文。相反,协程共享同一个线程,因此协程需要有自己的栈或者栈帧。这个栈或者栈帧用于在上下文切换时保存协程的状态。
-
状态: 每个协程维护它自己的运行状态。最基础的两个状态是“运行中”和“挂起”。协程能够在这两个状态之间自由切换。
-
上下文切换: 这是协程中一个最关键的概念。当一个协程挂起时,运行的控制权需要被移交给其他协程或者调度器。在这个过程中,需要保存当前协程的状态,并且恢复下一个协程的状态。这就叫做上下文切换。
-
调度器: 这是实现协程的另一个关键组件。当一个协程挂起时,需要有一个调度器来决定下一个要运行的协程是哪一个。
在实际实现中,协程的底层机制会和具体使用的编程语言和平台有关。例如,Python 的 asyncio 库就是通过生成器这一语言特性来实现协程的:生成器可以保存自己的状态,因此每一个生成器就可以被看作一个协程。当运行到 yield 表达式时,生成器会暂停,这就相当于协程的挂起。另外,asyncio 还提供了一个事件循环,可以作为协程的调度器,用来控制协程的执行顺序。
多态
开销:
空间消耗 --》虚函数表【数组,子类copy父类并覆盖重写函数】(虚函数个数*4bytes)、虚函数指针(4bytes)
时间消耗 --》寻找函数地址消耗
存储:
虚函数表(vtable)在静态存储区,虚函数指针在对象开始位置的4bytes
创建:
子类会copy父类的虚函数表,并用新地址覆盖重写函数地址
C++中的虚函数是用来实现运行时多态的主要机制。使用虚函数会带来一些开销,主要包括以下几个方面:
-
存储开销: 每一个含有虚函数的类(或者从含有虚函数的类派生出来的类)的对象在内存中都会有一些额外的存储开销。因为这些对象需要存储一个指向虚函数表(vtable,或虚函数指针表)的指针。虚函数表是一个存储指向类的虚函数的指针的数组。
-
内存开销: 虚函数表是静态的,它对于某个类的所有对象都是相同的。这意味着,即使你创建了很多某个类的对象,虚函数表只会有一份。但是,如果你有很多包含虚函数的类,那么每个类都有一份虚函数表,这就会占用一些内存。
-
时间开销: 如果一个函数被声明为虚函数,那么调用这个函数将可能需要额外的时间开销。因为调用虚函数需要通过虚函数表,并且可能需要进行一次间接寻址。在需要高性能的情况下,这可能成为一个问题。
-
二进制代码大小开销: 生成虚函数表会增加二进制代码的大小。
这些开销通常是可以接受的,因为虚函数提供了强大的功能:运行时多态。它们允许你写出更通用、更易于扩展的代码。然而,在某些性能敏感或资源受限的情况下,这些开销可能需要被考虑进去。因此,当设计一个类的时候,需要根据情况来决定是否需要使用虚函数。
C++虚函数表(vtable)是存储在对象的内存空间的,但这个位置不是作为对象的正常数据成员,而是隐藏的额外成员。每一个包含虚函数的类或者子类的对象,都会在内存布局的开始位置,保留一个指向虚函数表的指针,这个指针通常叫做vptr。可执行文件在执行时,它们的vtable放在静态存储区里,这样所有的对象都可以共享这些vtable。
例如,你创建一个派生对象(包含虚函数)的时候,这个对象在内存中的布局通常是这样的:
-
在对象内存布局的开始位置是一个指向虚函数表的指针(vptr)。
-
紧接着是派生类继承的基类的数据成员(如果有的话)。
-
然后是派生类自身的数据成员。
值得注意的是,C++的标准并没有指定虚函数表应该如何实现,这依赖于具体的编译器和对象模型。虚函数表的实现和存储位置可能因编译器和平台的不同而不同。所以,在跨平台开发,甚至是不同编译器间,对虚函数表的直接操作或假设,都可能导致程序的不兼容。
在C++中,当一个类(称为子类或派生类)从另一个类(称为基类)派生,并重写(覆盖)了基类的虚函数时,编译器会在子类的虚函数表(vtable)中为该函数生成一个新的条目。
基类的虚函数表中,每一个函数都有一个指向实现这个函数的代码的指针。当子类重写了这个函数,编译器会生成一个新的函数实现,并且在子类的虚函数表中,将这个函数的条目改为指向新的实现。
当你通过一个指向子类对象的基类指针调用这个虚函数时,运行时系统会根据存储在对象中的虚函数表指针(vptr)找到子类的虚函数表,然后调用表中对应函数的新的实现,实现运行时多态。