协程实现单线程并发(入门)

简介: 协程实现单线程并发(入门)

协程源码:https://github.com/wuj1nquan/bitco ,每一行都有详细注释

进阶篇:

协程源码剖析进阶(一)

协程如何解决单线程并发?

首先作者尊重历史,协程的提出及最初实现者是Melvin Conway

先解释什么是协程:

协程(Coroutine)是一种计算机程序组件,它允许在特定的位置暂停执行,并在稍后恢复执行。

协程组件通常由以下几个主要部分组成:

  1. 协程对象(Coroutine Object):表示一个协程,它可以在特定的位置暂停执行,并在稍后恢复执行。协程对象通常由编程语言或框架提供的特殊语法或函数创建。
  2. 调度器(Scheduler):负责管理和调度多个协程的执行。调度器决定何时暂停一个协程并切换到另一个协程,以实现并发执行。
  3. 事件循环(Event Loop):是调度器的一种实现,它负责监控协程的状态并决定何时执行哪个协程。事件循环通常是异步编程中的核心组件,它驱动整个协程的执行过程。
  4. 协程间通信机制(Inter-coroutine Communication Mechanism):用于协程之间的通信和数据交换。这可以是通过共享变量、消息传递、管道等方式实现的。
  5. 异常处理机制(Exception Handling Mechanism):处理协程执行过程中可能出现的异常情况。异常处理机制可以捕获和处理协程中的异常,并根据需要采取适当的措施。

这些组件共同构成了协程的基本框架,使得程序员可以使用协程来编写高效、可维护的异步代码,实现并发执行和异步操作。不同的编程语言和框架可能提供不同的实现方式和特性,但通常都会包含类似的组件来支持协程。

而一个协程对象实际上就是一个支持被调度器调度的函数

本文讨论目前需要协程的原因:

在高并发的服务器请求下,我们想尽可能提高效率,当然可以通过多线程解决,但这是人傻钱多的举动,如果创建销毁一个线程只为了处理一个简单的请求,我们觉得这样很不划算,甚至是一种性能浪费。

答案:单线程处理多任务

这可能吗?单线程一次只能处理一个任务,同时处理多个任务不是并发吗,确定没搞错成多线程和多进程吗?

首先,没搞错,我们的目的是用单线程处理多任务,但不是同时,考虑操作系统是如何实现并发的,单个cpu上多个进程也不是同时运行的,而是时间分片为我们造成了多个进程同时运行的假象。

解决方案:模拟时间分片

先来看看没有协程的单线程中多任务是如何处理的(伪代码)

↓ 时间线

任务1

send(request); //客户端发送请求 1

等待服务端回复; time...

recv();//接收数据

任务2

send(request); //客户端发送请求 2

等待服务端回复; time...

recv();//接收数据

任务3

send(request); //客户端发送请求 3

等待服务端回复; time...

recv();//接收数据

重点在这里!!!

可以看到这三个请求是按顺序依次执行的,任务二需要等待任务一执行完后开始执行,然而任务一在运行时也需要等待,这样一来,不只是任务一在等待time,而是三个任务都在等待任务一的time,这时整个线程都在等待,什么都没有做!

就像你需要烧水,洗衣服,做饭。先打水、烧水然后什么也不干,等着水烧开再去洗衣服!

第二个例子我们都会解决:先烧上水,然后去开洗衣机、再去做饭,做饭时找空闲时间,每隔几分钟看看水烧开了没有,衣服洗好了没有

所以,单线程能不能也这样做呢?

答案是肯定的,我们需要在任务一等待时切换到任务二...切换回任务一... 如此一来,我们避免了大量的等待时间!

核心问题:

如何实现任务切换(调度器)?

  • 利用 glibc 的 ucontext 组件(使用起来最简单)
  • 使用汇编代码来切换上下文(效率最高)
  • 利用 C 语言的 setjmp 和 longjmp(可移植性最好)

实现任务切换

这里用ucontext举例说明(因为我不会汇编0.0)

// ucontext_t结构体 用于保存线程的执行状态(上下文)
ucontext_t ctx[3];
ucontext_t main_ctx;
int count = 0;
// coroutine 1
void func1(void) {
    while(count++ < 30) {
        printf("1\n");
        swapcontext(&ctx[0], &main_ctx);// context switch
         printf("2\n");
    }
}
// coroutine 2
void func2(void) {
    while(count++ < 30) {
        printf("3\n");
        swapcontext(&ctx[1], &main_ctx);// context switch
        printf("4\n");
    }
}
//coroutine 3
void func3(void) {
    while(count++ < 30) {
        printf("5\n");
        swapcontext(&ctx[2], &main_ctx);// context switch
        printf("6\n");
    }
}
// schedule
int main() {
    // 每个上下文的栈空间
    char stack1[STACK_SIZE] = {0};
    char stack2[STACK_SIZE] = {0};
    char stack3[STACK_SIZE] = {0};
    // getcontext初始化结构体全部属性
    getcontext(&ctx[0]);// 将当前线程的执行状态保存到ctx[0]结构体中
    // 自定义设置结构体部分属性
    ctx[0].uc_stack.ss_sp = stack1;
        ctx[0].uc_stack.ss_size = sizeof(stack1);
        ctx[0].uc_link = &main_ctx;
        makecontext(&ctx[0], func1, 0); //修改ctx[0]的上下文为func1的上下文
    getcontext(&ctx[1]);
    ctx[1].uc_stack.ss_sp = stack2;
    ctx[1].uc_stack.ss_size = sizeof(stack2);
    ctx[1].uc_link = &main_ctx;
    makecontext(&ctx[1], func2, 0);
    
    getcontext(&ctx[2]);
    ctx[2].uc_stack.ss_sp = stack3;
    ctx[2].uc_stack.ss_size = sizeof(stack3);
    ctx[2].uc_link = &main_ctx;
    makecontext(&ctx[2], func3, 0);
    printf("swapcontext\n");
    while (count <= 30) {  // scheduler
        swapcontext(&main_ctx, &ctx[count%3]);
        // 这里的main_ctx未被初始化,仅用作其他上下文之间的调度
    }
    printf("\n");
    return 0;
}

