【并发编程】多路复用_进程_并发&并行_僵尸、孤儿、守护进程_进程锁_多线程_协程

wx-x / 2024-01-23 / 原文

【一】操作系统及其作用

操作系统是一种系统软件,它是计算机硬件和应用软件之间的桥梁。它主要负责管理和控制计算机系统的各种资源,并提供给应用程序一个简单、一致的接口来访问这些资源。操作系统的主要目标是提供一个方便、高效、可靠和安全的计算环境。

操作系统解决了以下几个重要问题:

  1. 资源管理:操作系统负责管理计算机系统的各种资源,包括处理器、内存、存储设备、输入输出设备等。它通过资源分配、调度和回收等机制,合理地利用和管理这些资源,以满足不同应用程序的需求。

  2. 进程管理:操作系统通过进程管理来实现多任务处理。它允许多个程序同时运行,并负责调度和切换这些程序的执行。操作系统为每个程序创建一个独立的进程,并提供进程间通信和同步机制,以实现程序之间的协作和数据交换。

  3. 内存管理:操作系统负责管理计算机系统的内存资源。它分配和回收内存空间,将程序加载到内存中,并提供内存保护和虚拟内存等机制。内存管理使得多个程序可以同时存在于内存中,并共享内存资源。

  4. 文件系统:操作系统提供文件系统来管理计算机系统的存储设备。文件系统负责文件的创建、读写、删除和保护等操作,以及文件的组织和存储管理。它为应用程序提供了访问和操作文件的接口。

  5. 设备管理:操作系统负责管理计算机系统的输入输出设备。它通过设备驱动程序控制和管理各种设备,提供设备的抽象和统一接口,使得应用程序可以方便地使用设备进行输入输出操作。

  6. 用户界面:操作系统提供用户界面,使得用户可以与计算机系统进行交互。用户界面可以是命令行界面、图形界面或其他形式的界面,它为用户提供了操作系统和应用程序的控制和访问方式。

【二】多路复用在操作系统中的作用

多路复用(Multiplexing)是一种技术,允许单个进程或线程同时监视和处理多个输入和输出通道(如文件描述符或套接字),以提高系统的效率和响应性能。它可以通过一种机制来同时处理多个通道的输入和输出,从而实现并发处理。

多路复用在计算机领域中有以下几个主要作用:

  1. 提高系统效率:多路复用允许单个进程或线程同时监视和处理多个输入和输出通道,避免了为每个通道创建独立的进程或线程的开销。这样可以减少系统资源的占用,提高系统的效率。

  2. 实现并发处理:通过多路复用,可以同时处理多个输入和输出通道的请求,实现并发处理。在网络编程中,可以同时处理多个客户端连接请求,提高服务器的并发性能。

  3. 减少上下文切换:使用多路复用可以减少进程或线程之间的上下文切换次数。相比于为每个通道创建独立的进程或线程,多路复用可以通过一个进程或线程来处理多个通道的请求,减少了上下文切换的开销,提高了系统的响应性能。

  4. 简化编程模型:多路复用提供了一种方便的编程模型,使得程序员可以同时处理多个通道的输入和输出。通过使用多路复用机制,可以将关注点集中在就绪通道的处理上,而不需要关注每个通道的具体细节,简化了编程任务。

总的来说,多路复用可以提高系统的效率、并发性能和响应性能,减少资源占用和上下文切换开销,同时简化编程模型。它在网络编程中广泛应用,用于处理多个客户端连接请求,提高服务器的并发处理能力。

(2.1)多路复用的实现方式

  • select:select是一种基于轮询的多路复用机制,通过一个集合来监视多个文件描述符的状态变化,并在有事件发生时通知应用程序。
  • poll:poll是一种改进的多路复用机制,与select类似,但使用了更高效的数据结构来管理文件描述符集合。
  • epoll:epoll是Linux特有的多路复用机制,通过事件驱动的方式,使用红黑树来管理文件描述符集合,具有更好的性能和扩展性。

这些多路复用机制可以与阻塞和非阻塞 I/O 模式结合使用。在阻塞模式下,当没有就绪的文件描述符时,进程或线程会被阻塞,直到有文件描述符就绪。而在非阻塞模式下,进程或线程会立即返回,不管文件描述符是否就绪,从而可以继续处理其他任务。

(2.2)多路复用的应用场景

多路复用在计算机领域有广泛的应用场景,特别是在网络编程中。以下是一些常见的多路复用应用场景:

  1. 网络服务器:多路复用可以用于处理网络服务器中的多个客户端连接请求。服务器可以使用多路复用机制同时监视多个套接字,以便在有数据可读或可写时立即处理。这样可以提高服务器的并发性能,同时减少资源开销。

  2. 并发下载器:在下载大文件时,可以使用多路复用来实现并发下载。通过同时监视多个下载通道,可以并发地下载不同的文件块,提高下载速度。

  3. 聊天程序:多路复用可以用于实现即时聊天程序,允许多个用户同时进行聊天。服务器可以使用多路复用机制来处理多个客户端的消息,以便实时地转发消息给其他用户。

  4. 实时数据处理:多路复用可以用于实时数据处理应用,如传感器数据采集和实时监控系统。通过同时监视多个数据源,可以实时地处理和分析数据,以便进行实时决策和响应。

  5. 虚拟化技术:在虚拟化环境中,多路复用可以用于处理虚拟机之间的通信。通过使用多路复用机制,可以在宿主机上同时处理多个虚拟机的网络请求,提高虚拟化环境的性能和效率。

总之,多路复用在网络编程和并发处理中有广泛的应用。它可以提高系统的并发性能和响应性能,减少资源开销,同时简化编程模型。多路复用适用于各种需要同时处理多个输入和输出通道的场景。

(2.3)阻塞与非阻塞模式与多路复用结合

阻塞和非阻塞是指在进行 I/O 操作时,进程或线程的行为方式。

  1. 阻塞模式(Blocking):在阻塞模式下,当进程或线程执行 I/O 操作时,如果数据没有准备好或者无法立即进行读取或写入,那么进程或线程会被阻塞,暂停执行,直到数据准备好或者操作完成。在阻塞模式下,I/O 操作是同步的,进程或线程需要等待操作完成才能继续执行后续的代码。阻塞模式会导致进程或线程的等待时间增加,降低系统的并发性能。

  2. 非阻塞模式(Non-blocking):在非阻塞模式下,当进程或线程执行 I/O 操作时,如果数据没有准备好或者无法立即进行读取或写入,进程或线程不会被阻塞,而是立即返回一个错误或者特殊的值,让进程或线程可以继续执行其他任务。在非阻塞模式下,I/O 操作是非同步的,进程或线程可以立即返回并继续执行后续的代码。非阻塞模式可以提高系统的并发性能,但需要进程或线程主动轮询或使用其他机制来检查数据是否准备好。

阻塞和非阻塞模式可以与多路复用结合使用:

  • 阻塞模式与多路复用:在阻塞模式下,可以使用多路复用来同时监视多个通道,并在有数据可读或可写时进行阻塞式的 I/O 操作。这样可以在等待数据准备好时阻塞进程或线程,避免了忙等待的开销。

  • 非阻塞模式与多路复用:在非阻塞模式下,可以使用多路复用来同时监视多个通道,并在有数据可读或可写时进行非阻塞式的 I/O 操作。进程或线程可以通过轮询多路复用的结果,判断哪些通道有数据可读或可写,然后进行相应的处理。

选择使用阻塞模式还是非阻塞模式取决于具体的应用需求。阻塞模式简单直观,但可能会导致资源浪费和响应性能下降。非阻塞模式可以提高系统的并发性能,但需要适当的编程技巧来处理非阻塞 I/O 操作的结果。

(2.4)文件操作符

文件描述符(File Descriptor)是操作系统中用于标识和访问文件或其他输入/输出资源的抽象概念。在大多数操作系统中,文件描述符是一个非负整数,它代表了一个打开的文件或其他类型的 I/O 资源。

文件描述符的主要作用是通过操作系统提供的系统调用来对文件或其他 I/O 资源进行读取、写入、关闭等操作。每个进程在其打开的文件或 I/O 资源上都会有一组文件描述符,它们是唯一标识这些资源的方式。

在 POSIX 标准中,通常将文件描述符的值定义为整数常量,如 0、1 和 2,分别表示标准输入、标准输出和标准错误。其他的文件描述符则通过系统调用(如 open())获得。

文件描述符的具体值和操作方式可能因操作系统而异,但通常遵循以下规则:

  • 0 是标准输入(stdin)的文件描述符。
  • 1 是标准输出(stdout)的文件描述符。
  • 2 是标准错误(stderr)的文件描述符。
  • 大于 2 的文件描述符通常由应用程序打开和使用,用于访问其他文件或 I/O 资源。

在编程中,可以使用文件描述符来操作文件或其他 I/O 资源。例如,可以使用文件描述符进行读取、写入、关闭等操作。操作系统提供的系统调用(如 read()、write()、close())通常需要传递文件描述符作为参数来指定要操作的资源。

需要注意的是,文件描述符是进程级别的,不同进程的文件描述符可能相同,但代表的是不同的资源。此外,文件描述符也可以用于表示网络套接字、管道、设备文件等非文件资源。

(2.4.1)文件描述符与多路复用的关系

文件描述符与多路复用之间有密切的关系。多路复用是一种 I/O 模型,用于同时监视多个文件描述符的状态,并在其中任何一个文件描述符准备好进行 I/O 操作时进行相应的处理。

在多路复用中,通常使用一种特殊的文件描述符来监视多个文件描述符的状态变化,这个特殊的文件描述符被称为"多路复用器"(Multiplexer)或"事件循环"(Event Loop)。多路复用器可以通过系统调用(如 select()、poll()、epoll())来监视多个文件描述符的可读、可写或错误状态。

当某个文件描述符准备好进行读取或写入操作时,多路复用器会通知应用程序,并提供相关的文件描述符,应用程序可以根据文件描述符执行相应的 I/O 操作。

文件描述符与多路复用的关系可以总结如下:

  1. 多路复用器使用文件描述符来监视多个 I/O 资源的状态变化。
  2. 多路复用器通过系统调用来等待文件描述符的状态变化,以决定哪些文件描述符可读、可写或出现错误。
  3. 当文件描述符准备好进行 I/O 操作时,多路复用器会通知应用程序,并提供相关的文件描述符。
  4. 应用程序可以使用提供的文件描述符来执行相应的读取、写入或其他操作。

多路复用在处理并发 I/O 操作时非常有用,它可以减少对多个线程或进程的创建和管理,提高系统的性能和资源利用率。通过使用文件描述符和多路复用,应用程序可以同时处理多个文件或网络连接,提高系统的吞吐量和响应性能。

(2.5)事件驱动编程

事件驱动编程是一种编程范式,它基于事件的发生和处理来组织程序的执行流程。在事件驱动编程中,程序会等待事件的发生,一旦事件发生,就会触发相应的事件处理程序来处理事件。

事件可以是各种类型的信号、消息、用户输入、定时器到期等。事件驱动编程的核心思想是将程序的执行流程分解为事件的发生和事件的处理两个阶段。程序会等待事件的发生,当事件发生时,会调用相应的事件处理程序来处理事件。

事件驱动编程的特点包括:

  1. 事件驱动:程序等待事件的发生,而不是按照固定的顺序执行指令。
  2. 异步性:事件可以在任何时间发生,程序需要具备处理异步事件的能力。
  3. 非阻塞:事件的处理通常是非阻塞的,即程序可以同时处理多个事件,而不会因为等待某个事件的完成而阻塞其他事件的处理。
  4. 回调机制:事件处理通常通过回调函数或回调方法来实现,当事件发生时,会调用相应的回调函数来处理事件。

