【7.0】网络并发总复习解释版

发布时间 2023-06-27 09:27:35作者: Chimengmeng

【7.0】网络并发总复习解释版

网络编程部分

【一】软件开发架构

【1】什么是CS架构

  • CS架构即 客户端/服务端架构,如APP应用

【2】什么是BS架构

  • BS架构即 浏览器/服务端架构,如网页的网站

【3】二者相比的优缺点

(1)优点

  • CS架构
    • 服务器运行数据载荷轻
    • 数据的储存管理较为透明
  • BS架构
    • 维护和升级简单
    • 成本低,方法简单

(2)缺点

  • CS架构
    • 维护成本高昂
  • BS架构
    • 服务器载荷较重

【二】互联网协议

【1】OSI七层

  • 应用层
  • 表示层
  • 会话层
  • 传输层
  • 网络层
  • 数据连接层
  • 物理层

【2】五层协议

(1)物理层

  • 该层的主要任务是确保计算机之间可以发送和接收原始的比特流。
  • 这个层次定义了比特传输率、物理连接方式、电气标准、线路传输模式等。

(2)数据链路层

  • 该层的主要任务是通过使用MAC地址来传递帧,以便将比特流转换为网络可以理解的格式。
  • 这一层还处理差错检测和纠错功能。

(3)网络层:

  • 该层的主要任务是向数据包添加路径信息,以便在不同网络之间传输数据。
  • 该层还为IP地址提供了一个标准结构,并定义了路由器之间转发数据的方法。

(4)传输层:

  • 该层提供端到端通信的控制,使得应用程序能够通过分配端口号来区分彼此。
  • 所使用的协议有TCP(Transmission Control Protocol)和UDP(User Datagram Protocol)。

(5)应用层:

  • 该层的主要任务是提供各种网络服务,例如文件传输、电子邮件和万维网浏览器等。
  • 这一层提供了使用网络服务所需的各种协议和工具。

【3】以太网协议

  • 计算机网络中最广泛的一种局域网协议
  • 该协议定义了计算机通过共享媒介在局域网中传输数据的规则,以及如何让不同的设备共享网络资源。

【4】IP协议

  • 是一种网络通信协议,它是TCP/IP协议族中的一个分组交换的协议,用于在网络中把数据包从源主机发送到目的主机。
  • IP协议定义了互联网上每个设备的地址和路由规则,通过这些地址和路由规则可以让数据包在网络各个节点之间传送,使得互联网上的亿万个计算机、服务器和其他设备都能够相互通信。
  • IP协议还有一个重要的特点,即可以处理不同媒体的数据传输,例如局域网、广域网等,并保证了数据包的传输顺序。IP协议是Internet上最核心的协议之一,为上层协议提供了可靠的数据传输支持。

【5】广播风暴

  • 广播风暴是指在计算机网络中,某一个广播消息在网络中不断转发,造成整个网络过载、拥塞等问题的现象。
  • 广播风暴常见于局域网中,当网络中一个节点上的广播消息没有被其他节点正确处理时,它将继续向其他节点广播这个消息。如果网络中有很多节点同时进行广播,那么将会导致不断的广播和转发,从而引起网络的拥塞和过载。

【6】Mac地址

  • Mac地址,也称为物理地址或硬件地址,是网络接口控制器(NIC)的唯一标识符,通常由48位二进制数字表示。
  • Mac地址可以用来唯一标识一个设备
  • 一个常见的Mac地址可能是这样的格式:00-11-22-33-44-55,其中前24位的“00-11-22”是厂商识别码,后24位的“33-44-55”是设备序列号。

【7】TCP/UDP

(1)TCP

  • TCP(Transmission Control Protocol)是一种面向连接的协议,使用三次握手建立连接并且通过确认和重传保证数据传输的可靠性。
  • TCP提供流量控制、拥塞控制和错误校验等功能,可以确保数据在传输过程中正确无误并且有序到达接收端。
  • 由于TCP具有可靠性和顺序传递性,所以通常用于需要准确无误地传输数据的应用程序,例如文件传输、电子邮件、网页浏览、远程登录等。

(2)UDP

  • UDP(User Datagram Protocol)是一种无连接的协议,不需要建立连接和关闭连接,只需要把数据包发出去。
  • UDP仅提供基本的数据传输功能,并不保证传输的可靠性和顺序性。
  • UDP的优点在于传输效率高,适合于大量数据的传输和实时性要求高的应用程序,例如多媒体流、实时视频和音频应用程序、在线游戏等。