部分运行结果:

swapcontext 1 3 5 2 1 4 3 6 5 2 1 4 3 6 5 2 1

目录
相关文章
|
1天前
|
缓存 并行计算 安全
【并发编程系列一】并发编年史:线程的双刃剑——从优势到风险的全面解析
【并发编程系列一】并发编年史:线程的双刃剑——从优势到风险的全面解析
|
2天前
|
安全 Java
java线程之List集合并发安全问题及解决方案
java线程之List集合并发安全问题及解决方案
8 1
|
3天前
|
Java
synchronized关键字在Java中为多线程编程提供了一种简便的方式来管理并发,防止数据竞争和死锁等问题
Java的`synchronized`关键字确保多线程环境中的数据一致性,通过锁定代码段或方法防止并发冲突。它可修饰方法(整个方法为临界区)或代码块(指定对象锁)。例如,同步方法只允许一个线程执行,同步代码块则更灵活,可锁定特定对象。使用时需谨慎,以避免性能影响和死锁。
9 0
|
3天前
|
Java
Java中的`synchronized`关键字是一个用于并发控制的关键字,它提供了一种简单的加锁机制来确保多线程环境下的数据一致性。
【6月更文挑战第24天】Java的`synchronized`关键字确保多线程数据一致性,通过锁定代码块或方法防止并发冲突。同步方法整个方法体为临界区,同步代码块则锁定特定对象。示例展示了如何在`Counter`类中使用`synchronized`保证原子操作和可见性,同时指出过度使用可能影响性能。
18 4
|
6天前
|
Java
Java Socket编程与多线程:提升客户端-服务器通信的并发性能
【6月更文挑战第21天】Java网络编程中,Socket结合多线程提升并发性能,服务器对每个客户端连接启动新线程处理,如示例所示,实现每个客户端的独立操作。多线程利用多核处理器能力,避免串行等待,提升响应速度。防止死锁需减少共享资源,统一锁定顺序,使用超时和重试策略。使用synchronized、ReentrantLock等维持数据一致性。多线程带来性能提升的同时,也伴随复杂性和挑战。
|
7天前
|
Java 开发者
JAVA多线程通信入门:wait()、notify()、notifyAll()大揭秘!
【6月更文挑战第20天】Java多线程中,`wait()`, `notify()`, `notifyAll()`是Object类的关键通信方法。`wait()`让线程等待并释放锁,`notify()`随机唤醒一个等待的线程,`notifyAll()`唤醒所有。示例展示了在共享资源类中如何使用它们来协调生产者消费者线程。调用前需持有锁,否则抛异常。注意避免死锁和活锁,恰当使用这些方法至关重要。
|
8天前
|
Java 开发者
告别单线程时代!Java 多线程入门:选继承 Thread 还是 Runnable?
【6月更文挑战第19天】在Java中,面对多任务需求时,开发者可以选择继承`Thread`或实现`Runnable`接口来创建线程。`Thread`继承直接但限制了单继承,而`Runnable`接口提供多实现的灵活性和资源共享。多线程能提升CPU利用率,适用于并发处理和提高响应速度,如在网络服务器中并发处理请求,增强程序性能。不论是选择哪种方式,都是迈向高效编程的重要一步。
|
10天前
|
数据挖掘 调度 开发者
Python并发编程的艺术:掌握线程、进程与协程的同步技巧
并发编程在Python中涵盖线程、进程和协程,用于优化IO操作和响应速度。`threading`模块支持线程,`multiprocessing`处理进程,而`asyncio`则用于协程。线程通过Lock和Condition Objects同步,进程使用Queue和Pipe通信。协程利用异步事件循环避免上下文切换。了解并发模型及同步技术是提升Python应用性能的关键。
37 5
|
11天前
|
Java 开发者 计算机视觉
探索Python中的并发编程:线程与协程
本文将深入探讨Python中的并发编程,重点比较线程和协程的工作机制、优缺点及其适用场景,帮助开发者在实际项目中做出更明智的选择。
|
14天前
|
存储 Java 调度
Android面试题之Kotlin协程到底是什么?它是线程吗?
本文探讨了协程与线程的区别,指出协程并非线程,而是轻量级的线程替代。协程轻量体现在它们共享调用栈,内存占用少,仅需几个KB。协程切换发生在用户态,避免了昂贵的内核态切换。在Kotlin中,协程通过Continuation对象实现上下文保存,允许高效并发执行,而不会像线程那样消耗大量资源。通过`runBlocking`和`launch`示例展示了协程的非阻塞挂起特性。总结来说,协程的轻量主要源于内存占用少、切换开销低和高并发能力。
18 0