事件驱动编程广泛应用于图形用户界面(GUI)编程、网络编程、服务器开发等领域。它能够提高程序的并发性和响应性能,使程序能够同时处理多个事件,提供良好的用户体验和系统性能。常见的事件驱动编程框架包括Qt、Node.js、Twisted等。

(2.5.1)事件驱动编程与多路复用的关系

事件驱动编程和多路复用是密切相关的概念,多路复用可以作为事件驱动编程的基础设施,用于监听和处理各种事件。

多路复用是一种 I/O 模型,用于同时监视多个文件描述符的状态变化。它可以用于实现事件驱动编程模型。在多路复用中,多路复用器会等待多个文件描述符的状态变化,当某个文件描述符准备好进行 I/O 操作时,多路复用器会通知应用程序,应用程序可以根据文件描述符执行相应的事件处理。

事件驱动编程中的事件可以与文件描述符关联起来,因为文件描述符是操作系统中用于标识和访问文件或其他输入/输出资源的抽象概念。应用程序可以将不同的事件关联到不同的文件描述符上,并使用多路复用器来等待这些事件的发生。

一旦事件发生,多路复用器会通知应用程序,并提供相关的文件描述符,应用程序可以根据文件描述符执行相应的事件处理。这种机制使得应用程序能够同时监听和处理多个事件,提高系统的并发性和响应性能。

因此,事件驱动编程和多路复用的关系可以总结如下:

  1. 多路复用是一种 I/O 模型,用于同时监视多个文件描述符的状态变化。
  2. 多路复用可以作为事件驱动编程的基础设施,用于监听和处理各种事件。
  3. 事件驱动编程中的事件可以与文件描述符关联起来,利用多路复用器来等待事件的发生。
  4. 多路复用器会通知应用程序,提供相关的文件描述符,应用程序可以根据文件描述符执行相应的事件处理。

通过将事件与文件描述符关联,并利用多路复用器来等待事件的发生,可以实现高效的事件驱动编程模型,提高系统的并发性和响应性能。

【三】多道在操作系统中的作用

多道技术是一种在计算机系统中同时执行多个程序的技术。它允许多个程序并发地执行,从而提高了系统的吞吐量、资源利用率和响应性能。

在多道技术中,操作系统将计算机的内存划分为多个分区,每个分区可以加载一个程序。每个程序被称为一个作业,作业可以是用户提交的应用程序、系统任务或其他需要执行的任务。操作系统根据一定的调度算法,将多个作业加载到内存中,并分配给可用的CPU时间片进行执行。

多道技术的实现通常涉及以下几个关键概念和步骤:

  1. 内存分区:操作系统将内存划分为多个分区,每个分区用于加载一个作业。分区可以是固定大小的,也可以是可变大小的。每个分区包含作业的代码、数据和堆栈等信息。

  2. 调度算法:操作系统使用调度算法来决定哪个作业应该被加载到内存中并获得CPU时间片进行执行。常见的调度算法包括先来先服务(FCFS)、最短作业优先(SJF)、轮转调度(Round Robin)等。

  3. 上下文切换:当操作系统从一个作业切换到另一个作业时,需要进行上下文切换。上下文切换包括保存当前作业的状态信息(如寄存器值、程序计数器等),加载下一个作业的状态信息,并将控制权转移到下一个作业。

  4. 并发执行:在多道技术中,多个作业可以并发地执行。当一个作业在等待输入/输出操作或其他事件时,操作系统可以切换到另一个作业的执行,从而充分利用计算资源。这种并发执行可以提高系统的吞吐量和响应性能。

  5. 进程同步:在多道技术中,由于多个作业并发执行,可能会涉及到共享资源的访问问题。为了确保共享资源的正确访问,操作系统提供了进程同步机制,如互斥锁、信号量等。

多道技术的优点包括提高系统的吞吐量、资源利用率和响应性能,允许多个作业并发执行,提高了系统的并发性和并行性。然而,多道技术也面临一些挑战,如资源竞争、上下文切换开销和调度算法的选择等。因此,在设计和实现多道技术时需要综合考虑这些因素,以达到最佳的系统性能和用户体验。

【四】进程

在计算机科学中,进程(Process)是操作系统中的一个基本概念,用于表示正在运行的程序实例。一个进程可以看作是一个独立的执行单位,它包含了程序的代码、数据和执行状态。

每个进程都有自己的地址空间,用于存储程序的指令、数据和堆栈等信息。进程之间相互独立,彼此隔离,每个进程都有自己的运行环境和资源。进程可以并发地执行,通过操作系统的调度机制,多个进程可以在同一时间段内交替执行。

进程的创建通常是由操作系统的调度器或其他进程发起的。在创建进程时,操作系统为新进程分配独立的资源,包括内存空间、文件描述符、进程标识符等。进程可以执行各种操作,包括读写文件、网络通信、进行计算等。

进程之间可以通过进程间通信(Inter-Process Communication,IPC)机制进行数据交换和协作。常见的IPC机制包括管道(Pipe)、共享内存(Shared Memory)、消息队列(Message Queue)和套接字(Socket)等。

进程拥有自己的执行状态,包括程序计数器(Program Counter)、寄存器值、堆栈指针等。操作系统通过进程调度算法来决定哪个进程在某个时间片内执行,并负责进程的切换和调度。

总结起来,进程是操作系统中的一个基本概念,用于表示正在运行的程序实例。它是一个独立的执行单位,拥有自己的地址空间、资源和执行状态。进程之间相互独立,可以并发地执行,并通过进程间通信机制进行数据交换和协作。操作系统通过进程调度算法来管理和调度进程的执行。

(4.1)进程调度算法

进程调度算法是操作系统用来决定哪个进程在某个时间片内执行的策略。调度算法的目标是提高系统的性能和资源利用率,同时满足进程的公平性和响应性要求。下面介绍一些常见的进程调度算法:

  1. 先来先服务(First-Come, First-Served,FCFS):按照进程到达的顺序进行调度。当一个进程执行完毕或阻塞时,下一个进程按照队列中的顺序被选中执行。FCFS算法简单直观,但可能导致长作业等待时间(长作业优先问题)。

  2. 最短作业优先(Shortest Job Next,SJN):选择估计运行时间最短的进程进行调度。这个算法可以减少平均等待时间,但需要准确地预测进程的运行时间,对于长作业可能会出现饥饿问题。

  3. 优先级调度(Priority Scheduling):为每个进程分配一个优先级,优先级高的进程先执行。可以根据进程的重要性、紧急性或其他因素来确定优先级。但是,如果没有适当的调度策略,可能会导致优先级反转问题。

  4. 时间片轮转(Round Robin,RR):将CPU时间划分为固定的时间片,每个进程按照时间片顺序执行,当一个进程的时间片用完后,它会被放到就绪队列的末尾,下一个进程开始执行。RR算法可以实现公平性和响应性,但可能导致上下文切换开销增加。

  5. 多级反馈队列调度(Multilevel Feedback Queue Scheduling):将进程划分为多个优先级队列,每个队列具有不同的时间片大小。新创建的进程进入最高优先级队列,如果一个进程的时间片用完仍未执行完毕,它会被移到下一级队列,以此类推。这种调度算法可以根据进程的行为动态地调整优先级。

以上只是一些常见的进程调度算法,实际上,调度算法的选择取决于系统的需求和特点。现代操作系统通常采用复杂的调度算法来平衡系统性能和进程响应性。

(4.2)进程间通信

进程间通信(Inter-Process Communication,IPC)是指不同进程之间进行数据交换、共享资源和协作的机制。在多进程的计算机系统中,不同的进程可能需要相互传递数据、进行协作和共享资源,而进程间通信提供了一种机制来实现这种交互。

进程间通信可以用于以下情况:

  1. 数据传递:不同进程之间需要传递数据,例如一个进程产生的结果需要传递给另一个进程进行处理。

  2. 共享资源:多个进程需要共享某些资源,如共享内存区域、文件、设备等。

  3. 协作操作:多个进程需要进行协作操作,如同步操作、互斥操作等。

常见的进程间通信方式包括:

  1. 管道(Pipe):管道是一种半双工的通信方式,用于在具有亲缘关系的两个进程之间传递数据。管道可以是匿名的,也可以是有名字的。

  2. 消息队列(Message Queue):消息队列是一种通过消息传递进行进程间通信的机制。它允许一个进程向队列中发送消息,而另一个进程从队列中接收消息。

  3. 共享内存(Shared Memory):共享内存是一种将内存区域映射到多个进程地址空间的机制,使得多个进程可以直接访问同一块内存区域,实现数据共享。

  4. 信号量(Semaphore):信号量是一种用于进程间同步和互斥的机制。它可以用来保护共享资源,控制进程的执行顺序,以及实现进程间的互斥操作。

  5. 套接字(Socket):套接字是一种用于网络通信的通信机制,它可以在不同的计算机之间进行进程间通信。

这些进程间通信方式各有优劣,选择适当的方式取决于具体的应用场景和需求。在实际的程序设计和开发中,需要根据实际情况选择合适的进程间通信方式来实现进程之间的数据交换和协作。

(4.2.1)进程中通信的操作特性(同步、异步、阻塞、非阻塞)

同步、异步、阻塞、非阻塞是描述计算机系统中不同操作的执行方式和行为的概念。

  1. 同步(Synchronous):同步操作是指发起一个操作后,必须等待该操作完成后才能继续执行后续的操作。在同步操作中,调用者会主动等待被调用者完成任务并返回结果,然后才能继续执行其他操作。同步操作通常是阻塞的,即调用者会一直等待直到操作完成。

  2. 异步(Asynchronous):异步操作是指发起一个操作后,不需要等待该操作完成,而是可以立即继续执行后续的操作。在异步操作中,调用者不会主动等待被调用者完成任务,而是通过回调函数、轮询或其他机制来获取操作的结果。异步操作通常是非阻塞的,即调用者可以继续执行其他操作而不必等待操作完成。

  3. 阻塞(Blocking):阻塞是指一个操作在执行过程中会暂时停止,直到满足某个条件后才能继续执行。在阻塞操作中,调用者会一直等待直到操作完成或条件满足。阻塞操作会占用系统资源,并且调用者无法进行其他操作。

  4. 非阻塞(Non-blocking):非阻塞是指一个操作在执行过程中不会停止,无论是否满足条件,都会立即返回。在非阻塞操作中,调用者可以继续执行其他操作,而不必等待操作完成或条件满足。非阻塞操作不会占用系统资源,并且调用者可以同时进行其他操作。

这些概念通常用于描述进程间通信、I/O操作、多线程编程等场景中的操作行为和执行方式。具体使用哪种方式取决于应用需求和系统设计。

PS:同步、异步、阻塞、非阻塞是描述进程间通信的操作特性,它们不是特定于某一种进程间通信方式的

管道(Pipe)、消息队列(Message Queue)和共享内存(Shared Memory)都可以采用同步、异步、阻塞、非阻塞的方式进行进程间通信。

例如,在管道中,可以使用阻塞方式进行通信,即写入端进程在管道已满时会被阻塞,直到有空间可用;读取端进程在管道为空时会被阻塞,直到有数据可读。也可以使用非阻塞方式进行通信,即写入端进程在管道已满时会立即返回错误,而不会被阻塞;读取端进程在管道为空时会立即返回错误,而不会被阻塞。

