问小白 wenxiaobai
资讯
历史
科技
环境与自然
成长
游戏
财经
文学与艺术
美食
健康
家居
文化
情感
汽车
三农
军事
旅行
运动
教育
生活
星座命理

什么是协程?协程和线程的区别

创作时间:
作者:
@小白创作中心

什么是协程?协程和线程的区别

引用
CSDN
1.
https://m.blog.csdn.net/qq_35578171/article/details/140106944

协程和线程是并发编程中常用的两种技术,它们在实现方式、性能特点和适用场景上存在显著差异。本文将从基础知识开始,逐步深入探讨协程的概念、实现原理以及与线程的主要区别,帮助读者更好地理解这两种并发模型的特点和应用场景。

前置知识

在了解协程之前,我们需要先理解一些相关的基本知识。

应用程序和内核

内核具有最高权限,可以访问受保护的内存空间,可以访问底层的硬件设备。而这些是应用程序所不具备的,但应用程序可以通过调用内核提供的接口来间接访问或操作。

以一次网络 IO 操作为例,请求的数据会先被拷贝到系统内核的缓冲区(内核空间),然后再从内核缓冲区拷贝到应用程序的地址空间(用户空间)。这个过程包括两个阶段:

  1. 等待数据准备:数据从网络接口读取并放入内核缓冲区。
  2. 拷贝数据:数据从内核缓冲区复制到应用程序的用户空间。

阻塞和非阻塞

从上面我们可以清楚地知道,一次 IO 操作分为两步:

  • 等待数据准备
  • 拷贝数据

如果等待数据准备过程是阻塞的,则我们称为阻塞操作;如果不必等待数据准备完成,而是返回是否就绪标志,则称为非阻塞。

同步和异步

用户线程发起 IO 操作,阻塞等待 IO 操作完成,则操作是同步的;如果用户发起 IO 操作,不必等待操作完成,等待内核完成 IO 操作后通知用户线程,则为异步,如常见的 aio_read 函数。

并发和并行

  • 并发(concurrency):逻辑上具备同时处理多个任务的能力。
  • 并行(parallesim):物理上在同一时刻执行多个并发任务,依赖多核处理器等物理设备。

IO 发展历史

在没有协程的时代,处理 IO 操作我们一般使用下面三种方式:

同步编程

应用程序阻塞等待IO结果(比如等待打开一个大的文件,或者等待远端服务器的响应)。

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;

