纤程又称协程,它是运行在线程之上的,一个线程可以包含多个纤程(只有一个纤程的纤程没有意义),多个纤程可以快速切换来执行不同的任务。因为纤程实际上是运行在线程之上的,所以一个线程上的多个纤程实际上也是顺序执行的,但纤程之间的切换很快,这样就好像是多个线程在执行一样。
纤程是在用户模式下实现的,内核对线程了如指掌,但对纤程却一无所知,内核会根据我们定义的算法来对纤程进行调度,线程一次只能执行一个纤程的代码,具体执行哪个纤程由我们来决定。
内核对于线程分配的花销比较大,而纤程的花销则比较小,而且线程切换会经历用户态、内核态的切换,速度较慢,纤程则是在用户态上即可完成切换,切换的上下文信息也很小,所以切换速度很快。又因为多个协程是在一个线程上执行的,所以协程之间没有锁机制,执行效率更高。
可以在一个线程里创建数万甚至数十万个纤程,就像数万次函数调用一样,而如果是线程的话就会产生大量的上下文切换花销,而且线程所需的资源比纤程多的多,一个进程里能创建的线程数量远远不如纤程多。
对于传统的“生产者-消费者模型”,一般使用多线程实现,这就不可避免的产生线程切换、全局资源的锁机制、休眠等待的一系列开销。为了提高效率,可以改用协程,如下所示为windows下使用协程的生产消费模型:
#include <windows.h>
#define MAIN 0
#define PRODUCER 1
#define CONSUMER 2
int data;
LPVOID aryFiberAddr[2];
// 生产者
void __stdcall producer(LPVOID param)
{
int counter = 0;
int count_sum = *(int*)param;
while (counter++ < count_sum)
{
Sleep(5000); // 每五秒生产一次数据
data = rand();
SwitchToFiber(aryFiberAddr[CONSUMER]); //切换到消费者纤程,直到消费纤程中调用SwitchToFiber()切换到生产者纤程,才会继续执行下面的代码
}
SwitchToFiber(aryFiberAddr[MAIN]); //生产结束,切换到主纤程
}
// 消费者
void __stdcall consumer(LPVOID param)
{
while (1)
{
int i = data;
SwitchToFiber(aryFiberAddr[PRODUCER]); // 完成数据消费后切换到生产者纤程,直到生产纤程中调用SwitchToFiber()切换到消费纤程,才会继续执行下面的代码
}
}
int main(int argc, char** argv)
{
aryFiberAddr[MAIN] = ConvertThreadToFiber(NULL); // 创建主纤程,只有纤程才能切换执行其他纤程
int taskCount = 1;
aryFiberAddr[PRODUCER] = CreateFiber(0, producer, &taskCount);// 创建生产者纤程
aryFiberAddr[CONSUMER] = CreateFiber(0, consumer, 0); // 创建消费者纤程
SwitchToFiber(aryFiberAddr[PRODUCER]); // 切换到生产者纤程,直到其它纤程里调用SwitchToFiber()切换到主纤程,下面的代码才会继续执行
// 纤程清理
DeleteFiber(aryFiberAddr[PRODUCER]);
DeleteFiber(aryFiberAddr[CONSUMER]);
//DeleteFiber(aryFiberAddr[MAIN]); //当前运行的光纤调用 DeleteFiber()的话,则其线程将调用 ExitThread
return 0;
}
多个纤程虽然是快速切换的,但是因为它们是在一个线程上运行的,所以它们实际上也是串行执行的,所以在协程中尽量不要调用阻塞IO的方法,因为这样会让整个线程进入阻塞状态,从而使其它纤程也得不到调度。
一个线程内的多个协程是串行执行的,不能利用多核,所以,协程不适合计算密集型的场景。而对于IO密集型的场景,如每秒10万次IO操作,可以使用协程配合多线程来进行处理。比如一个有10万连接的服务,有的连接没有数据可读,有的连接需要读取数据,那么就可以使用协程,当本纤程需要阻塞等待数据的时候,就可以切换到另一个纤程上去读取数据。
golang从编译器和语言基础库多个层面对协程做了实现,所以,go的协程是目前各类有协程概念的语言中实现的最完整和成熟的。go对各种IO函数进行了封装,而其内部是调用了操作系统的非阻塞或异步IO函数,当这些函数返回 busy 或 bloking 时,go利会将现有的执行序列压栈,让线程去拉另外一个协程的代码来执行,即从当前协程切换到另一个协程去执行。我们原来都是基于IO事件调度的异步编程来实现高性能,IO执行结果通过回调通知,程序的线性被打乱,如Node.js中的层层Callback。而Go中的协程实现了对 IO 事件的封装,通过语言级的支持让异步的代码看上去像同步执行的一样。
目前C++20中提供的协程比较偏底层,协程的实现依赖于几个关键的概念和关键字,如co_await、co_return、co_yield。我们可以使用async_simple这种封装好的简单易用的协程库来使用协程,关于这个库的更多可以参考这篇文章:C++20协程原理和应用。
&spm=1001.2101.3001.5002&articleId=125806499&d=1&t=3&u=9dec16933b60469f8a185228c54b8d95)
1830

被折叠的 条评论
为什么被折叠?