类似地,消息队列和共享内存也可以根据需要选择同步、异步、阻塞或非阻塞的方式进行进程间通信。

因此,同步、异步、阻塞、非阻塞这些特性不是特定于某一种进程间通信方式,而是通用的概念,可适用于多种进程间通信方式。

(4.3)进程的层次结构

进程的层次结构是指进程在操作系统中以树状结构组织的形式。在这种结构中,进程之间存在父子关系,形成一个层次化的结构。下面详细介绍一下进程的层次结构:

  1. 根进程(Root Process):根进程是进程层次结构的顶层进程,通常是操作系统启动时创建的第一个进程。根进程没有父进程,它是其他进程的祖先。根进程的任务是管理和控制整个进程层次结构。

  2. 父进程(Parent Process):父进程是创建其他进程的进程。每个进程都有一个唯一的父进程,除了根进程外。父进程负责创建、管理和控制它的子进程。父进程可以监视子进程的执行情况,并根据需要进行资源分配和协调。

  3. 子进程(Child Process):子进程是由父进程创建的进程。一个父进程可以创建多个子进程,但一个子进程只能有一个父进程。子进程继承了父进程的一些属性和资源,并可以独立地执行任务。子进程可以创建自己的子进程,形成更深层次的层次结构。

通过父子关系的递归组合,进程层次结构可以形成多层的树状结构。每个进程都可以成为其他进程的父进程和子进程,从而形成一个层次化的关系。

进程层次结构的优点包括:

  • 组织和管理:进程层次结构可以方便地组织和管理进程。父进程可以监控和控制其子进程的行为,并根据需要进行资源分配和协调。
  • 协作和通信:进程层次结构可以用于实现进程间的协作和通信。父进程可以与其子进程进行数据交换和同步操作,通过进程间通信机制实现。
  • 权限控制:进程层次结构可以用于实现权限控制。父进程可以限制子进程的权限,确保系统的安全性和稳定性。
  • 继承和传递:进程层次结构可以实现资源的继承和传递。子进程可以继承父进程的一些属性和资源,并可以将这些资源传递给它的子进程。

需要注意的是,进程层次结构是动态变化的,进程可以动态地创建和终止,从而改变层次结构。此外,不同操作系统可能对进程层次结构的实现和管理方式有所不同。

(4.4)进程的状态

进程在操作系统中可以处于不同的状态,表示其在不同阶段的执行状态和条件。常见的进程状态包括:

![[进程状态.png]]

  1. 新建状态(New):当一个进程被创建时,它处于新建状态。在这个状态下,操作系统为进程分配必要的资源,并对其进行初始化。新建状态的进程还没有开始执行,等待系统调度分配CPU资源。

  2. 就绪状态(Ready):当一个进程已经准备好执行,并等待系统调度时,它处于就绪状态。在就绪状态下,进程已经具备运行所需的所有资源,包括CPU、内存和I/O设备等。进程在就绪队列中等待调度,一旦得到CPU资源,就可以进入运行状态。

  3. 运行状态(Running):当一个进程获得CPU资源并开始执行时,它处于运行状态。在运行状态下,进程正在执行其指令,占用CPU资源。一个系统通常只能有一个进程处于运行状态,因为CPU一次只能执行一个进程的指令。

  4. 阻塞状态(Blocked):当一个进程由于等待某个事件的发生而暂时无法继续执行时,它处于阻塞状态。在阻塞状态下,进程会释放CPU资源,并进入等待队列。常见的事件包括等待I/O操作完成、等待某个信号或等待资源的分配等。一旦事件发生,进程将从阻塞状态转换为就绪状态。

  5. 终止状态(Terminated):当一个进程完成其任务或被操作系统终止时,它进入终止状态。在终止状态下,进程释放占用的资源,并等待操作系统回收其相关的数据结构。终止状态的进程不再参与调度和执行。

进程在不同状态之间的转换是由操作系统的调度算法和事件驱动机制控制的。例如,当一个进程的时间片用完时,它从运行状态转换为就绪状态;当一个I/O操作完成时,阻塞状态的进程可以转换为就绪状态。

需要注意的是,不同操作系统可能对进程状态的命名和实现方式有所不同,但基本的概念和含义是相似的。进程状态的管理和转换是操作系统调度和协调进程执行的重要基础。

【五】并发&并行

(5.1)什么是并发

并发是指系统中同时执行多个独立的任务或操作的能力。在计算机领域,特别是操作系统和并行计算中,并发是指多个任务或进程在同一时间段内交替执行,给人一种同时运行的感觉。

并发可以通过多种方式实现,包括多线程、多进程、多核处理器等。这些并发执行的任务可以是相互独立的,也可以是相互依赖的。

并发的好处包括:

  1. 提高系统的资源利用率:通过同时执行多个任务,可以充分利用系统的处理能力和资源,提高系统的效率和吞吐量。

  2. 增强用户体验:并发可以使系统对用户的响应更加迅速,减少等待时间,提高用户体验。

  3. 实现任务的并行处理:通过并发执行,可以将一个大任务拆分成多个小任务并行处理,加快任务的完成时间。

  4. 支持多用户环境:在多用户系统中,通过并发执行可以使多个用户同时使用系统,提高系统的并发性和可用性。

然而,并发执行也会带来一些挑战和问题,例如数据竞争、死锁、资源争用等。因此,在设计并发系统时需要考虑合适的同步机制和调度算法,以确保并发执行的正确性和可靠性。

总结来说,并发是指系统中同时执行多个独立任务或操作的能力。通过并发执行,可以提高系统的资源利用率、增强用户体验、实现任务的并行处理和支持多用户环境。然而,并发执行也需要解决一些挑战和问题。

(5.2)什么是并行

并行是指同时执行多个任务或操作的能力。与并发不同,并行是指多个任务在同一时刻真正地同时执行,而不是交替执行。

在计算机领域,特别是并行计算中,并行通常涉及到同时使用多个处理器、多个计算核心或多个计算节点来执行任务。这些任务可以是相互独立的,也可以是相互依赖的。

并行计算的好处包括:

  1. 提高计算速度:通过同时利用多个处理器或计算核心,可以将一个任务分解成多个子任务并行执行,从而加快任务的完成速度。

  2. 处理大规模数据:对于需要处理大规模数据的任务,通过并行计算可以将数据分割成多个部分,并在多个处理单元上并行处理,提高处理效率。

  3. 解决复杂问题:某些复杂问题需要进行大量的计算和搜索,通过并行计算可以将问题分解成多个子问题并行求解,从而加速求解过程。

  4. 提高系统的可靠性:通过并行计算,可以实现冗余计算和错误检测,提高系统的可靠性和容错能力。

并行计算也面临一些挑战,例如任务划分和负载均衡、数据同步和通信、并行算法设计等。在设计并行系统时需要考虑这些挑战,并选择合适的并行编程模型和并行算法来充分利用计算资源和提高系统性能。

总结来说,并行是指同时执行多个任务或操作的能力。通过并行计算,可以提高计算速度、处理大规模数据、解决复杂问题和提高系统的可靠性。然而,并行计算也需要解决一些挑战和问题。

(5.3)并发与并行的关系

并发和并行是两个相关但不完全相同的概念。

并发是指系统中同时执行多个独立任务或操作的能力,这些任务可以是交替执行的,也可以是并行执行的。并发更侧重于任务的调度和执行顺序,强调任务之间的交替执行和共享资源的管理。

并行是指真正同时执行多个任务或操作的能力,这些任务在同一时刻并行执行,利用多个处理器、计算核心或计算节点来完成。并行更侧重于任务的同时执行和计算资源的利用。

可以说,并行是并发的一种特殊情况,即多个任务在同一时刻真正地同时执行。而并发则更广泛,可以包括交替执行和并行执行两种情况。

在实际应用中,通常会将并发和并行结合起来使用,以充分利用计算资源和提高系统性能。例如,在一个多核处理器上,可以通过并行执行多个线程或进程来实现并发。这样可以同时执行多个任务,并且每个任务在自己的处理器核心上并行执行。

总结来说,并发和并行是相关但不完全相同的概念。并发强调任务的交替执行和资源共享,而并行强调任务的同时执行和计算资源的利用。并行是并发的一种特殊情况,即多个任务在同一时刻真正地同时执行。在实际应用中,可以将并发和并行结合起来使用,以充分利用计算资源和提高系统性能。

【六】僵尸进程

僵尸进程(Zombie Process)是指一个子进程在执行完毕后,但其父进程尚未对其进行处理(即未调用 wait()waitpid() 等系统调用来获取子进程的退出状态),此时子进程的进程控制块仍保留在系统进程表中,成为僵尸进程。

僵尸进程的主要特点如下:

  1. 子进程已经终止:僵尸进程是指子进程已经执行完毕,但其父进程尚未对其进行处理。子进程已经终止,但它的进程控制块仍然存在。

  2. 子进程的退出状态未被处理:僵尸进程的父进程尚未调用相应的系统调用来获取子进程的退出状态。这通常是因为父进程忽略了子进程的退出状态,或者父进程还没有执行到处理子进程退出的代码。

  3. 占用系统资源:僵尸进程占用了系统的进程表项和其他资源,尽管它不再执行任何代码。

僵尸进程的存在可能会导致系统资源的浪费,尤其是当大量的僵尸进程堆积时。因此,及时清理僵尸进程是很重要的。

解决僵尸进程的方法是父进程调用适当的系统调用来处理子进程的退出状态,通常是使用 wait()waitpid() 系统调用。这些系统调用会阻塞父进程,直到子进程退出并返回其退出状态。通过处理子进程的退出状态,操作系统可以将僵尸进程从进程表中删除,并释放相关的资源。这样,僵尸进程就会被清理掉,不再占用系统资源。

另外,还可以使用一些其他方法来避免僵尸进程的产生,例如:

  • 父进程在创建子进程后,使用 os.setpgrp() 将子进程置于新的进程组,然后父进程调用 os.waitpid() 来等待子进程的退出状态,从而避免僵尸进程的产生。

  • 父进程可以使用 signal.signal() 函数来注册 SIGCHLD 信号的处理函数,当子进程退出时,会发送 SIGCHLD 信号给父进程,父进程可以在信号处理函数中调用 wait()waitpid() 来处理子进程的退出状态。

综上所述,僵尸进程是子进程已经终止但父进程尚未处理的进程,通过适当的处理子进程的退出状态,可以及时清理僵尸进程,避免资源的浪费。

【七】孤儿进程

孤儿进程(Orphan Process)是指父进程在子进程终止之前就先终止了或者意外终止,而子进程仍然在继续执行的情况下产生的进程。

孤儿进程的主要特点如下:

  1. 父进程终止:孤儿进程的父进程在子进程终止之前就先终止了,或者意外终止(如进程异常退出、系统崩溃等)。父进程的终止可能是由于正常的退出、异常终止、被其他进程终止等原因。

  2. 子进程继续执行:即使父进程终止,孤儿进程仍然在继续执行。孤儿进程会被操作系统自动转移到一个新的父进程,通常是 init 进程(进程ID为1)。

  3. 孤儿进程没有父进程:孤儿进程的父进程已经终止,因此孤儿进程没有有效的父进程。

对于孤儿进程的处理,操作系统会自动将孤儿进程转移到一个新的父进程,通常是 init 进程。init 进程是操作系统启动的第一个进程,它会负责接管孤儿进程,并正确处理它们的终止。init 进程会调用适当的系统调用来等待孤儿进程的退出,并回收它们的资源。

