C++ 协程(1):函数和协程

C++ 协程(1):函数和协程 这篇文章的目的是探究 C++ 中协程的机制和用法,以及怎样利用协程的特性来构建上层的库和应用。 1. 栈帧和函数 栈帧是一个函数执行的环境,包括函数参数、函数返回地址、局部变量等信息。操作系统每次调用一个函数,都会为其分配一个新的栈帧,相关的概念有: ESP:栈指针寄存器(Extended Stack Pointer),其内存中存放一个始终指向系统栈最顶部栈帧栈顶的指针 EBP:基址指针寄存器(Extended Base Pointer),其内存中存放一个始终指向系统最顶部栈帧栈底的指针 函数栈帧:ESP和EBP之间的内存空间为当前栈帧,EBP标识了当前栈帧的底部,ESP标识了当前栈帧的顶部 对于普通的函数来说,一般我们可以对其进行两种操作:call(调用)和 return(返回)。为了方便对比,此处不讨论 throw exception 的情况。在运行一个 C++ 程序时,编译器会先执行 C++ runtime,然后会调用 main 函数,再由 main 函数调用其他的函数。 call 操作一般包含以下几个步骤: 参数入栈:参数从右向左依次入栈 返回地址入栈:将当前代码区的下一条待执行的指令入栈,以便在函数 return 之后执行 代码区跳转:处理器跳转到被调函数的入口 栈帧调整,包括: 保存当前栈帧状态值,EBP 入栈 从当前栈帧切换到新的栈帧,更新 EBP,将 EBP 的值设置为 ESP 的值 给新的栈帧分配内存空间,更新 ESP,将 ESP 的值减去所需空间的大小 当一个函数通过 return 语句返回时,执行的步骤与调用时相反: 2. 协程 协程由程序所控制,即在用户态执行,而不是像线程一样由操作系统内核管理,使用协程时,不需要如线程一般频繁地进行上下文切换,性能能够得到很大的提升,因此协程的开销远远小于线程的开销。一般来说协程有三种特性: suspend 悬停:暂停当前协程的执行,将执行权交还给调用者,但是保留当前栈帧。和函数的 return 类似,协程的 suspend 只能由协程自身发起 resume 恢复:继续执行已经 suspend 的协程,重新激活协程的栈帧 destroy 销毁:销毁协程的栈帧和其对应的内存 可以看到,协程可以在不清除栈帧的情况下被挂起而不被销毁,因此我们不能够使用调用栈这样的数据结构来严格保证活动栈帧的生命周期,我们可以把协程存储在堆中。我们可以把协程的栈帧分为两部分,一部分是执行栈帧,这部分仅在当前协程执行期间存在,在执行结束,即协程 suspend 的时候被释放;另一部分是数据栈帧,这部分即使在协程 suspend 的时候依然存在。 ...

January 20, 2020 · 2 min

C++ 智能指针(3):shared_ptr

C++智能指针(3):shared_ptr 分析 UniquePointer对象只能绑定单个指针,要实现指针的自动管理和销毁需要引入计数器 private: int *counter; T *pointer; D *deleter; 计数器的主要作用是标识当前指针被几个智能指针对象所引用,在析构当前对象时,使其计数器自减1。如果计数器等于0,则表示已经没有其他的对象在使用当前指针,此时则可以销毁指针,计数器和删除器。 template<typename T, typename D> void SharedPointer<T, D>::release() { if (pointer) { std::cout << "SharedPointer " << this << " counter remains " << *counter << std::endl; if (--(*counter) == 0) { std::cout << "SharedPointer " << this << " destructor called." << std::endl; (*deleter)(pointer); (*deleter)(counter); (*deleter)(deleter); pointer = nullptr; counter = nullptr; deleter = nullptr; } } } reset函数将指针设为other的指针 template<typename T, typename D> void SharedPointer<T, D>::reset(const SharedPointer<T, D> &other) { pointer = other.pointer; counter = other.counter; deleter = other.deleter; if (pointer) ++(*counter); } 析构函数可以直接调用release函数 ...

January 25, 2019 · 7 min

C++ 智能指针(2):unique_ptr