public class SynchronousIO {
    public static void main(String[] args) {
        try (BufferedReader reader = new BufferedReader(new FileReader("example.txt"))) {
            String line;
            while ((line = reader.readLine()) != null) {
                System.out.println(line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

优点:编程简单,方便理解
缺点:阻塞读取,效率低下,与 IO 无关的操作也需要等待 IO 完成

异步多线程/进程

将IO操作频繁的逻辑、或者单纯的IO操作独立到一/多个线程中,业务线程与IO线程间靠通信/全局变量来共享数据。

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class AsynchronousMultiThreadIO {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(4);
        executor.submit(() -> {
            try (BufferedReader reader = new BufferedReader(new FileReader("example.txt"))) {
                String line;
                while ((line = reader.readLine()) != null) {
                    System.out.println(line);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        });
        executor.shutdown();
    }
}

优点:多线程处理,提高系统响应速度;充分利用 CPU 资源、避免阻塞其它业务
缺点:上下文切换成本较高,编程复杂度较高,需要管理大量线程

异步消息 + 回调函数(响应式编程)

在响应式编程中,IO 操作是非阻塞的,并且通过回调函数来处理结果。

示例代码(使用 CompletableFuture):

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.util.concurrent.CompletableFuture;

public class AsynchronousCallbackIO {
    public static void main(String[] args) {
        CompletableFuture.runAsync(() -> {
            try (BufferedReader reader = new BufferedReader(new FileReader("example.txt"))) {
                String line;
                while ((line = reader.readLine()) != null) {
                    System.out.println(line);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }).thenRun(() -> System.out.println("File reading completed."));
    }
}

示例代码(使用 Reactor):

import reactor.core.publisher.Mono;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

public class ReactiveIO {
    public static void main(String[] args) {
        Path path = Paths.get("example.txt");
        Mono.fromCallable(() -> Files.readString(path))
            .subscribe(content -> System.out.println(content),
                       error -> error.printStackTrace(),
                       () -> System.out.println("File reading completed."));
    }
}

优点:

  1. 相比多线程 IO,响应式编程的资源开销更低,能够更好地利用系统资源
  2. 响应式编程模型适合处理高并发、高吞吐量的应用,便于扩展和维护

缺点:

  1. 学习曲线陡峭:响应式编程需要理解异步编程和回调机制,对于初学者来说可能比较困难
  2. 调试复杂:由于异步操作的非顺序执行,调试和错误处理变得更加复杂

协程

协程基本概念

维基百科定义:

Coroutines are computer program components that generalize subroutines for non-preemptive multitasking, by allowing multiple entry points for suspending and resuming execution at certain locations. Coroutines are well-suited for implementing more familiar program components such as cooperative tasks, exceptions, event loops, iterators, infinite lists and pipes.

中文翻译:

协程是一种计算机程序组件,它通过允许多个入口点在特定位置暂停和恢复执行,将非抢占式多任务的子程序进行了一般化。协程非常适合实现更熟悉的程序组件,如协作任务、异常、事件循环、迭代器、无限列表和管道。

简而言之:

协程(Goroutines)是一种轻量级的并发编程模型,由编程语言或运行时环境管理,用于执行并发任务。与传统的操作系统线程相比,协程更轻量级,切换开销更小,因此在高并发场景中非常高效。协程在许多现代编程语言中都有实现,包括 Go、Python、JavaScript(在某种程度上通过异步函数和生成器)等。

协程从一定程度来讲,可以说是“用同步的语义解决异步问题”,即业务逻辑看起来是同步的,但实际上并不阻塞当前线程(一般是靠事件循环处理来分发消息)。

Go 示例代码

下面是一个使用 Go 协程协作的示例,这个示例展示了如何使用 sync.WaitGroup 和 channel 来实现协程之间的协作:

package main

import (
    "fmt"
    "sync"
    "time"
)

// 定义一个 WaitGroup 以等待所有协程完成
var wg sync.WaitGroup

// 定义两个 channel 用于协程间的通信
var ch1 = make(chan int)
var ch2 = make(chan int)

func worker1() {
    defer wg.Done() // 在函数结束时减少 WaitGroup 计数
    for i := 0; i < 5; i++ {
        fmt.Println("Worker 1: Sending", i)
        ch1 <- i // 将数据发送到 ch1
        time.Sleep(500 * time.Millisecond)
    }
    close(ch1) // 关闭 channel,通知 worker2 没有更多数据
}

func worker2() {
    defer wg.Done() // 在函数结束时减少 WaitGroup 计数
    for {
        val, ok := <-ch1 // 从 ch1 接收数据
        if !ok {
            break // 如果 ch1 已关闭,退出循环
        }
        fmt.Println("Worker 2: Received", val)
        fmt.Println("Worker 2: Sending", val*val)
        ch2 <- val * val // 将数据发送到 ch2
        time.Sleep(500 * time.Millisecond)
    }
    close(ch2) // 关闭 channel,通知 main 没有更多数据
}

func main() {
    // 启动 worker1 和 worker2 协程
    wg.Add(2)
    go worker1()
    go worker2()

    // 在主协程中从 ch2 接收数据
    go func() {
        for val := range ch2 {
            fmt.Println("Main: Received", val)
        }
    }()

    wg.Wait() // 等待所有 worker 协程完成
}

协程和线程的区别

  • 协程属于用户级线程,线程属于内核级线程,线程的创建、上下文切换远比协程消耗更大。
  • 协程属于非抢占式,不会被其它协程所抢占,而是由开发者自己调度;线程属于抢占式,受到操作系统调度。
  • 协程的编码相比与多线程的编码更加复杂,但是协程大多数场景下更适合大并发任务。
© 2023 北京元石科技有限公司 ◎ 京公网安备 11010802042949号