孤儿进程的存在可以有一些潜在的问题,如资源泄漏和进程管理的混乱。因此,编写良好的程序应该正确处理子进程的终止,确保父进程能够及时等待子进程的退出,并进行适当的资源回收和清理操作。

总结起来,孤儿进程是指父进程在子进程终止之前就先终止了或者意外终止,而子进程仍然在继续执行的进程。操作系统会将孤儿进程转移到一个新的父进程(通常是 init 进程)来处理它们的终止。正确处理子进程的终止是编写健壮程序的重要方面,以避免孤儿进程的产生和相关问题的发生。

【八】守护进程

守护进程(Daemon Process)是在计算机操作系统中以服务方式运行的一种特殊类型的后台进程。守护进程通常在系统启动时被启动,并在系统运行期间持续运行,负责执行特定的任务或提供某种服务。

以下是守护进程的一些特点和行为:

  1. 后台运行:守护进程在后台运行,不与任何用户终端相关联。它们通常在系统启动时由系统初始化脚本或服务管理器启动,并在系统关闭时由系统关闭脚本或服务管理器关闭。

  2. 无终端关联:守护进程不与任何用户终端相关联,因此它们没有标准输入、输出和错误流。它们通常将日志信息写入日志文件或系统日志,以便记录和跟踪其活动。

  3. 脱离会话:守护进程通常会调用系统调用 setsid() 来创建一个新的会话,并脱离当前的终端会话。这样可以确保守护进程不会受到终端会话的影响,并且可以独立于终端运行。

  4. 无控制终端:守护进程没有控制终端,因此无法接收用户输入或向用户输出信息。它们通常通过其他方式与其他进程或系统进行通信,例如使用套接字、管道、信号等。

  5. 任务执行:守护进程负责执行特定的任务或提供某种服务。这些任务可以是周期性的,也可以是一次性的。常见的守护进程包括网络服务(如 Web 服务器、数据库服务器)、系统监控进程、定时任务调度器等。

  6. 错误处理:守护进程通常需要具备良好的错误处理机制。它们应该能够处理和记录错误,以便及时发现和解决问题。

  7. 生命周期管理:守护进程的启动、停止和重启通常由系统的初始化脚本、服务管理器或特定的管理工具来管理。这些工具提供了对守护进程的控制和监控功能,以确保守护进程的正常运行和可靠性。

守护进程在系统中扮演着重要的角色,它们提供了各种服务和功能,为用户和其他进程提供了便利。编写和管理守护进程需要考虑到它们的特殊性,包括后台运行、无终端关联、脱离会话等。同时,守护进程应该具备良好的错误处理机制和适当的生命周期管理,以确保它们的稳定性和可靠性。

【九】multiprocessing 模块

multiprocessing 模块是 Python 标准库中用于支持多进程编程的模块。它提供了创建和管理进程的类和函数,使得在 Python 中可以方便地实现并行计算和多进程任务。

下面是对 multiprocessing 模块的的介绍:

  1. 进程类(Process Class):multiprocessing 模块提供了 Process 类,用于创建和管理进程。通过实例化 Process 类,可以创建一个新的进程对象,并指定要执行的任务。进程对象可以通过调用 start() 方法启动,并通过 join() 方法等待进程的结束。进程类还提供了其他方法和属性,用于管理进程的状态和行为。

  2. 进程池类(Pool Class):multiprocessing 模块还提供了 Pool 类,用于创建进程池。进程池是一组预先创建的进程,可以通过将任务分配给进程池中的进程来并行执行。进程池类提供了 map()apply() 等方法,用于方便地分发任务和收集结果。

  3. 队列类(Queue Class):multiprocessing 模块中的 Queue 类是一个线程安全的队列,用于在多进程之间进行通信。进程可以通过 put() 方法将数据放入队列,而其他进程可以通过 get() 方法从队列中获取数据。队列类还提供了其他方法和属性,用于控制队列的大小、阻塞和非阻塞操作等。

  4. 锁类(Lock Class):multiprocessing 模块中的 Lock 类是一个线程安全的锁,用于在多进程之间进行同步。进程可以通过 acquire() 方法获取锁,执行临界区代码,然后通过 release() 方法释放锁。锁类还提供了其他方法和属性,用于控制锁的状态和行为。

  5. 其他辅助函数和类:multiprocessing 模块还提供了一些其他的辅助函数和类,用于支持多进程编程。例如,ValueArray 类用于在多进程之间共享数据,Event 类用于进程间的事件通知,Semaphore 类用于进程间的信号量控制等。

(6.1)Process类-创建进程的两种方式

Process 类是 multiprocessing 模块中用于创建和管理进程的类。通过实例化 Process 类,可以创建一个新的进程对象,并指定要执行的任务。以下是对 Process 类的详细介绍和使用方法:

(6.1.1)构造函数创建进程

  1. 创建进程对象:
    要创建一个进程对象,需要实例化 Process 类,并传入要执行的任务函数和相关参数。例如:

    from multiprocessing import Process
    
    def my_function(arg1, arg2):
        # 执行任务的代码
    
    p = Process(target=my_function, args=(arg1_value, arg2_value))
    
  2. 启动进程:
    创建进程对象后,可以通过调用 start() 方法启动进程。启动进程后,进程对象会自动调用指定的任务函数,并开始执行任务。例如:

    p.start()
    
  3. 等待进程结束:
    可以使用 join() 方法等待进程的结束。调用 join() 方法会阻塞当前进程,直到被调用的进程执行完毕。例如:

    p.join()
    
  4. 进程状态和属性:
    Process 类提供了一些方法和属性,用于管理进程的状态和行为。常用的方法和属性包括:

    • is_alive():判断进程是否正在运行。
    • terminate():终止进程的执行。
    • name:进程的名称。
    • pid:进程的ID。
    • daemon:设置进程是否为守护进程。
  5. 进程间通信:
    Process 类支持进程间的通信,可以使用 QueuePipe 等通信机制进行数据交换。例如,可以在主进程中创建一个 Queue 对象,并将其作为参数传递给子进程,实现进程间的数据传递。

需要注意的是,进程之间是独立的执行单位,拥有各自的内存空间和资源。因此,在多进程编程中,需要注意进程间的数据共享和同步问题,避免出现竞态条件和数据不一致的情况。

(6.1.2)继承Process类

可以通过继承 Process 类,并重写其 run() 方法来创建一个新的子进程。在子类中,可以定义子进程要执行的代码。

例如:

from multiprocessing import Process  
  
  
class MyProcess(Process):  
    def __init__(self, name, age):  
        super().__init__()  
        self.name = name  
        self.age = age  
  
    def run(self):  
        # 子进程运行的代码  
        print('子进程')  
        pass  
  
  
if __name__ == '__main__':  
    p = MyProcess('wx', 'age')  
    p.start()  
    print('主进程')

(6.1.3). join( ) 阻塞当前进程,直到被调用的子进程执行结束或超时

join()Process 类提供的一个方法,用于等待子进程的结束。调用 join() 方法会阻塞当前进程,直到被调用的子进程执行结束或超时。

join() 方法有以下特点和用法:

  1. 阻塞当前进程:调用 join() 方法会使当前进程进入阻塞状态,直到被调用的子进程执行结束。在子进程执行期间,父进程会一直等待,直到子进程结束或超时。

  2. 等待子进程结束:join() 方法会等待被调用的子进程执行结束。子进程结束的条件可以是子进程的代码执行完毕,或者调用了子进程的 terminate() 方法终止了子进程。

  3. 超时参数:join() 方法可以接受一个可选的超时参数,用于设置等待子进程结束的最长时间。如果超过指定的超时时间子进程仍未结束,join() 方法会返回,当前进程可以继续执行后续的代码。

  4. 嵌套调用:join() 方法可以嵌套调用,即在一个进程等待另一个进程的同时,可以再启动其他子进程并等待它们的结束。这样可以实现多个子进程的并发执行和管理。

下面是一个使用 join() 方法的示例:

from multiprocessing import Process
import time

def my_function():
    print("子进程开始执行")
    time.sleep(2)
    print("子进程执行结束")

if __name__ == '__main__':
    p = Process(target=my_function)
    p.start()

    print("父进程开始执行")
    p.join()  # 等待子进程结束
    print("父进程执行结束")

在上述代码中,创建了一个子进程 p,目标函数是 my_function。在父进程中,先输出一条消息,然后调用 p.join() 等待子进程结束。子进程执行了一个简单的任务,等待2秒后输出一条消息。当子进程执行结束后,父进程才会继续执行后续的代码。

通过使用 join() 方法,可以实现父进程对子进程的同步等待,确保在子进程执行结束后再进行后续的操作。这对于需要等待子进程完成的任务和进程间的协作非常有用。

(6.2)Lock类-进程锁

进程互斥锁(Process Mutex)是一种同步机制,用于在多进程环境中实现进程间的互斥访问。它是一种保护共享资源的机制,可以确保在任意时刻只有一个进程可以访问共享资源,从而避免竞争条件和数据不一致的问题。

在多进程环境中,每个进程都有自己独立的内存空间,无法直接共享内存。因此,进程互斥锁通常使用操作系统提供的特殊机制来实现。具体实现方式可能因操作系统而异,但它们的目标都是提供一种互斥访问的机制。

进程互斥锁的主要特点如下:

  1. 互斥性:进程互斥锁保证在任意时刻只有一个进程可以获取锁并访问共享资源。当一个进程获得锁后,其他进程将被阻塞,直到该进程释放锁。

  2. 安全性:进程互斥锁提供了一种安全的机制,确保共享资源在被一个进程访问时不会被其他进程同时修改,从而避免了数据竞争和不一致性。

  3. 阻塞等待:如果一个进程尝试获取锁但锁已经被其他进程持有,该进程将被阻塞,直到锁被释放。这种等待机制可以避免忙等待(busy-waiting),节省了系统资源。

进程互斥锁的使用场景包括但不限于以下情况:

  • 多个进程需要访问共享资源,例如共享文件、共享数据库等。
  • 需要保护关键代码段,确保同一时间只有一个进程可以执行该代码段。
  • 需要控制对某些系统资源的独占性访问,例如打印机、串口等。

需要注意的是,进程互斥锁只能在同一个系统中的不同进程之间起作用,对于不同系统之间的进程,无法使用进程互斥锁进行同步。在跨平台的分布式系统中,通常使用其他的同步机制,如分布式锁、分布式互斥量等。

总结来说,进程互斥锁是一种用于多进程环境中实现进程间互斥访问的同步机制。它通过提供互斥性、安全性和阻塞等待等特性,确保在任意时刻只有一个进程可以访问共享资源,从而避免竞争条件和数据不一致的问题。

from multiprocessing import Process, Lock, Value  
  
  
def buy_ticket(name, tickets, lock):  
    with lock:  
        if tickets.value > 0:  
            tickets.value -= 1  
            print(f'{name}买到了车票,剩余票数: {tickets.value}')  
        else:  
            print('票已售空')  
  
  
if __name__ == '__main__':  
    lock = Lock()  
    tickets = Value('i', 2)  # 创建共享的整数对象  
  
    processes = []  
    for i in range(5):  
        p = Process(target=buy_ticket, args=(i, tickets, lock))  
        p.start()  
        processes.append(p)  
  
    for p in processes:  
        p.join()

在 Value('i', 2) 中,'i' 是用来指定共享数据的类型的参数。在这里,'i' 表示共享的数据类型是整数(int)。这个参数是必须的,因为它告诉了系统需要创建一个共享的整数对象。