【三】三次握手,四次挥手(****

【1】三次握手

三次握手指建立TCP协议连接时客户端和服务器之间的交互过程

  • 客户端向服务器发送一个SYN(同步)报文
    • 请求建立连接。
  • 服务器收到SYN报文后向客户端回送一个SYN-ACK(同步-确认)报文
    • 表示可以建立连接。
  • 客户端接收到SYN-ACK报文后再发送一个ACK(确认)报文
    • 表示确认连接已建立。

【2】四次握手

四次握手指断开TCP协议连接时客户端和服务器之间的交互过程

  • 客户端向服务器发送一个FIN(结束)报文
    • 表示要关闭连接。
  • 服务器收到FIN报文后向客户端回送一个ACK报文
    • 表示已经收到客户端的请求。
  • 如果服务器还有数据需要发送给客户端
    • 则继续发送直到数据全部发送完毕。
  • 服务器发送一个FIN报文,表示数据已经全部发送完毕
    • 可以关闭连接。

客户端收到FIN报文后向服务器发送一个ACK报文,表示确认收到关闭请求。

此时客户端需要进入TIME-WAIT状态,等待两倍的报文最大生存时间,以保证数据已经被全部传输完毕。

如果服务器没有收到来自客户端的ACK报文,则会重传FIN报文。

【四】socket协议

Socket协议是一种计算机网络通信中常用的应用层协议。它提供了一个端到端的双向通信流,在通信过程中可以用来实现客户端和服务器之间的数据传输和通信。

  • 核心思想是将网络通信本身抽象为一组文件操作,从而简化了网络编程的难度。

    • 类似于文件操作,发送方在进行Socket通信时可以将数据封装成一个数据包进行发送,接收方则按照相同的规则解析数据包并进行响应。
  • TCP/IP协议中的传输控制协议(TCP)和用户数据报协议(UDP)都依赖于Socket协议来实现数据传输和通信

【五】TCP粘包问题(定值固定长度报头)

【1】什么是粘包问题

  • TCP粘包问题是指多个独立的数据包在传输过程中被当作一个数据包被接收方接收到,或者一个数据包被拆分成多个小的数据包进行传输的情况。
    • 这种情况发生的原因是TCP在传输过程中会对数据进行缓存和优化,同时也可能会将多个数据包合并在一起进行传输,从而导致这种问题的出现。

【2】解决办法:添加报头

  • 针对TCP粘包问题,可以采用定长固定值报头的处理方式
    • 即在每个数据包前都加上固定长度的报头
      • 报头中包含了数据的长度信息
    • 这样接收方就可以根据报头中的长度信息对整个数据包进行接收和拆分
      • 从而避免了TCP粘包的问题。

【3】案例讲解

  • 假设我们规定每个数据包的报头长度为4字节
    • 其中包含了后面数据部分的长度信息
  • 那么发送方每次发送数据时可以先计算出数据部分的长度
    • 然后在发送数据前添加一个4字节的报头
    • 报头中包含了数据部分的长度信息;
  • 接收方在接收数据时
    • 首先先接收4个字节的报头信息
    • 并根据报头中的长度信息来确定需要接收的数据长度
    • 然后再接收相应长度的数据部分。
  • 这种方式虽然可以解决TCP粘包问题
    • 但是也会降低传输效率
    • 因为每个数据包前都需要添加一个固定长度的报头
    • 如果数据本身比较短,则报头会占据很大的比例。

因此,在实际应用中,需要根据具体的情况来选择采用何种方式来解决TCP粘包问题。

【六】UDP协议

【1】什么是UDP协议

  • UDP(User Datagram Protocol)是一种无连接、不可靠的传输层协议。
    • 与TCP协议不同,UDP不需要在发送数据之前建立连接,也不保证数据能够成功到达接收方。
    • UDP主要用来传输实时性要求较高,但数据重要性不高的应用,比如视频、音频、游戏等领域。

【2】UDP协议的特点包括

  • 无连接:

    • UDP在传输数据时无需事先建立连接,减少了数据传输的延迟时间,并且不需要对每个数据包进行确认或者重传,因此可以提高数据传输速度。
  • 不可靠:

    • UDP无法保证每个数据包都能够成功到达目标节点,如果发生丢包或者乱序的情况,UDP并不会进行重传,数据丢失可能会影响应用的正确性和完整性。
  • 简单:

    • UDP协议简单,头部只有8个字节,不包含复杂的控制信息,同时没有超时重传、拥塞控制等机制,所以实现简单、效率较高。
  • 支持一对一、一对多、多对多的数据传输方式:

    • UDP可以支持点对点的数据传输,也可以支持一对多和多对多的数据传输。
  • 适用于实时数据传输:

    • 由于UDP协议无需等待握手过程和确认应答过程,所以它能够快速向目标端传输数据,适合实时性要求较高的信息传输,比如语音、视频、游戏等领域。

UDP协议相比于TCP协议缺少了一些重要的特性,但是对于那些实时性要求高、但是数据完整性不是最关键的应用来说,UDP是一个较好的选择。

【七】TCP协议

【1】什么是TCP协议

  • TCP(Transmission Control Protocol)是一种面向连接、可靠的传输层协议。
    • 它提供了诸如流量控制、重传机制、拥塞控制等特性,以保证数据的可靠传输。

【2】TCP协议的特点包括

  • 面向连接:

    • 在TCP传输数据之前,发送和接收端必须建立一个连接。
    • 这种连接是可靠的,在连接的整个过程中,数据的发送和接收都需要经过一系列复杂的步骤来确保连接可靠性和有效性。
  • 可靠性高:

    • TCP协议通过各种手段实现对数据传输的可靠性保证,如确认应答、重传机制、流量控制、拥塞控制等。
    • 这些机制可以保证数据能够成功到达目标节点,且不会发生数据重复或者丢失等情况。
  • 简单:

    • 虽然TCP协议比UDP协议复杂,但TCP的头部还是很简洁的,只有20个字节左右。
  • 支持点对点的传输:

    • TCP协议支持点对点的数据传输,一次只能够在发送端和接收端进行传输。
    • 在传输端与接收端进行数据交换时,TCP协议要保证无差错、按照正确的顺序和及时交付的基本过程。
  • 适用于大量的数据传输:

    • TCP协议在传输过程中具有可靠性好、无数据丢失和可避免拥塞等优势。
    • 因此,它通常用于如HTTP、FTP等高速数据传输领域。

总之,TCP协议相较UDP协议增加了连接控制、可靠性确认、会话保持等特性,使数据传输更加可靠、完整和安全。

【八】socketserver模块

【1】什么是socketserver模块

  • socketserver 模块是 Python 标准库中的一个模块
    • 提供了一个用于编写网络服务器的框架
    • 并且实现了基于网络的通信协议如 TCP、UDP 等。
    • 它可以让程序员无需深入底层的 socket 编程,而是更关注服务端和客户端交互的逻辑。

【2】用途

  • 通过 socketserver 模块,Python 程序员可以很方便地创建多种类型的 Socket 服务器,包括:

    • TCPServer

      • 基于 TCP 协议的 Socket 服务器。
    • UDPServer

      • 基于 UDP 协议的 Socket 服务器。
    • UnixStreamServer

      • 基于 Unix 本地套接字的 Socket 服务器。
    • UnixDatagramServer

      • 基于 Unix 本地套接字的单向数据报 Socket 服务器。

这些服务器支持多线程、多进程、异步 I/O 等方式,使得开发者能够根据不同的场景选择适合的方式。

【3】使用步骤

  • 使用 socketserver 模块编写 Socket 服务器通常需要以下几个步骤:

    • 继承 BaseRequestHandler 类:

      • 该类定义了处理客户端请求的方法,继承该类并实现其中的 handle 方法。
    • 继承 TCPServer 或其他类型的服务器类:

      • 创建相应类型的 Socket 服务器,指定自定义的处理器类和服务器地址、端口等参数。
    • 启动服务器:

      • 调用服务器对象的 serve_forever 方法,开始运行服务器。

总之, socketserver 模块为 Python 开发者提供了一种方便、快捷、高效的方法,使得他们能够轻松地编写 Socket 服务器并处理客户端请求。

并发编程部分

【一】操作系统发展史

  • 操作系统是计算机的核心组件之一

    • 是介于应用程序与硬件之间的软件
    • 它为计算机用户和底层硬件提供了一个标准化的接口。
  • 随着计算机技术的发展,操作系统也经历了多个阶段的演进。下面是操作系统发展史的概括:

  • (1)手动编程阶段

    • 在计算机诞生之初,程序员需要手工布线和配置计算机硬件,以便电子管、晶体管等物理部件能够正常运行。
    • 这个阶段的操作系统其实是没有操作系统的。
  • (2)批处理阶段

    • 1950年代,出现了第一个带有操作系统的机器UNIVAC。
    • 在这个阶段,将具有相似功能的任务集在一起执行并按顺序递交给操作系统,操作系统一次性地完成所有任务后将结果输出。
  • (3)分时操作系统阶段

    • 1960年代,随着存储器的容量和速度的提高,IBM开发出了分时操作系统OS/360。同时UNIX操作系统也在开发中。多用户分时操作系统使得多个终端可以同时连接到主机上,大大提高了计算效率,人们可以在各自的终端上进行独立的任务。
  • (4)个人计算机阶段

    • 1980年代,随着微型计算机在个人和小型企业中的广泛应用,个人计算机操作系统开始蓬勃发展。
    • 微软推出了DOS操作系统,Apple推出了Macintosh系统。
  • (5)网络时代阶段

    • 1990年代至今,互联网飞速发展,客户端-服务器网络结构逐渐普及,操作系统迅速向网络化方向发展。
    • UNIX和Windows NT成为市场主流操作系统,Linux也逐渐流行。

总之,操作系统是计算机科学的重要组成部分,经过数十年的演进,各种类型的操作系统呈现出多样性和统一性并存的状态,并扮演着至关重要的角色。

【二】多道技术

【1】什么是多道技术

  • 多道技术(Multiprogramming)指在计算机内同时运行多个程序
    • 由操作系统负责控制和管理这些程序的调度。
  • 每个程序都有独立的内存空间和CPU时间片
    • 在不同的程序间切换
    • 以最大化地利用CPU资源
    • 提高计算机系统的效率。

【2】多道技术主要分为两种模式:批处理和交互式。

(1)批处理模式

  • 批处理是指将多个用户提交的相同或类似的任务进行批量处理。
    • 批处理系统先将作业序列按照优先级、文件大小等因素排序
      • 然后挑选出最高优先级的作业进入内存执行
    • 当该作业I/O等待时
      • 操作系统将控制权切换到其他作业执行。
    • 再次轮到该作业时
      • 操作系统从中断点继续执行。

(2)交互式模式

  • 交互式模式是一种用户与计算机交互的方式
    • 用户可以通过计算机屏幕或终端输入命令并得到实时反馈。
  • 在这种模式下
    • CPU时间片被切分成较小的时间段
    • 多个用户分时共享CPU。
    • 用户的输入和输出同时进行
    • 而操作系统必须及时响应用户的请求
    • 及时切换进程。

多道技术有效提高了计算机的利用率,减少了资源浪费,提高了计算机的处理效率和可靠性。

而且,多道技术也对后来的计算机操作系统的设计产生了很大的影响,是操作系统从单任务到多任务发展的必要条件之一。

【三】进程理论

【1】什么是进程

  • 进程是指在操作系统中正在运行的程序实体
    • 它代表了一个正在执行中的程序以及与该程序相关联的所有状态信息。
    • 在进程理论中,进程被视为计算机资源的基本分配单位。

【2】进程的机制

  • 进程理论最重要的概念之一就是上下文切换(Context Switch)
    • 也就是在并发执行多个进程时
    • 操作系统必须及时将处理器从一个进程切换到另一个进程。
  • 在进行上下文切换时
    • 操作系统需要保存当前进程的上下文信息(如寄存器状态、虚拟内存等)以便之后可以恢复该进程的执行状态
    • 同时加载下一个进程的上下文信息。
  • 上下文切换是操作系统保证多进程协同工作的重要机制之一。

【3】进程理论中几个重要概念

(1)进程的状态:

进程可以处于就绪、运行或阻塞三种状态之一。

  • 就绪状态表示进程已经准备好运行
    • 但是处理器暂时没有空闲时间;
  • 运行状态表示进程正在执行;
  • 阻塞状态表示进程被暂停执行
    • 直到某些事件(比如等待I/O操作)完成后再继续执行。

(2)进程控制块:

  • 进程控制块(Process Control Block, PCB)是操作系统为每个进程所创建的数据结构
    • 用于保存进程本身的状态信息(比如进程ID、进程优先级、进程状态、进程上下文等)。
  • PCB可以看作是程序和操作系统之间交互的纽带
    • 在进程需要进行状态切换时
    • 操作系统使用该数据结构记录并管理进程状态。

(3)进程同步和进程通信:

  • 当多个进程同时访问共享资源时,需要进行进程同步和协调。
    • 进程同步机制包括信号量、互斥量、条件变量等方式,以保证多个进程按照特定的顺序访问共享资源。
    • 而进程通信机制则包括管道、消息队列、共享内存等方式,让多个进程之间可以相互传递信息、协调工作,完成复杂的任务。

【四】开启进程的两种方式

【1】基于process方法创建对象

  • 在Python中,可以使用multiprocessing模块来创建进程。
    • 其中,可以通过Process方法来创建一个进程对象。
  • 下面是一个简单的示例代码
    • 演示如何基于Process方法创建一个进程对象:
import multiprocessing

def foo():
    print("This is child process")

if __name__ == '__main__':
    p = multiprocessing.Process(target=foo)
    p.start()
    p.join()
  • 在这个示例中,首先定义了一个函数foo用于作为子进程要执行的任务。
    • 然后,在主程序中,通过Process方法创建了一个进程对象p
    • 指定了其要执行的任务为foo
    • 调用p.start()启动进程,并通过调用p.join()等待该进程结束。

需要注意的是,如果不加if __name__ == '__main__': 判断语句

​ 在Windows系统下会导致程序执行出现异常。

这是因为在Windows系统中,每个Python脚本文件都被看作一个新的进程

​ 如果不加判断语句,一旦在子进程中创建了新的进程对象,会无限递归导致无法正常结束程序

而加上判断语句便可避免这种情况的发生。

【2】基于继承process的类创建进程对象

  • 利用multiprocessing模块也可以基于继承Process的类来创建进程对象。

    • 下面是一个示例代码
    • 演示如何基于继承Process的类来创建一个进程对象:
import multiprocessing

class MyProcess(multiprocessing.Process):
    
    def run(self):
        print("This is child process")

if __name__ == '__main__':
    p = MyProcess()
    p.start()
    p.join()
  • 在这个示例中
    • 首先定义了一个继承自Process的类MyProcess
      • 该类包含了一个run()方法,表示进程要执行的任务。
    • 在主程序中,通过实例化该类来创建一个进程对象p
      • 并调用p.start()方法启动进程
      • 再调用p.join()方法等待该进程结束。

需要注意的是

在继承Process的类中必须包含一个run()方法

该方法就是进程要执行的任务

所有具体的子类需要重写这个方法

否则会抛出 NotImplementedError 异常。

【五】互斥锁

【1】什么是互斥锁

  • 互斥锁(Mutex)是一种同步机制
    • 用于保护对共享资源的访问
    • 避免多个线程同时修改共享数据造成的竞争条件。
  • 在进程间通信和多线程编程中都经常使用互斥锁。

【2】互斥锁有什么用

  • 当多个线程需要访问共享数据时
    • 通过申请互斥锁来获得对共享数据的访问权限
    • 执行完修改操作后再释放锁
    • 让其他线程可以继续访问共享数据。

Python中的threading模块提供了Lock、RLock等互斥锁的实现类,还有一些其他同步机制,如信号量Semaphore、事件Event等。

使用互斥锁需要注意死锁问题,即多个线程相互等待对方释放锁而无法继续执行的情况。

【3】什么是死锁

  • 死锁是指两个或多个线程都在等待对方释放资源的情况下互相卡住
    • 无法继续执行或退出。
import threading

# 模拟死锁:t1和t2互相等待对方释放锁
lock1 = threading.Lock()
lock2 = threading.Lock()

def func1():
    lock1.acquire()
    print("func1 acquire lock1")
    lock2.acquire()
    print("func1 acquire lock2")
    lock2.release()
    lock1.release()

def func2():
    lock2.acquire()
    print("func2 acquire lock2")
    lock1.acquire()
    print("func2 acquire lock1")
    lock1.release()
    lock2.release()

t1 = threading.Thread(target=func1)
t2 = threading.Thread(target=func2)

# 死锁情况,需要手动终止程序
t1.start()
t2.start()
  • 在以上代码中,func1()func2()模拟了死锁的情况
    • 首先线程t1申请锁lock1,再申请锁lock2;
    • 而线程t2则是相反的顺序,首先申请锁lock2,再申请锁lock1。
    • 如果两个线程同时执行,则会陷入互相等待的情况,进入死锁状态。

【4】什么是递归锁

  • 递归锁是一种特殊的互斥锁
    • 允许同一个线程多次获得该锁而不会造成死锁。
  • 递归锁实现了"可重入性",即同一线程可以在持有锁的情况下多次申请该锁而不会被自身所阻塞。
import threading

# 演示递归锁:t3申请了多次锁但由于是同一线程不会被阻塞
lock = threading.RLock()
def func3(n):
    if n <= 0:
        return

    lock.acquire()
    print("func3 acquire lock: ", n)
    func3(n-1)
    lock.release()

t3 = threading.Thread(target=func3, args=(5,))
t3.start()
  • 在递归锁的例子中
    • func3()函数一开始会获取锁lock,然后递归调用自身,并多次申请锁
    • 由于这些锁都是同一线程获得,因此即使申请多次,也不会被阻塞。
    • 运行该代码可以看到,输出信息依次打印,并没有出现死锁等异常情况。

【六】生产者消费者模型

  • 生产者-消费者模型是一种多线程协作的经典问题。
    • 它涉及到两类线程,生产者和消费者,共同操作一个有限缓冲区,其中生产者是向缓冲区放入数据的线程,而消费者则是从缓冲区读取数据的线程。
  • 该模型的主要目的是解决在生产者和消费者之间进行交互和同步操作
    • 以防止缓冲区溢出或者消费者试图访问空缓冲区的情况。
  • 下面是一个简单的Python实现:
import threading
import time

# 假设缓冲区最多可存储10个物品
buffer_size = 10
buffer = []
buffer_lock = threading.Lock()
buffer_not_full = threading.Condition(buffer_lock)
buffer_not_empty = threading.Condition(buffer_lock)

class Producer(threading.Thread):
    def run(self):
        for i in range(20):  # 生产者往缓冲区中生产20个物品
            time.sleep(0.5)
            buffer_lock.acquire()
            while len(buffer) == buffer_size:  # 如果缓冲区已满,则释放锁等待消费者消费
                buffer_not_full.wait()
            buffer.append(i)
            print("Producer produced", i)
            buffer_not_empty.notify()  # 唤醒等待缓冲区非空的线程
            buffer_lock.release()

class Consumer(threading.Thread):
    def run(self):
        for i in range(20):  # 消费者从缓冲区中消费20个物品
            time.sleep(1)
            buffer_lock.acquire()
            while not buffer:  # 如果缓冲区为空,则释放锁等待生产者生产
                buffer_not_empty.wait()
            item = buffer.pop(0)
            print("Consumer consumed", item)
            buffer_not_full.notify()  # 唤醒等待缓冲区未满的线程
            buffer_lock.release()

if __name__ == '__main__':
    producer = Producer()
    consumer = Consumer()
    producer.start()
    consumer.start()
    producer.join()
    consumer.join()
  • 在以上代码中
    • 创建了两个线程Producer和Consumer
      • 分别模拟了生产者和消费者
      • 通过继承Thread类并实现run()方法来定义线程操作。
    • buffer是缓冲区
      • buffer_lock是一个互斥锁用于加锁保护buffer。
      • buffer_not_full和buffer_not_empty是条件变量
        • 分别用于生产者等待缓冲区未满和消费者等待缓冲区非空。
  • 当生产者线程启动时
    • 每隔0.5秒往缓冲区中生产一个物品。
      • 如果缓冲区已满,则释放互斥锁
    • 并调用buffer_not_full.wait()将当前线程阻塞
      • 直到被其他线程唤醒(例如消费者消费了一些物品后导致缓冲区未满)。
    • 然后再加锁继续执行循环。
  • 当消费者线程启动时
    • 每隔1秒从缓冲区中取出一个物品。
    • 如果缓冲区为空,则释放互斥锁,并调用buffer_not_empty.wait()
    • 将当前线程阻塞,直到被其他线程唤醒(例如生产者生产了一些物品导致缓冲区非空)。
    • 然后再加锁继续执行循环。
  • 该程序运行时,可以看到生产者和消费者的操作交替进行,并且保证不会出现缓冲区溢出或消费者试图访问空缓冲区的情况。

【七】线程理论

【1】什么是线程

  • 线程是操作系统中能够独立执行的最小单位。
    • 每个线程都有自己的程序计数器、寄存器和栈,但是共享该进程中的其他资源,如内存和文件句柄。线程在进程内部运行,因此多个线程可以并发执行。

【2】线程具有以下特性:

  • 线程轻量级:

    • 相对于进程而言,线程非常轻量,创建和撤销线程的开销很小。
  • 共享内存:

    • 线程可以访问所属进程的地址空间,即可以读写相同的变量和数据结构,因此通信比较容易。
  • 调度:

    • 线程的调度由操作系统内核来完成,与进程无区别。
  • 同步互斥:

    • 线程之间可以通过信号量、互斥锁和条件变量等手段进行同步互斥操作。

【3】在多线程编程中,需要考虑以下几点:

  • 线程安全:

    • 多个线程同时访问同一份数据时可能产生竞态条件,需要采用同步互斥的方式保证线程安全。
  • 死锁:

    • 当两个或多个线程相互等待彼此释放占用的资源时,会发生死锁,导致线程无法向前推进。为避免死锁,应尽量避免多个线程同时占用多个资源。
  • CPU利用率:

    • 多个线程之间共享CPU时间片,需要合理设计线程数量和互斥操作等,以充分利用CPU资源。
  • 任务分配和负载均衡:

    • 当使用线程池进行任务调度时,需要考虑任务分配和负载均衡的问题,防止线程饥饿或过载等情况发生。

【八】开启线程的两种方式

【1】继承Thread类并重写其run()方法:

import threading

# 自定义线程类,继承Thread类
class MyThread(threading.Thread):
  
  # 重写run()方法,在其中编写需要执行的代码
  def run(self):
    for i in range(1, 11):
      print("Thread " + self.getName() + ": " + str(i))

# 在主函数中创建MyThread对象并启动
if __name__ == "__main__":
  thread1 = MyThread()
  thread2 = MyThread()
  
  thread1.start()
  thread2.start()

【2】实现Runnable接口并传入Thread类的构造函数中:

import threading

# 实现了Runnable接口的类
class MyRunnable:
  
  # 实现run()方法,在其中编写需要执行的代码
  def run(self):
    for i in range(1, 11):
      print("Thread: " + threading.current_thread().getName() + ", " + str(i))

# 在主函数中创建MyRunnable对象,并将其作为参数传入Thread构造函数中
if __name__ == "__main__":
  myRunnable = MyRunnable()
  
  thread1 = threading.Thread(target=myRunnable.run)
  thread2 = threading.Thread(target=myRunnable.run)
  
  thread1.start()
  thread2.start()

【九】GIL全局解释器锁

【1】什么是GIL锁

  • GIL(Global Interpreter Lock)是Python解释器中的一个重要概念
    • 它是一种线程同步机制,采用互斥锁来实现对Python解释器中解释器状态的控制,以保证同一时刻只有一个线程可以执行Python字节码。
    • 这意味着,即使在多核CPU上运行Python程序时,由于GIL的限制,多线程不能真正地并行执行,仅仅只是在单个CPU上实现了多个任务之间的切换。

【2】GIL锁的优点

  • GIL带来的好处是,简化了Python解释器内存管理机制和多线程调度机制,提高了解释器的稳定性和实现的简便性。
  • 但同时也意味着,在密集计算型和多线程高并发的场景下,CPU利用率将会受到较大的影响。

【3】GIL锁的应用

  • 因此,对于如CPU密集型计算、大规模数据处理等相关的应用场景,建议采用诸如多进程、协程等方式来替代多线程,并且对于需要进行I/O操作的场景,多线程仍然是一种较为有效的并发方式。

【十】进程池/线程池

【1】进程池

  • 进程池指的是一种预先创建好一定数量的进程,并将其集中管理。
  • 当有任务需要处理时,直接从进程池中取一个空闲的进程来执行任务,任务执行完毕后,该进程归还给进程池,等待执行新的任务。
  • 这样就避免了每次任务执行都需要重新创建进程的开销。
  • 进程池由于进程之间相互独立,避免了多进程之间的数据共享与同步问题,即使是计算密集型任务也能够很好的利用多核CPU资源。

【2】线程池

  • 线程池则是预先创建一定数量的线程对象,并将其集中管理。
  • 当有任务需要处理时,直接从线程池中取一个空闲的线程来执行任务,任务执行完毕后,该线程归还给线程池等待执行新的任务。
  • 线程池优点在于,相对于进程而言,线程具有更轻量级、更高效率,不需要像进程那样频繁地切换上下文,因此适合于I/O密集型任务或需要大量线程操作的场景。

【3】代码演示

# 导入相关库
import concurrent.futures
import time

# 定义一个耗时任务
def slow_task(seconds):
    print(f'开始执行任务:{seconds}秒')
    time.sleep(seconds)
    return f'任务执行完成,耗时{seconds}秒'

def main():
    # 创建进程池和线程池对象
    with concurrent.futures.ProcessPoolExecutor(max_workers=3) as proc_pool:
        with concurrent.futures.ThreadPoolExecutor(max_workers=5) as thread_pool:
            # 提交任务到进程池和线程池
            tasks = [proc_pool.submit(slow_task, i) for i in range(1, 7)]
            tasks += [thread_pool.submit(slow_task, i) for i in range(1, 8)]
            # 按顺序获取任务执行结果
            for future in concurrent.futures.as_completed(tasks):
                try:
                    result = future.result()
                except Exception as e:
                    print(f'任务执行出错:{e}')
                else:
                    print(result)

if __name__ == '__main__':
    main()
  • 这个示例首先定义了一个耗时任务slow_task,模拟需要执行一定时间的任务。
    • 然后,使用concurrent.futures.ProcessPoolExecutor()函数创建一个进程池对象
    • 使用concurrent.futures.ThreadPoolExecutor()函数创建一个线程池对象
    • 分别指定最大工作线程或进程数量。
    • 接着,使用submit()方法将任务提交到进程池和线程池中,并返回Future对象,代表任务的未来结果。
    • 最后,使用as_completed()函数按照任务提交的顺序获取各个任务的结果。
  • 在实际应用中,也可以根据实际情况通过调整max_workers参数来设置池中的线程或进程数量,以实现更好的性能优化。

【十一】协程的概念

【1】什么是协程

  • 协程(Coroutine)是一种用户态的轻量级线程,它由程序员控制、调度和携程状态的转换。
  • 与操作系统级别的线程相比,协程不需要进行线程切换的开销,不会出现资源竞争的问题,可以更加高效地利用CPU的资源,特别适合于I/O密集型任务。

【2】协程的原理

  • 协程的核心思想在于,一个进程可以拥有多个协程,这些协程之间共享进程的资源,并且能够相互配合完成一定的任务。
  • 协程本身是一个函数,函数执行过程中可以主动中断,然后转而执行其他协程,最后再返回上一次执行的位置继续执行。
  • 这种中断和继续执行的过程被称为“挂起”和“恢复”,通过这种方式,协程可以实现对任务执行的动态调度和状态保存。

【3】协程的实现

  • 在Python中,协程是通过asyncio模块来实现的。
  • 使用async def关键字定义一个协程函数,传统的函数定义方式不能够定义协程。
  • 协程函数内部可以使用await关键字暂停自己的执行,等待其他协程执行完成后再恢复执行。

协程是异步编程的重要组成部分,常用于处理网络通信和文件IO等阻塞操作。在Python 3.7及以后的版本中,通过asyncio模块可以快速构建异步应用程序。

【4】代码演示

import asyncio

async def coro1():
    print("Coroutine 1 started")
    await asyncio.sleep(2)
    print("Coroutine 1 finished")

async def coro2():
    print("Coroutine 2 started")
    await asyncio.sleep(1)
    print("Coroutine 2 finished")

async def main():
    task1 = asyncio.create_task(coro1())
    task2 = asyncio.create_task(coro2())
    await task1
    await task2

asyncio.run(main())
  • 代码中我们定义了3个协程函数coro1coro2main
    • 其中main函数是整个程序的入口。
    • main函数中,我们通过异步方式启动了两个协程任务,即task1task2,并且等待它们执行完成。
  • 协程任务的执行过程中,使用await关键字暂停了协程的执行并切换到其他协程执行,直到等待的异步IO操作完成后再返回原协程继续执行。
    • 在这个例子中,我们通过asyncio.sleep()函数模拟了一个阻塞的过程,使协程暂停一段时间后再继续执行。
  • 运行这个程序,我们可以看到以下输出:
Coroutine 1 started
Coroutine 2 started
Coroutine 2 finished
Coroutine 1 finished
  • 可以看到,两个协程函数是同时开始执行的,但是因为第二个协程等待的时间更短,所以先执行完成。
    • 整个程序的执行过程是异步的,有效地利用了CPU资源。

【十二】IO模型的了解

IO模型,又称为网络编程模型,在不同的操作系统中实现方式不同。常见的IO模型有5种:

【1】阻塞IO模型(Blocking IO)

  • 在这个模型中,当用户向内核发起一个系统调用时,应用程序会一直等待,直到内核返回结果再继续执行。
    • 因此,在执行一个系统调用期间,应用程序不能做其他事情,整个过程处于阻塞状态。
  • 这种模型会造成资源的严重浪费,因为应用一直在等待而不能执行其他任务。

【2】非阻塞IO模型(Non-blocking IO)

  • 非阻塞IO模型不同于阻塞IO模型,应用程序将会立即返回一个值或错误码,不管内核是否已经执行完成网络请求。
    • 如果之前请求还没有完成,则需要在稍后的时候重复调用系统调用,直到请求完成。
  • 这种模型可以很好地利用CPU资源,但是需要频繁地轮询,造成CPU的浪费。

【3】IO多路复用模型(IO Multiplexing)

  • 在这个模型中,内核可以异步监听多个socket连接的IO事件,收到通知后再进行处理。
    • 服务器通过一个系统调用(如select或poll等)等待多个client的请求,当发生了IO事件时,内核会通知服务器,并返回就绪的IO列表给应用程序,使得应用程序可以异步地处理多个IO请求。
  • 这种模型可以支持高并发,且不会频繁的轮询,因为只有当IO事件就绪时才会返回,而不是定时进行轮询。

【4】信号驱动IO模型(Signal-driven IO)

  • 这种模型在内核准备好数据时给应用程序发送一个信号,应用程序立刻启动读取数据操作。
  • 这个信号可以通过SIGIO实现,可以让内核将准备好的数据马上发送到应用程序。

【5】异步IO模型(Asynchronous IO)

  • 异步IO模型通过回调函数来实现异步操作,应用程序通过注册一个事件回调函数到事件循环中,当事件发生时,内核会直接调用回调函数。
  • 应用程序无需等待,可以直接返回执行其他任务。这种模型可以在操作系统支持异步IO的情况下实现真正的异步操作,但是复杂度比较高。

以上5种IO模型各有优缺点,根据实际应用场景来选择合适的模型。例如在高并发的网络编程中,通常采用IO多路复用模型来优化服务器性能。