C++智能指针(2):unique_ptr 分析 在使用 AutoPointer 的时候会发生所有权转移和内存泄漏的问题,所以我们可以对 AutoPointer 类稍加修改,修复这两个问题。 所有权转移 为了规避可能发生所有权转移的情况,我们可以直接禁止它使用拷贝构造函数和赋值操作符。 UniquePointer(UniquePointer<T> &other) = delete; UniquePointer<T> &operator=(const UniquePointer<T> &other) = delete; 但很多时候我们都需要使用到传递指针的操作,如果只是使用 deleted 函数禁止拷贝构造函数和赋值操作符,那么这个智能指针存在的意义就不大了,我们可以通过 move 语义来实现移动构造函数和移动赋值操作符,从而在使用 UniquePointer 的时候可以在特定情况下进行所有权转移。 UniquePointer(UniquePointer<T> &&other) noexcept; UniquePointer &operator=(UniquePointer &&other) noexcept; 内存泄漏 为了防止发生内存泄漏,我们可以在UniquePointer的私有成员中增加一个删除器,并根据当前指针对象的类型指定删除器,从而防止发生内存泄漏。 class Deleter { template<typename T> void operator()(T *p) { if (p) delete p; } }; template<typename T, typename D> class UniquePointer { ... private: T *pointer; Deleter deleter; }; 实现 根据unique_ptr的源码,能够大致实现UniquePointer类 template<typename T, typename D> class UniquePointer { public: explicit UniquePointer(T *t, const D &d); ~UniquePointer(); T &operator*(); T *operator->(); T *release(); void reset(T *p); UniquePointer(UniquePointer &&other) noexcept; UniquePointer &operator=(UniquePointer &&other) noexcept; UniquePointer(const UniquePointer &other) = delete; UniquePointer &operator=(const UniquePointer &other) = delete; private: T *pointer; D deleter; }; template<typename T, typename D> UniquePointer<T, D>::UniquePointer(T *t, const D &d) { std::cout << "UniquePointer " << this << " constructor called." << std::endl; this->pointer = t; this->deleter = d; } template<typename T, typename D> UniquePointer<T, D>::~UniquePointer() { std::cout << "UniquePointer " << this << " destructor called." << std::endl; deleter(this->pointer); } template<typename T, typename D> T &UniquePointer<T, D>::operator*() { return *this->pointer; } template<typename T, typename D> T *UniquePointer<T, D>::operator->() { return this->pointer; } template<typename T, typename D> T *UniquePointer<T, D>::release() { T *new_pointer = this->pointer; this->pointer = nullptr; return new_pointer; } template<typename T, typename D> void UniquePointer<T, D>::reset(T *p) { if (this->pointer != p) { deleter(this->pointer); this->pointer = p; } } template<typename T, typename D> UniquePointer<T, D>::UniquePointer(UniquePointer<T, D> &&other) noexcept { std::cout << "UniquePointer " << this << " move constructor called." << std::endl; this->pointer = other.release(); deleter(std::move(other.deleter)); } template<typename T, typename D> UniquePointer<T, D> &UniquePointer<T, D>::operator=(UniquePointer<T, D> &&other) noexcept { std::cout << "UniquePointer " << this << " assignment operator called." << std::endl; if (this->pointer != other.pointer) { reset(other.release()); deleter = std::move(other.deleter); } return *this; } 测试 尝试使用移动构造函数 ...

January 19, 2019 · 6 min

C++ 智能指针(1.5):move 语义

C++智能指针(1.5):move语义 move语义 定义 右值引用(Rvalue Referene)是 C++ 11中引入的新特性,它实现了转移语义(Move Sementics)和精确传递(Perfect Forwarding),其主要目的有 消除两个对象交互时不必要的对象拷贝,节省运算存储资源,提高效率。 能够更简洁明确地定义泛型函数。 实现 move 语义的实现非常简单,它将传入的参数 _Tp&& __t 使用静态类型转换 static_cast<_Up&&>(__t) 转变成了成了对应类型的右值,也就是说使用 move 语义之后,编译器窃取(一般会在移动构造函数和移动赋值操作符里将原有对象指向 nullptr)了原有对象的右值,并延长了这个右值的生命周期并将其用来赋值给其他的对象,而没有对右值做任何拷贝操作。 template <class _Tp> typename remove_reference<_Tp>::type&& move(_Tp&& __t) _NOEXCEPT { typedef typename remove_reference<_Tp>::type _Up; return static_cast<_Up&&>(__t); } 测试 定义一个 Object 类和一个 MoveObject 函数使用 move 语义返回一个 Object 的类对象,可以看到在 MoveObject 函数返回右值后,obj 对象调用了移动构造函数。 class Object { public: Object() { std::cout << "Construct" << std::endl; } Object(const Object &other) { std::cout << "Copy" << std::endl; } Object(Object &&other) noexcept { std::cout << "Move" << std::endl; } ~Object() { std::cout << "Destruct" << std::endl; } void Print() { std::cout << "Print" << std::endl; } }; Object MoveObject() { Object obj; return move(obj); } int main() { Object obj = MoveObject(); return 0; } /* output: Construct Move Destruct Destruct */ 返回值优化(RVO,Return value optimisation) 返回值优化是一种编译器优化技术,允许编译器在调用点(call site)直接构造函数的返回值。 ...

January 2, 2019 · 2 min

C++ 智能指针(1):auto_ptr

C++智能指针(1):auto_ptr 分析 C++ 中经常会出现因为没有 delete 指针而造成的内存泄漏,例如有一个 Object 类 class Object { public: Object() { std::cout << "Construct" << std::endl; } Object(const Object &other) { std::cout << "Copy" << std::endl; } Object(Object &&other) noexcept { std::cout << "Move" << std::endl; } ~Object() { std::cout << "Destruct" << std::endl; } void Print() { std::cout << "Print" << std::endl; } }; 创建一个指向 Object 类型的指针 int main() { Object *o = new Object(); o->Print(); return 0; } /* output: Construct Print */ 我们没有进行delete o的操作,导致o没有被正确地析构,造成了内存泄漏。作为对比,创建一个Obj类型的对象 int main() { Object *o1 = new Object(); o1->Print(); Object o2 = Object(); o2.Print(); return 0; } /* output: Construct Print Construct Print Destruct */ 产生这样的结果是因为对象创建在栈(stack)上,编译器会自动进行对象的创建和销毁,而指针是创建在堆(heap)上,需要手动进行创建和销毁。为了规避这样的问题,我们可以封装一个智能指针类,用类来管理指针,防止造成内存泄漏,并且尽可能的模仿指针的用法。 ...

December 27, 2018 · 4 min