除了整数 'i' 外,还可以使用其他参数来指定其他类型的共享数据,例如:

  • 'd':双精度浮点数
  • 'f':单精度浮点数
  • 'c':字符
  • 'u':Unicode字符

(6.3)JoinableQueue类-进程间通信的队列

JoinableQueue是Python中multiprocessing模块提供的一种队列类型,它继承自Queue类。JoinableQueueQueue的基础上增加了一些额外的功能,用于任务管理和同步。

JoinableQueue提供了以下主要方法:

  • put(item[, block[, timeout]]): 将一个项目放入队列中。可选的block参数指定在队列已满时是否阻塞,默认为True。可选的timeout参数指定在队列已满且阻塞时的超时时间,默认为None(无超时)。

  • get([block[, timeout]]): 从队列中取出并返回一个项目。可选的block参数指定在队列为空时是否阻塞,默认为True。可选的timeout参数指定在队列为空且阻塞时的超时时间,默认为None(无超时)。

  • join(): 阻塞调用线程,直到队列中的所有任务都被处理完毕。join()方法会阻塞直到队列中的每个任务都调用了task_done()方法。

  • task_done(): 由消费者进程调用,用于通知队列一个任务已经被处理完毕。每次调用task_done()方法后,队列内部的计数器会减少,直到计数器为零时,join()方法才会解除阻塞。

(6.3.1)JoinableQueue的作用

JoinableQueue在多进程编程中具有重要的作用,它主要用于任务管理和同步。以下是JoinableQueue的几个用途:

  1. 任务分发和处理:JoinableQueue可以作为任务队列,生产者进程可以将任务放入队列中,而消费者进程可以从队列中获取任务并进行处理。这种任务分发和处理的方式可以有效地利用多个进程来并行处理任务。

  2. 同步机制:JoinableQueue提供了一种同步机制,确保生产者进程等待队列中的所有任务都被处理完毕。通过调用join()方法,生产者进程可以阻塞,直到队列中的所有任务都被处理完毕。这对于需要等待所有任务完成后再进行后续操作的情况非常有用。

  3. 进程间通信:JoinableQueue可以在多个进程之间传递数据。生产者进程可以将数据放入队列中,而消费者进程可以从队列中获取数据进行处理。这样可以方便地实现进程间的数据交换和通信。

  4. 结果收集:JoinableQueue可以用于收集消费者进程处理任务的结果。消费者进程可以将处理结果放入队列中,而生产者进程可以从队列中获取结果进行进一步处理或展示。

通过使用JoinableQueue,可以简化多进程编程中的任务管理和进程间的同步操作。它提供了一种方便且可靠的方式来处理并发任务和进程间通信,从而提高程序的性能和可维护性。

from multiprocessing import Process, JoinableQueue  
  
def producer(queue):  
    for i in range(5):  
        item = f"Item {i}"  
        queue.put(item)  
        print(f"Produced: {item}")  
    queue.join()  
  
  
def consumer(queue):  
    while True:  
        item = queue.get()  
        if item is None:  
            break  
        print(f"Consumed: {item}")  
        queue.task_done()  
  
  
if __name__ == '__main__':  
    queue = JoinableQueue()  
  
    # 创建生产者进程  
    producer_process = Process(target=producer, args=(queue,))  
    producer_process.start()  
  
    # 创建消费者进程  
    consumer_process = Process(target=consumer, args=(queue,))  
    consumer_process.start()  
  
    # 等待生产者进程结束  
    producer_process.join()  
  
    # 向队列中添加退出标记  
    queue.put(None)  
  
    # 等待消费者进程结束  
    consumer_process.join()

(6.4)进程间通信(IPC)

进程间通信(Inter-Process Communication,IPC)是指在操作系统中,不同的进程之间交换数据和信息的机制和技术。由于每个进程拥有独立的内存空间,因此进程之间不能直接访问彼此的内存。为了实现进程间的数据传输和通信,操作系统提供了多种IPC机制,包括以下几种常见的方式:

  1. 管道(Pipe):管道是一种半双工的通信方式,可以在具有亲缘关系的进程之间进行通信。它可以是匿名管道(仅在父子进程之间使用)或命名管道(允许无关进程之间的通信)。

  2. 共享内存(Shared Memory):共享内存是一种高效的IPC方式,允许多个进程共享同一块内存区域。进程可以直接读写共享内存,避免了数据的复制和传输开销。

  3. 信号量(Semaphore):信号量是一种用于进程同步和互斥的机制。它可以用来实现进程之间的互斥访问共享资源。

  4. 消息队列(Message Queue):消息队列是一种通过消息传递进行进程间通信的机制。进程可以将消息发送到队列中,其他进程可以从队列中接收消息。

  5. 套接字(Socket):套接字是一种网络编程中常用的通信机制,它也可以用于进程间通信。通过套接字,进程可以在网络上进行通信,实现分布式的进程间通信。

这些IPC机制各有特点,适用于不同的场景和需求。选择合适的IPC机制取决于进程之间的关系、数据传输的性质和要求,以及操作系统提供的支持。进程间通信在操作系统和分布式系统中起着重要的作用,使得不同的进程能够协同工作、共享资源和信息。

(6.4.1)子进程与主进程之间通过队列发送消息

from multiprocessing import Process, Queue


def worker(q):
    info = 'from worker process!'
    q.put(info)


if __name__ == '__main__':
    q = Queue()  # 创建一个队列对象,用于进程间通信
    p = Process(target=worker, args=(q,))  # 创建一个子进程,传递队列对象作为参数
    p.start()  # 启动子进程

    message = q.get()  # 从队列中获取子进程发送的消息
    print(message)  # 打印接收到的消息
    p.join()  # 等待子进程结束

(6.4.2)主进程通过队列给子进程发送消息

from multiprocessing import Process, Queue  
  
  
def worker(q):  
    message = q.get()  # 子进程从队列中获取消息  
    print("Message received in worker process:", message)  # Message received in worker process: Hello from the main process!  
  
  
if __name__ == '__main__':  
    q = Queue()  # 创建一个队列对象,用于进程间通信  
    p = Process(target=worker, args=(q,))  # 创建一个子进程,传递队列对象作为参数  
    p.start()  # 启动子进程  
  
    message = "Hello from the main process!"  # 要发送的消息  
    q.put(message)  # 主进程将消息放入队列中  
  
    p.join()  # 等待子进程结束

除了发送消息,主进程和子进程之间通过队列还可以进行其他类型的通信和数据交换。以下是一些常见的用法:

  1. 发送任务:主进程可以将任务放入队列中,子进程可以从队列中获取任务并执行。这在任务分发和并行处理的场景中非常有用。

  2. 传递数据:主进程可以将数据放入队列中,子进程可以从队列中获取数据并进行处理。这样可以实现主进程和子进程之间的数据交换。

  3. 共享状态:主进程和子进程可以使用队列共享状态信息。例如,主进程可以将某个状态值放入队列中,子进程可以从队列中获取该值并进行相应的处理。

  4. 进程间同步:队列可以用作进程间的同步机制。主进程可以在队列中放入一个特定的标记,子进程可以通过检查队列中的标记来等待主进程的指示。

  5. 多对多通信:主进程可以与多个子进程进行通信,通过为每个子进程创建独立的队列对象,可以实现多对多的进程间通信。

通过队列进行进程间通信非常灵活,可以根据具体的需求进行扩展和定制。队列提供了一个可靠且线程安全的机制,使主进程和子进程能够高效地进行数据和消息传递。

(6.4.3)子进程与子进程之间借助队列发送数据

from multiprocessing import Process, Queue  
  
  
def sender(q):  
    data = [1, 2, 3, 4, 5]  
    for item in data:  
        q.put(item)  # 子进程向队列中发送数据  
  
  
def receiver(q):  
    received_data = []  
    while not q.empty():  
        item = q.get()  # 子进程从队列中接收数据  
        received_data.append(item)  
    print("Received data:", received_data)  
  
  
if __name__ == '__main__':  
    q = Queue()  # 创建一个队列对象,用于进程间通信  
  
    p1 = Process(target=sender, args=(q,))  
    # 创建发送数据的子进程,传递队列对象作为参数 
     
    p2 = Process(target=receiver, args=(q,))  
    # 创建接收数据的子进程,传递队列对象作为参数  
  
    p1.start()  # 启动发送数据的子进程  
    p2.start()  # 启动接收数据的子进程  
  
    p1.join()  # 等待发送数据的子进程结束  
    p2.join()  # 等待接收数据的子进程结束

【十】queue 内置队列模块

queue.Queue是Python标准库中的一个类,用于实现线程安全的队列。它提供了一种在多线程环境下安全地传递数据的机制。

queue.Queue类的特点包括:

  1. 线程安全:queue.Queue是线程安全的,可以在多个线程之间安全地进行数据传递,而无需额外的同步机制。

  2. 先进先出(FIFO):queue.Queue实现了先进先出的队列数据结构,即最早放入队列的元素最早被获取。

  3. 阻塞操作:queue.Queue提供了阻塞操作,当队列为空时,获取操作(get())会阻塞线程,直到有新的元素放入队列;当队列满时,放入操作(put())会阻塞线程,直到队列有空间可用。

  4. 容量限制:queue.Queue可以通过参数指定队列的最大容量,当队列满时,继续放入操作会被阻塞。

queue.Queue类提供了以下常用方法:

  1. put(item[, block[, timeout]]):将元素放入队列的末尾。如果队列已满且block参数为True(默认值),则阻塞线程直到队列有空间可用;如果队列已满且block参数为False,则立即引发queue.Full异常。可选的timeout参数指定阻塞超时时间。

  2. get([block[, timeout]]):从队列的头部移除并返回元素。如果队列为空且block参数为True(默认值),则阻塞线程直到队列有新的元素;如果队列为空且block参数为False,则立即引发queue.Empty异常。可选的timeout参数指定阻塞超时时间。

  3. empty():检查队列是否为空。如果队列为空,返回True;否则返回False

  4. qsize():返回队列中元素的数量。

  5. task_done():在使用队列进行任务处理时,用于通知队列已完成一个任务。每次从队列中获取一个元素并完成相应的任务后,应调用task_done()方法。

  6. join():在使用队列进行任务处理时,用于阻塞线程,直到队列中的所有任务都已完成。在所有任务都已经被获取并完成后,调用join()方法会解除阻塞。

  7. get_nowait():用于从队列中非阻塞地获取一个任务。如果队列为空,调用get_nowait()方法会立即引发一个queue.Empty异常。

(7.1)put() 将元素放入队列的末尾

语法: `put(item[, block[, timeout]])

from queue import Queue  
  
q = Queue(5)  
  
q.put('wx')  
q.put('xd')  
  
print(q.qsize())  # 2

(7.2)get() 从队列的头部移除并返回元素

语法:get([block[, timeout]])

from queue import Queue  
  
q = Queue(5)  
  
q.put('wx')  
q.put('xd')  
  
get_res = q.get()  
get_res1 = q.get()  
print(get_res)  # wx  
print(get_res1)  # xd

(7.3)empty() 检查队列是否为空,如果为空为 True,如果不为空为 False

from queue import Queue  
  
q = Queue(5)  
  
q.put('wx')  
q.put('xd')  
  
print(q.empty())  # False  
  
get_res = q.get()  
get_res1 = q.get()  
  
print(get_res)  # wx  
print(get_res1)  # xd  
  
print(q.empty())  # True

(7.4)qsize() 返回队列中元素的数量

from queue import Queue  
  
q = Queue(5)  
  
q.put('wx')  
q.put('xd')  
  
print(q.qsize())  # 2

(7.5)task_done() 跟踪队列中未完成的任务数量,并在任务完成时进行标记

task_done()方法是在使用queue.Queue进行任务处理时使用的一个方法。它用于通知队列已完成一个任务。

当我们从队列中获取一个元素并完成相应的任务后,应调用task_done()方法。这样做的目的是为了告诉队列,该任务已经被处理完毕,队列可以相应地更新内部的计数器。

在多线程环境中,当使用queue.Queue进行任务处理时,我们可以通过调用task_done()方法来追踪任务的完成情况。通过与join()方法配合使用,我们可以在主线程中阻塞,直到队列中的所有任务都已完成。

简而言之,task_done()方法是用于标记队列中的一个任务已经完成的方法,以便于在多线程任务处理中进行协调和同步。

模板:

import threading
import queue

# 创建任务队列
task_queue = queue.Queue()

# 定义工作线程的处理函数
def worker():
    while True:
        # 从队列中获取任务
        item = task_queue.get()

        # 处理任务
        # ...

        # 标记任务完成
        task_queue.task_done()

# 创建并启动工作线程
for _ in range(3):
    t = threading.Thread(target=worker)
    t.start()

# 向队列中添加任务
for i in range(10):
    task_queue.put(i)

# 等待所有任务完成
task_queue.join()

示例:

import threading  
import queue  
  
q = queue.Queue()  
  
  
def worker(q):  
    while True:  
        item = q.get()  
        if item is None:  
            break  
        print(f"Processing item: {item}")  
        # q.task_done()  
  
  
workers = 3  
threads = []  
  
for _ in range(workers):  
    t = threading.Thread(target=worker, args=(q,))  
    t.start()  
    threads.append(t)  
  
for i in range(5):  
    q.put(i)  
  
q.join()  
  
for _ in range(workers):  
    q.put(None)  
  
for t in threads:  
    t.join()

如果没有调用task_done()方法,将会导致以下问题:

  1. 阻塞问题:在使用join()方法等待任务完成时,如果没有正确调用task_done()方法,主线程将一直阻塞,无法继续执行后续的代码。这会导致程序无法正常结束或无法进行下一步操作。

  2. 计数错误:每个queue.Queue对象内部都有一个计数器,用于跟踪队列中未完成的任务数量。如果没有调用task_done()方法,计数器将无法准确地反映任务的完成情况。这可能导致程序在判断任务是否完成时产生错误。

  3. 资源泄漏:task_done()方法的另一个重要作用是释放由get()方法获取的资源。在多线程环境中,如果没有调用task_done()方法,可能会导致资源无法正确释放,从而引发资源泄漏问题。

综上所述,task_done()方法在多线程任务处理中是必不可少的。它确保了任务的正确跟踪、计数和资源释放,同时也保证了主线程能够正确等待任务的完成。如果没有调用task_done()方法,将会导致程序阻塞、计数错误和资源泄漏等问题的发生。

(7.6)join() 阻塞线程,直至队列中的所有任务完成

import threading
import queue

def worker(q):
    while True:
        item = q.get()  # 从队列中获取任务
        if item is None:  # 如果获取到的任务为None,表示任务结束
            break
        # 模拟任务处理
        print(f"Processing item: {item}")
        q.task_done()  # 标记任务完成

q = queue.Queue()  # 创建任务队列

# 创建并启动多个线程
num_workers = 3
threads = []
for _ in range(num_workers):
    t = threading.Thread(target=worker, args=(q,))
    t.start()
    threads.append(t)

# 向队列中添加任务
for i in range(10):
    q.put(i)

# 阻塞主线程,直到队列中的所有任务都完成
q.join()

# 停止所有线程
for _ in range(num_workers):
    q.put(None)  # 向队列中添加任务结束标记
for t in threads:
    t.join()  # 等待所有线程结束

(7.7)get_nowait 用于从队列中非阻塞地获取一个任务,如果队列为空则报错

import queue  
  
q = queue.Queue()  
  
# 向队列中添加任务  
q.put(1)  
q.put(2)  
q.put(3)  
  
# 从队列中非阻塞地获取任务  
while True:  
    try:  
        item = q.get_nowait()  
        print(f"Processing item: {item}")  
    except queue.Empty:  
        print('队列已为空')  
        break

【十一】线程

线程(Thread)是计算机科学中的一个概念,指的是在一个进程内部同时执行的多个执行单元。线程是操作系统进行调度的最小单位,它拥有独立的执行流程、栈空间和执行上下文。

线程的出现主要是为了提高程序的并发性和响应性。相比于单线程程序,多线程程序可以同时执行多个任务,从而提高程序的处理能力和效率。线程可以并发执行独立的任务,也可以共享进程的资源,如内存空间、文件句柄等。通过合理地使用线程,可以实现并行处理、异步操作、响应用户输入等功能。

以下是线程的一些重要特点和概念:

  1. 并发执行:线程可以并发执行,即多个线程可以同时执行不同的任务。操作系统通过调度算法来决定各个线程的执行顺序和时间片分配。

  2. 共享进程资源:线程属于同一个进程,它们共享进程的资源,如内存空间、文件句柄、全局变量等。这使得线程之间可以方便地进行数据共享和通信。

  3. 独立的执行流程:每个线程都有自己的执行流程,它们可以独立地执行指令、调用函数、访问变量等。线程之间的执行流程可以通过线程调度来切换。

  4. 线程同步:由于线程共享进程的资源,可能会出现多个线程同时访问和修改同一个资源的情况,导致数据不一致或冲突。为了保证数据的一致性和正确性,需要使用线程同步机制,如互斥锁、信号量、条件变量等。

  5. 线程安全:线程安全是指多线程环境下,对共享资源的访问和修改不会导致数据错误或不一致。编写线程安全的程序需要考虑并发访问的竞态条件,并采用适当的同步机制来保护共享资源。

在Python中,可以使用threading模块来创建和管理线程。threading模块提供了创建线程、线程同步、线程间通信等功能,使得在Python中编写多线程程序变得简单和方便。

(11.1)线程的优缺点

线程作为并发编程的一种机制,具有一些优点和缺点。下面是线程的主要优点和缺点:

优点:

  1. 提高程序的并发性:线程可以同时执行多个任务,从而提高程序的并发性和处理能力。多线程可以将耗时的操作和阻塞的I/O操作与其他任务并行执行,提高程序的效率和响应速度。

  2. 共享进程资源:线程属于同一个进程,它们共享进程的资源,如内存空间、文件句柄、全局变量等。这使得线程之间可以方便地进行数据共享和通信,简化了多任务之间的数据交换和协作。

  3. 灵活性和响应性:线程可以独立执行,具有独立的执行流程和上下文,可以方便地实现异步操作、响应用户输入和事件驱动等。线程可以快速地启动和销毁,灵活性较高。

  4. 资源开销较小:相比于进程,线程的创建和切换的开销较小,因为线程共享进程的资源。线程的切换只需要保存和恢复线程的上下文,开销较小。

缺点:

  1. 线程安全问题:多线程共享进程资源,可能会导致数据竞争、冲突和不一致的问题。需要使用适当的同步机制来保护共享资源,避免线程安全问题。

  2. 调试和测试困难:多线程程序的调试和测试相对复杂,因为线程之间的执行是并发的,可能出现难以复现的问题。线程的并发执行可能导致程序的非确定性行为,增加了调试和测试的难度。

  3. 死锁和饥饿问题:多线程程序可能出现死锁和饥饿问题。死锁是指多个线程相互等待对方释放资源而无法继续执行的情况,饥饿是指某个线程长时间无法获取到所需的资源而无法执行的情况。

  4. 上下文切换开销:线程的切换需要保存和恢复线程的上下文,这涉及到寄存器的保存和恢复、栈的切换等操作,会带来一定的开销。当线程数量较多时,频繁的上下文切换可能会影响程序的性能。

综上所述,线程具有提高并发性、共享进程资源、灵活性和响应性、资源开销较小等优点,但也存在线程安全问题、调试和测试困难、死锁和饥饿问题、上下文切换开销等缺点。在使用线程编程时,需要综合考虑这些因素,并合理地设计和管理线程。

【十二】threading模块

(12.1)Thread类-创建进程的两种方式

(12.1.1)构造函数创建线程

import threading  
  
  
def task():  
    thread_name = threading.current_thread().name  
  
    print(f'{thread_name} is working')  
  
  
thread1 = threading.Thread(target=task, name='wx')  
thread2 = threading.Thread(target=task, name='xd')  
  
thread1.start()  
thread2.start()  
  
thread1.join()  
thread2.join()  
  
print('\n')  
print('work done')  
# wx is working  
# xd is working  
  
# work done

(12.1.2)继承Thread类

import threading

# 自定义线程类,继承 Thread 类
class MyThread(threading.Thread):
    def run(self):
        thread_name = threading.current_thread().name
        print(f"线程 {thread_name} 正在执行任务")

# 创建线程对象
thread1 = MyThread(name="Thread 1")
thread2 = MyThread(name="Thread 2")

# 启动线程
thread1.start()
thread2.start()

# 主线程等待子线程执行完毕
thread1.join()
thread2.join()

print("所有线程执行完毕")

# 线程 Thread 1 正在执行任务
# 线程 Thread 2 正在执行任务
# 所有线程执行完毕

(12.2)线程间数据共享

线程间数据共享是指多个线程在同一个进程中共享相同的内存空间,可以通过共享数据来实现线程之间的通信和协作。在多线程编程中,线程间数据共享是一个重要的问题,需要特别注意数据的正确性和同步性。

以下是一些常见的线程间数据共享的方式和技术:

  1. 全局变量:全局变量是在整个程序中都可见的变量,多个线程可以通过访问和修改全局变量来进行数据共享。但需要注意的是,多个线程同时访问和修改全局变量时可能会导致数据竞争和不一致性的问题,因此需要使用同步机制来保证数据的一致性。

  2. 互斥锁(Mutex):互斥锁是最常用的同步机制之一,用于保护共享数据的访问。在访问共享数据之前,线程需要先获取互斥锁,保证同一时间只有一个线程可以访问共享数据,其他线程需要等待锁的释放。通过互斥锁,可以避免多个线程同时修改共享数据导致的竞态条件和数据不一致性问题。

  3. 读写锁(ReadWrite Lock):读写锁允许多个线程同时读取共享数据,但只允许一个线程进行写操作。这样可以提高读操作的并发性能。读写锁适用于读多写少的场景,可以减少对共享数据的争用。

  4. 条件变量(Condition):条件变量用于线程间的等待和通知机制。一个线程可以等待某个条件的发生,而其他线程可以在满足条件时通知等待的线程。条件变量通常与互斥锁配合使用,用于实现线程间的同步和协作。

  5. 信号量(Semaphore):信号量是一种计数器,用于控制对共享资源的访问数量。线程在访问共享资源之前需要获取信号量,每次访问完成后释放信号量。可以用于限制并发线程的数量和实现线程间的同步。

  6. 线程安全的数据结构:有些数据结构本身是线程安全的,可以直接在多线程环境中使用,如Python中的 queue 模块提供的线程安全的队列实现。使用这些线程安全的数据结构可以避免手动处理同步问题。

  7. 原子操作:原子操作是不可中断的操作,可以保证在多线程环境下的原子性。Python 提供了一些原子操作的函数,如 threading.Lockacquire()release() 方法。

  8. 线程局部数据:线程局部数据是每个线程独立拥有的数据,不同线程之间互不干扰。可以使用 threading.local() 创建线程局部数据对象,每个线程可以独立地访问和修改自己的局部数据。

在进行线程间数据共享时,需要根据具体的应用场景选择合适的同步机制和数据结构,以保证数据的正确性和线程的安全性。同时,需要注意避免过多地使用锁和同步机制,以免降低程序的性能和并发性能。

【十三】协程

协程(Coroutine)是一种轻量级的线程(称为"微线程"或"纤程")实现方式,它可以在单个线程内实现多个执行流的切换。与传统的线程相比,协程的切换操作更加高效,因为它不涉及线程上下文的切换和内核级别的操作。

协程可以看作是一种特殊的函数,它可以在执行过程中暂停并保存当前的状态,然后再次恢复执行。这种暂停和恢复的过程可以由协程自身控制,而不需要操作系统的干预。协程的切换是协作式的,即只有在协程主动让出执行权时,其他协程才能被执行。

协程的主要特点包括:

  1. 轻量级:协程的创建和切换操作非常轻量,不需要像线程那样消耗大量的系统资源。

  2. 高效性:协程的切换操作不涉及内核态和用户态的切换,因此切换速度非常快,几乎可以达到原子级别。

  3. 协作式调度:协程的调度是由协程自身控制的,不需要操作系统的调度器参与。协程需要主动让出执行权,才能切换到其他协程执行。

  4. 共享状态:多个协程可以共享相同的状态,这使得协程之间的通信和数据共享更加方便。

协程在编程中有广泛的应用,特别是在事件驱动的编程模型中,如异步编程和并发编程。通过使用协程,可以避免回调地狱和复杂的线程同步问题,使代码更加简洁和可读。

在Python中,协程可以使用asyncio模块和async/await语法来实现。asyncio提供了协程的调度和事件循环机制,async/await语法用于定义和管理协程的异步操作。通过使用asyncioasync/await,可以方便地编写异步IO、网络编程和并发任务等代码。

(13.1)asyncio模块

asyncio是Python标准库中提供的用于编写异步代码的模块。它提供了一种基于协程(coroutine)的并发框架,用于处理异步IO操作、并发任务和网络通信等场景。

asyncio模块的核心是事件循环(Event Loop),它是一个在单线程中执行协程的调度器。事件循环负责调度和执行协程,以及处理异步IO操作的完成通知。通过事件循环,可以实现高效的协程调度和并发执行。

asyncio模块提供了以下主要功能和组件:

  1. 协程支持: asyncio提供了asyncawait关键字,用于定义和管理协程。协程是一种特殊的函数,可以在执行过程中暂停并保存当前状态,然后再次恢复执行。使用协程可以编写非阻塞的异步代码。

  2. 事件循环: asyncio的核心是事件循环,它负责调度和执行协程。事件循环会根据协程的状态和IO操作的完成情况,决定哪个协程继续执行,哪个协程暂停等待。

  3. 异步IO支持: asyncio提供了一套异步IO操作的接口,包括文件IO、网络IO和子进程等。通过使用asyncio提供的异步IO接口,可以实现高效的非阻塞IO操作。

  4. 协程调度器: asyncio提供了协程的调度器,可以通过调度器来调度和执行协程。调度器可以控制协程的执行顺序、并发度和优先级等。

  5. 异步任务: asyncio支持创建和管理异步任务,通过Task对象可以对协程进行封装和管理。异步任务可以在事件循环中进行调度和执行。

  6. 协程通信和同步: asyncio提供了一些工具和机制,用于实现协程之间的通信和同步。例如,asyncio.Queue用于实现协程之间的消息传递,asyncio.Lock用于实现协程之间的互斥访问。

asyncio模块提供了一种高效的异步编程方式,能够处理大量的并发任务和异步IO操作。它在网络编程、Web开发、爬虫和高性能服务器等领域有着广泛的应用。通过使用asyncio,可以编写出简洁、高效和可维护的异步代码。

(13.1.1)asyncio 模块方法

asyncio 模块提供了一组用于异步编程的方法和工具,用于编写基于协程的异步代码。

下面是一些 asyncio 模块中常用的方法的详细介绍:

  1. asyncio.sleep(delay, result=None, *, loop=None): 在协程中暂停指定的时间。接受一个延迟时间 delay(以秒为单位)和一个可选的结果值 result。在暂停期间,事件循环可以切换到其他协程执行。返回一个可等待对象,可以使用 await 关键字等待其完成。

  2. asyncio.wait_for(aw, timeout, *, loop=None): 等待一个可等待对象完成,最长等待时间为 timeout 秒。如果超时时间到达而对象仍未完成,将引发 asyncio.TimeoutError 异常。

  3. asyncio.gather(*coros_or_futures, loop=None, return_exceptions=False): 并行运行多个协程或 Future 对象,并等待它们全部完成。返回一个包含所有协程或 Future 的结果的列表。如果设置 return_exceptions=True,则即使有协程或 Future 抛出异常,也会将异常包含在结果列表中。

  4. asyncio.wait(aws, *, loop=None, timeout=None, return_when=ALL_COMPLETED): 等待一组可等待对象完成。aws 是一个可迭代对象,包含协程、Future 或 Task 对象。return_when 参数指定何时返回,可以是 FIRST_COMPLETED(任意对象完成时返回)、FIRST_EXCEPTION(任意对象抛出异常时返回)或 ALL_COMPLETED(所有对象完成时返回)。返回一个包含已完成对象的集合的元组。

  5. asyncio.shield(aw, *, loop=None): 创建一个受保护的可等待对象,用于防止被取消。aw 是一个可等待对象,可以是协程、Future 或 Task 对象。返回一个新的可等待对象,它的行为类似于 aw,但不会受到取消的影响。

  6. asyncio.wait_for_multiple(aws, timeout, *, loop=None): 等待多个可等待对象完成,最长等待时间为 timeout 秒。与 wait_for() 方法类似,但可以等待多个对象的完成。

以上是 asyncio 模块中一些常用的方法。通过这些方法,可以创建协程任务、等待任务完成、设置超时、并行执行多个任务等,实现异步编程和协程的调度和控制。在实际应用中,可以根据具体需求选择合适的方法来实现异步操作。

(13.1.1.1)asyncio.run 运行异步程序的主入口点

asyncio.run(main, *, debug=False): 运行异步程序的主入口点。接受一个协程函数 main 作为参数,并创建一个事件循环,运行该协程函数直到完成。可以通过设置 debug=True 来启用调试模式。

参数

  • main: 异步程序的主协程函数。
  • debug(可选): 布尔值,表示是否启用调试模式。默认为 False

返回值

  • asyncio.run() 方法没有返回值。

功能

  • asyncio.run(main, *, debug=False) 方法用作异步程序的入口点。它会创建一个事件循环,并运行指定的协程函数 main,直到该函数完成。在运行过程中,asyncio.run() 方法会处理事件循环的启动、运行和关闭过程。

使用示例

import asyncio  
  
  
async def index():  
    print('start')  
  
  
async def main():  
    await index()  
  
  
asyncio.run(main())

(13.1.1.2)asyncio.create_task 将一个协程对象封装为一个 Task 对象

参数

  • coro: 要封装为 Task 的协程对象。

返回值

  • 返回创建的 Task 对象。

功能

  • asyncio.create_task(coro) 方法将一个协程对象封装为一个 Task 对象,并将该 Task 对象添加到事件循环的任务队列中。Task 对象是 Future 类的子类,表示一个异步任务。

使用示例

async def index():  
    print('start')  # start  
  
  
async def main():  
    func = index()  
    task = asyncio.create_task(func)  
    await task  
  
  
asyncio.run(main())

(13.1.1.3)asyncio.sleep 暂停当前协程的执行,让出控制权给事件循环

参数

  • delay:等待的时间,单位为秒。可以是一个整数或浮点数。
  • result:可选参数,指定在恢复执行时返回的结果。

返回值

  • 协程函数的返回值

功能

用于暂停当前协程的执行,让出控制权给事件循环,在指定的时间后再恢复执行。

asyncio.sleep() 方法的工作原理如下:

  1. 当协程调用 asyncio.sleep(delay) 时,它会通知事件循环暂停当前协程的执行,并设置一个定时器,指定在 delay 秒后再次唤醒该协程。
  2. 在等待的过程中,事件循环会继续处理其他协程和任务。
  3. 当定时器到期时,事件循环会重新激活暂停的协程,使其继续执行。
  4. 如果指定了 result 参数,则在恢复执行时,asyncio.sleep() 方法会返回该参数的值。

使用示例

import asyncio  
  
  
async def index():  
    print('sleep now')  # 打印 'sleep now'    await asyncio.sleep(2)  # 暂停执行 2 秒  
    print('sleep over')  # 打印 'sleep over'    return 'Done'  # 返回 'Done'  
  
async def main():  
    res = await index()  
    print(res)  
  
  
asyncio.run(main())

输出结果:

	sleep now
	sleep over
	Done

(13.1.1.4)asyncio.gather 并发运行多个协程或任务

参数

  • *coroutines_or_futures:要并发运行的协程对象或 Future 对象。可以传递一个或多个参数。
  • loop:可选参数,指定要使用的事件循环。如果未提供,则使用当前默认的事件循环。
  • return_exceptions:可选参数,指定是否返回异常。如果设置为 True,则即使某个协程或任务抛出异常,gather() 也会继续运行其他协程或任务,并将异常包装在结果中。如果设置为 False,则一旦有任何协程或任务抛出异常,gather() 将立即取消所有其他协程或任务,并将异常重新抛出。

返回值

  • 协程函数的返回值

功能

用于并发运行多个协程或任务,并等待它们全部完成并返回一个列表。

工作原理

  1. 当调用 asyncio.gather(*coroutines_or_futures) 时,它会创建一个任务组(Task Group)来并发运行传递的协程或任务。
  2. 任务组会同时启动所有的协程或任务,并等待它们全部完成。
  3. 如果某个协程或任务已经完成,它的结果会被收集起来。
  4. 如果某个协程或任务抛出异常,根据 return_exceptions 参数的设置,gather() 方法要么继续运行其他协程或任务,要么立即取消所有其他协程或任务,并将异常重新抛出。
  5. 当所有协程或任务都完成时,gather() 方法返回一个包含所有结果的列表,按照传递的顺序排列。

使用示例

import asyncio  
  
  
async def func(coroutines_id):  
    print(f'{coroutines_id} is start ')  
    await asyncio.sleep(1)  
    print(f'{coroutines_id} is end')  
    return f'Finish id :{coroutines_id}'  
  
  
async def main():  
    coroutines = [func(n) for n in range(3)]  
    res = await asyncio.gather(*coroutines)  
    print(res)  
  
  
asyncio.run(main())

(13.1.2)事件循环

事件循环(Event Loop)是异步编程中的核心概念,用于调度和执行协程(Coroutines)。它在单线程中管理协程的执行,处理异步IO操作的完成通知,并实现协程之间的切换和并发执行。让我们更详细地了解事件循环的工作原理和主要组成部分。

事件循环的主要组成部分包括以下几个要素:

  1. 注册协程(Coroutines): 在事件循环中,可以注册多个协程。这些协程可以是通过async def定义的异步函数,也可以是通过asyncio.create_task()创建的任务(Task)对象。注册的协程会被事件循环管理和调度。

  2. 事件驱动: 事件循环是基于事件驱动的编程模型。它监听和处理各种事件,如IO事件、定时器事件、信号事件等。通过事件的触发和处理,事件循环能够实现非阻塞的并发处理。

  3. 协程调度: 事件循环负责调度和执行协程。它决定哪个协程继续执行,哪个协程暂停等待。事件循环通过协程切换(Coroutine Switch)来实现协程之间的切换和并发执行。当一个协程遇到await关键字时,它会暂停执行,并将控制权返回给事件循环。

  4. 异步IO操作管理: 事件循环处理异步IO操作的完成通知。当协程遇到需要等待的IO操作时,它会将IO操作委托给事件循环,并注册一个回调函数(Callback)。然后,协程会暂停执行,等待IO操作完成的通知。一旦IO操作完成,事件循环会收到通知,并恢复相应的协程继续执行。

  5. 协程通信和同步: 事件循环提供了一些工具和机制,用于实现协程之间的通信和同步。例如,可以使用asyncio.Queue实现协程之间的消息传递,使用asyncio.Lock实现协程之间的互斥访问。

  6. 异常处理: 事件循环负责捕获和处理协程中的异常。当协程发生异常时,事件循环会捕获异常,并根据异常处理策略进行处理,如打印异常信息、终止协程等。

事件循环的工作流程如下:

  1. 事件循环开始执行,注册的协程处于等待状态。

  2. 事件循环监听各种事件,如IO事件、定时器事件等。

  3. 当某个事件发生时,事件循环会执行对应的处理逻辑。对于IO事件,事件循环会将IO操作委托给操作系统或底层库,并注册一个回调函数。

  4. 事件循环继续监听其他事件,同时等待IO操作完成的通知。

  5. 一旦IO操作完成,事件循环收到通知,执行相应的回调函数,并恢复相应的协程继续执行。

  6. 协程执行过程中,可能会遇到其他IO操作或需要等待的事件,事件循环会重复上述步骤,调度和执行相应的协程。

  7. 当所有注册的协程都执行完毕,事件循环结束执行。

在Python中,asyncio模块提供了对事件循环的支持和封装。可以使用asyncio.get_event_loop()获取默认的事件循环对象,也可以使用asyncio.new_event_loop()创建新的事件循环对象。通过loop.run_until_complete()方法,可以运行事件循环,直到所有注册的协程执行完毕。

事件循环是实现异步编程的重要机制,它使得异步IO操作和并发任务能够高效地在单线程中处理,提供了一种高效的非阻塞的编程方式。

(13.1.3)协程函数

协程函数是一种特殊的函数,用于异步编程。它使用async def语法来定义,并使用await关键字来暂停执行和恢复执行。协程函数常常与事件循环(Event Loop)一起使用,以实现并发和异步操作。

下面是一些关键概念和特性,以帮助更详细地理解协程函数:

  1. async def语法:协程函数使用async def语法来定义。这告诉解释器该函数是一个协程函数,并可以使用await关键字。

  2. await关键字:await关键字用于暂停协程函数的执行,等待一个异步操作完成,并返回异步操作的结果。在等待期间,协程函数的控制权会交给事件循环,让事件循环去处理其他协程或异步任务。等待的异步操作可以是网络请求、文件读写、数据库查询等耗时的操作。

  3. 协程对象:当调用协程函数时,它会返回一个协程对象。协程对象是一种特殊类型的对象,表示一个可执行的协程。协程对象可以通过调用await关键字来执行,或者使用事件循环的方法来调度执行。

  4. 事件循环(Event Loop):事件循环是一个用于调度和执行协程的机制。它负责管理协程的执行顺序,并处理异步操作的完成。事件循环可以通过调用协程对象的await方法或使用asyncio模块提供的方法来运行协程。

  5. 异步操作:协程函数通常用于执行异步操作,例如网络请求、文件读写、数据库查询等。这些操作通常是耗时的,并且可以并发执行,以提高程序的性能和响应性。通过使用await关键字,协程函数可以在等待异步操作完成时暂停执行,而不会阻塞主线程或其他协程。

下面是一个简单的示例,展示了协程函数的定义和使用:

import asyncio

async def my_coroutine():
    print("Coroutine started")
    await asyncio.sleep(1)  # 等待1秒钟
    print("Coroutine resumed")

# 创建一个事件循环对象
loop = asyncio.get_event_loop()

# 运行协程函数
loop.run_until_complete(my_coroutine())

在上面的示例中,my_coroutine()是一个协程函数,使用async def语法定义。在函数中,使用await asyncio.sleep(1)语句来暂停协程函数的执行,等待1秒钟。在等待期间,事件循环可以去处理其他协程或异步任务。1秒钟后,协程函数会被重新激活,继续执行下面的代码。

(13.1.4)协程对象

协程对象(Coroutine Object)是协程函数执行后返回的对象,可以被视为可执行的协程。协程对象可以与其他协程对象并发执行,实现并发和异步操作。

协程对象是通过使用async def语法定义的协程函数来创建的。在协程函数中,使用await关键字可以暂停协程的执行,并等待某个异步操作完成后再继续执行。当协程函数执行到await语句时,它会返回一个协程对象,表示该协程的执行状态。

协程对象具有以下特点:

  1. 可以被调度和执行:协程对象可以通过事件循环(Event Loop)进行调度和执行。事件循环会根据协程对象的状态和优先级,决定何时执行协程对象。

  2. 可以并发执行:多个协程对象可以并发执行,它们之间可以相互切换执行,实现并发操作。这使得协程对象非常适合处理大量的并发任务。

  3. 可以返回结果:协程对象可以返回一个结果或者抛出一个异常。在协程函数中,可以使用return语句返回结果,或者使用raise语句抛出异常。调用协程对象时,可以使用await关键字获取协程函数的返回值。

下面是一个简单的示例,展示了如何创建和执行协程对象:

import asyncio

async def my_coroutine():
    print('Coroutine is running')
    await asyncio.sleep(1)
    return 'Coroutine finished'

async def main():
    coroutine_obj = my_coroutine()  # 创建协程对象
    result = await coroutine_obj  # 执行协程对象
    print('Result:', result)

loop = asyncio.get_event_loop()  # 获取事件循环对象
loop.run_until_complete(main())  # 运行主协程

在上面的示例中,my_coroutine()是一个协程函数,通过调用my_coroutine()创建了一个协程对象coroutine_obj。然后,在main()函数中,通过await coroutine_obj来执行协程对象。最后,打印协程函数的返回值。

需要注意的是,协程对象的执行需要在事件循环中进行,因此需要先获取当前线程的事件循环对象,并使用run_until_complete()方法将主协程提交给事件循环执行。

总结来说,协程对象是协程函数执行后返回的对象,可以被视为可执行的协程。它可以与其他协程对象并发执行,实现并发和异步操作。

(13.1.5)await关键字

await 是一个关键字,用于在协程中暂停执行并等待一个异步操作完成。它通常与协程函数和协程对象一起使用,用于实现异步编程和并发执行。

当在协程中遇到 await 关键字时,它会暂停当前协程的执行,将控制权交还给事件循环(Event Loop),并等待被 await 的表达式完成。在等待期间,事件循环可以继续执行其他协程或处理其他事件。一旦被 await 的表达式完成,协程会从暂停的地方恢复执行。

await 关键字可以用于以下类型的表达式:

  1. 异步函数调用:可以使用 await 关键字等待调用异步函数的结果。异步函数是使用 async def 定义的函数,它返回一个协程对象。await 会暂停当前协程的执行,直到异步函数执行完毕并返回结果。

  2. 协程对象:可以使用 await 关键字等待协程对象的执行完成。协程对象是协程函数执行后返回的对象,表示一个可执行的协程。await 会暂停当前协程的执行,直到协程对象执行完毕并返回结果。

  3. Future 对象:可以使用 await 关键字等待 Future 对象的结果。Future 是 asyncio 模块中用于表示异步操作结果的对象。await 会暂停当前协程的执行,直到 Future 对象的结果可用。

需要注意的是,await 只能在协程函数或异步上下文中使用。在普通的同步函数或代码块中,不能使用 await 关键字。

下面是一个简单的示例,展示了 await 的使用:

import asyncio

async def my_coroutine():
    print('Coroutine is running')
    await asyncio.sleep(1)  # 等待 1 秒钟
    print('Coroutine resumed')
    return 'Coroutine finished'

async def main():
    result = await my_coroutine()  # 等待协程执行完成并获取结果
    print('Result:', result)

loop = asyncio.get_event_loop()
loop.run_until_complete(main())

在上面的示例中,my_coroutine() 是一个协程函数,使用 await asyncio.sleep(1) 在协程中等待 1 秒钟。在 main() 函数中,使用 await my_coroutine() 等待协程执行完成,并获取协程的返回值。

总结来说,await 关键字用于在协程中暂停执行并等待异步操作完成。它可以用于异步函数调用、协程对象和 Future 对象,实现异步编程和并发执行。

(13.1.6)Task

Task(任务)是异步编程中的一个概念,用于表示协程的执行状态和管理协程的调度。在 Python 的 asyncio 模块中,Task 是 Future 的子类,用于封装协程对象,并提供了更多的功能和操作。

Task 对象具有以下特点和功能:

  1. 封装协程对象:Task 对象可以封装一个协程对象,将其转化为一个可调度的任务。通过创建 Task 对象,可以将协程对象添加到事件循环的任务队列中,以便调度和执行。

  2. 调度和执行:Task 对象可以通过事件循环进行调度和执行。事件循环会根据任务队列中的 Task 对象,决定何时执行哪个协程。可以使用事件循环的 create_task() 方法创建 Task 对象,并使用 run_until_complete() 方法运行任务直到完成。

  3. 取消和取消等待:Task 对象可以通过调用 cancel() 方法来取消执行。取消一个 Task 对象会导致协程抛出一个 CancelledError 异常。此外,可以使用 await 关键字等待一个 Task 对象的完成,如果在等待期间 Task 对象被取消,会抛出 CancelledError 异常。

  4. 获取结果:Task 对象可以通过 result() 方法获取协程的返回值。如果协程抛出异常,可以通过 exception() 方法获取异常对象。

  5. 设置优先级:Task 对象可以设置优先级,以影响事件循环的调度顺序。可以使用 set_priority() 方法设置 Task 对象的优先级。

  6. 绑定回调函数:Task 对象可以绑定回调函数,以在任务完成时执行特定的操作。可以使用 add_done_callback() 方法添加回调函数。

下面是一个示例,展示了 Task 对象的使用:

import asyncio

async def my_coroutine():
    print('Coroutine is running')
    await asyncio.sleep(1)
    return 'Coroutine finished'

def callback(task):
    print('Task completed')

async def main():
    loop = asyncio.get_event_loop()
    task = loop.create_task(my_coroutine())  # 创建 Task 对象
    task.add_done_callback(callback)  # 绑定回调函数
    await asyncio.sleep(0.5)
    task.cancel()  # 取消 Task 对象的执行
    try:
        result = await task  # 等待 Task 对象的完成
        print('Result:', result)
    except asyncio.CancelledError:
        print('Task was cancelled')

loop = asyncio.get_event_loop()
loop.run_until_complete(main())

在上面的示例中,使用 loop.create_task() 创建了一个 Task 对象 task,并将 my_coroutine() 封装为 Task 对象。然后,使用 task.add_done_callback() 绑定了一个回调函数 callback,在任务完成时会被调用。在 main() 函数中,通过 await task 等待 Task 对象的完成,并获取协程的返回值。在等待之前的 0.5 秒钟,调用 task.cancel() 取消 Task 对象的执行。如果 Task 对象被取消,会捕获 CancelledError 异常并打印相应的消息。