文章目录
前言
Python是一种强大且易于学习的编程语言。通过这个21天的计划,我们将逐步深入并发编程。无论你是初学者还是有一定基础的开发者,这个计划都将帮助你巩固和扩展你的Python知识。
在学习本篇之前,我们先复习一下前面的内容:
day1:Python下载和开发工具介绍
day2:数据类型、字符编码、文件处理
day3:基础语法与课外练习
day4:函数简单介绍
day5:模块与包
day6:常用模块介绍
day7:面向对象
day8:面向对象高级
day9:异常处理
day10:网络编程
一、并发编程总体课程概述
在Python编程中,并发编程是一个非常重要的领域,它可以让程序更高效地利用系统资源,提高程序的性能。本课程将涵盖并发编程的多个方面,包括操作系统发展史、多道技术、进程调度、进程状态、同步异步与阻塞非阻塞等概念,以及创建进程的方法、进程间通信等实用技术。
课程内容大纲
- 操作系统发展史(了解):了解操作系统的发展历程,有助于理解并发编程的起源和背景。
- 多道技术(了解):多道技术是操作系统实现并发的基础,理解它能更好地掌握并发编程的原理。
- 进程调度(讲):介绍不同的进程调度算法,如先来先服务、短作业优先等。
- 进程三状态(就绪,阻塞,运行):掌握进程在不同阶段的状态变化。
- 同步异步 阻塞非阻塞:重点理解异步 + 非阻塞的概念,以及不同框架的同步异步特性。
- 创建进程的两种方式(重点):学习如何在Python中创建进程。
- join方法:用于等待子进程执行完成。
- 进程间数据相互隔离:了解进程之间数据的独立性。
- 僵尸进程与孤儿进程:认识这两种特殊的进程状态。
- 进程对象及其他方法:掌握进程对象的操作方法,如获取进程ID、判断进程是否存活等。
- 守护进程:了解守护进程的特点和使用场景。
- 互斥锁:用于解决多个进程对共享资源的竞争问题。
- 队列介绍:介绍队列的基本操作和使用方法。
- IPC机制:实现进程间的通信。
二、今日内容
2.1 进程调度
进程调度是操作系统对进程进行管理和分配CPU时间的过程,常见的调度算法有:
# 1 先来先服务
# 2 短作业优先
# 3 时间片轮转
# 4 多级反馈队列
2.2 僵尸进程与孤儿进程
僵尸进程
进程结束了,但资源还没来得及回收。
孤儿进程
主进程挂了,子进程还没结束,它会被专门的进程接管。
2.3 进程对象及其他方法
可以通过不同的方式获取进程的相关信息和操作进程。
# 1 windows:tasklist |findstr 进程id号
# 2 mac,Linux:ps aux | grep 进程id号
# 3 进程对象:t=Process(target=task, )或者是在进程内部:current_process()
# 4 t.pid或者current_process().pid 获取进程id号
# 5 os.getpid() 同上,获取进程id号
# 6 os.getppid() 获取父进程id号,子进程中获取父进程id,等于父进程的id号
# 7 t.is_alive()或者current_process().is_alive() 查看进程是否存活
# 8 t.terminate() 关闭进程,在主进程关闭
2.4 守护进程
守护进程是一种特殊的进程,主进程一旦结束,子进程也会结束。
from multiprocessing import Process,current_process
import time
import os
def task():
print(os.getpid())
print('子进程')
time.sleep(200)
print('子进程结束')
if __name__ == '__main__':
t = Process(target=task, )
# 守护进程:主进程一旦结束,子进程也结束
# t.daemon=True # 一定要加在启动之前
t.start()
time.sleep(1)
print('主进程结束')
2.5 互斥锁
互斥锁用于解决多个进程对共享资源的竞争问题,同一时间只有一个进程能获取到锁。
from multiprocessing import Process, Lock
import json
import time
import random
def search():
# 查票的函数
# 打开文件,读出ticket_count
try:
with open('ticket', 'r', encoding='utf-8') as f:
dic = json.load(f)
print('余票还有:', dic.get('ticket_count'))
except FileNotFoundError:
print("未找到 ticket 文件,请检查。")
def buy():
try:
with open('ticket', 'r', encoding='utf-8') as f:
dic = json.load(f)
time.sleep(random.randint(1, 3)) # 模拟一下网络延迟
if dic.get('ticket_count') > 0:
# 能够买票
dic['ticket_count'] -= 1
# 保存到文件中去
with open('ticket', 'w', encoding='utf-8') as f:
json.dump(dic, f)
print('买票成功')
else:
# 买票失败
print('买票失败')
except FileNotFoundError:
print("未找到 ticket 文件,请检查。")
# 写一个函数,先查票,再买票
def task(mutex):
search()
# 买票过程要加锁
with mutex:
buy()
if __name__ == '__main__':
# 检查 ticket 文件是否存在,如果不存在则创建并初始化票数
try:
with open('ticket', 'r', encoding='utf-8') as f:
pass
except FileNotFoundError:
initial_tickets = {'ticket_count': 5} # 初始化票数为 5
with open('ticket', 'w', encoding='utf-8') as f:
json.dump(initial_tickets, f)
# 锁的创建,在哪?主进程创建锁
mutex = Lock() # 创建一把锁
# 模拟十个人买票(开10个进程)
processes = []
for i in range(10):
t = Process(target=task, args=(mutex,))
processes.append(t)
t.start()
# 等待所有进程结束
for p in processes:
p.join()
print("所有进程执行完毕。")
2.6 队列介绍
队列是一种先进先出的数据结构,可用于进程间的数据传递。
from multiprocessing import Queue
# 实例化得到要给对象
q = Queue(5) # 默认很大,可以放很多,写了个5,只能放5个
# 往管道中放值
q.put(1)
q.put('lqz')
q.put(18)
q.put(19)
# q.put(20)
# q.put(21)
# q.put_nowait(100)
# 从管道中取值
# print(q.get())
# print(q.get())
# print(q.get())
# print(q.get(timeout=100)) # 等0.1s还没有值,就结束
# print(q.get_nowait()) # 不等了,有就是有,没有就没有
print(q.empty()) # 看一下队列是不是空的
print(q.full()) # 看一下队列是不是满的
# 总结:
'''
q=Queue(队列大小)
# 放值
q.put(asdf)
q.put_nowait(asdf) # 队列满了,放不进去就不放了,报错
# 取值
q.get() # 从队列头部取出一个值
q.get_nowait() # 从队列头部取值,没有就抛错
# 队列是否为空,是否满
print(q.empty()) # 看一下队列是不是空的
print(q.full()) # 看一下队列是不是满的
'''
2.7 IPC机制(进程间通信)
IPC机制用于实现进程间的通信,通过队列可以方便地在不同进程之间传递数据。
from multiprocessing import Process, current_process, Queue
import time
import os
def task1(q):
print('我是task1进程,我的id号是:%s'%os.getpid())
q.put('lqz is handsome')
def task2(q):
# res=q.get()
# print('我是task2进程,我的id号是:%s'%os.getpid(),res)
print('我是task2进程,我的id号是:%s'%os.getpid())
if __name__ == '__main__':
q = Queue(5)
t1 = Process(target=task1, args=(q,))
t1.start()
t2 = Process(target=task2, args=(q,))
t2.start()
print(q.get())
三、其他知识点补充
3.1 操作系统发展史
操作系统发展史可以大致分为以下几个阶段:
3.1.1 手工操作阶段 (1940s - 1950s):
- 特点: 没有操作系统,程序员直接操作硬件。程序和数据通过纸带或打孔卡输入。
- 缺点: 效率极低,资源利用率极低,程序员需要了解硬件细节。
3.1.2 批处理系统阶段 (1950s - 1960s):
- 特点: 将多个作业(程序和数据)成批地输入计算机,由操作系统自动依次执行。
- 优点: 提高了效率,减少了人工干预。
- 缺点: 用户不能直接与计算机交互,作业执行过程中出现错误难以处理,资源利用率仍然不高。 分为单道批处理(一次只能运行一个作业)和多道批处理(多个作业同时驻留在内存中,交替执行)。
3.1.3分时系统阶段 (1960s - 1970s):
- 特点: 允许多个用户同时使用计算机,每个用户分配一个时间片,轮流执行。用户可以通过终端与计算机交互。
- 优点: 用户可以及时得到响应,提高了交互性。
- 缺点: 需要更复杂的硬件和软件支持,安全性问题更加突出。
3.1.4 实时系统阶段 (1960s - 至今):
- 特点: 能够及时响应外部事件,并在规定的时间内完成处理。
- 应用: 工业控制、航空航天、医疗设备等对时间要求严格的领域。
- 分类: 硬实时系统(必须在严格的时间限制内完成任务)和软实时系统(允许一定的延迟)。
3.1.5 网络操作系统阶段 (1980s - 至今):
- 特点: 支持网络通信和资源共享,允许多台计算机通过网络协同工作。
- 代表: Windows NT Server, Linux, Unix
- 功能: 文件共享、打印服务、远程登录等。
3.1.6 分布式操作系统阶段 (1980s - 至今):
- 特点: 将多个计算机组成一个统一的系统,用户感觉就像在使用一台计算机。
- 目标: 提高系统的可靠性、性能和可扩展性。
- 挑战: 数据一致性、容错性、安全性等。
3.1.7 个人计算机操作系统阶段 (1980s - 至今):
- 特点: 专门为个人计算机设计的操作系统,注重用户友好性和易用性。
- 代表: MS-DOS, Windows, macOS, Linux
- 功能: 图形用户界面、应用程序支持、设备管理等。
3.1.8 移动操作系统阶段 (2000s - 至今):
- 特点: 专门为移动设备(如智能手机和平板电脑)设计的操作系统。
- 代表: Android, iOS
- 功能: 触摸屏支持、移动网络连接、应用程序商店等。
总结:
操作系统的发展是一个不断演进的过程,从最初的手工操作到现在的各种复杂的操作系统,其核心目标始终是提高计算机的效率、易用性和可靠性。 随着硬件技术的不断发展,操作系统也在不断地适应新的需求和挑战。
3.2 多道技术了解
多道技术是操作系统中的一项重要技术,它在计算机发展历程中起到了提高资源利用率和系统吞吐量的关键作用,以下为你详细介绍:
3.2.1 基本概念
多道技术是指在计算机内存中同时存放几道相互独立的程序,它们在管理程序控制之下,相互穿插地运行。当某道程序因某种原因(如等待I/O操作完成)不能继续运行下去时,管理程序便将另一道程序投入运行,这样可以使CPU及各外设尽量处于忙碌状态,从而提高计算机系统的整体效率。
3.2.2 实现原理
多道技术主要基于两个方面的原理来实现,分别是时间上的复用和空间上的复用:
- 时间复用:多个程序共享CPU时间。CPU以极快的速度在多个程序之间切换执行,由于切换速度非常快,用户感觉不到程序在交替执行,就好像多个程序在同时运行一样。例如,当一个程序进行I/O操作时,CPU会立即切换到另一个程序执行,避免了CPU的空闲等待。
- 空间复用:多个程序共享内存空间。操作系统将内存划分为多个区域,每个区域可以存放一个程序。通过合理的内存分配和管理,使得多个程序能够同时存放在内存中,提高了内存的利用率。
3.2.3 优点
- 提高CPU利用率:在单道程序系统中,程序在进行I/O操作时,CPU会处于空闲状态。而多道技术可以在程序进行I/O操作时,让CPU去执行其他程序,从而减少了CPU的空闲时间,提高了CPU的利用率。
- 提高内存和I/O设备利用率:多个程序同时存放在内存中,使得内存得到了更充分的利用。同时,多个程序可以同时使用不同的I/O设备,提高了I/O设备的利用率。
- 增加系统吞吐量:由于CPU、内存和I/O设备的利用率都得到了提高,系统可以在单位时间内完成更多的任务,从而增加了系统的吞吐量。
3.2.4 缺点
- 作业周转时间长:由于多个程序同时在内存中运行,每个程序的执行时间会受到其他程序的影响,导致作业的周转时间变长。
- 系统开销大:为了实现多道程序的并发执行,操作系统需要进行更多的管理和调度工作,如内存分配、CPU调度、进程同步等,这会增加系统的开销。
3.2.5 与单道程序系统的对比
- 单道程序系统:内存中只能有一道程序运行,程序执行时独占系统资源,直到该程序执行完毕。这种方式简单,但资源利用率低。
- 多道程序系统:内存中可以同时有多个程序运行,多个程序共享系统资源,通过时间和空间的复用提高了资源利用率,但系统管理相对复杂。
3.2.6 在现代操作系统中的应用
现代操作系统都广泛采用了多道技术。例如,在Windows、Linux等操作系统中,用户可以同时打开多个应用程序,如浏览器、文本编辑器、音乐播放器等,这些应用程序在操作系统的管理下并发执行,充分利用了系统资源。
3.3 同步异步 阻塞非阻塞
在Python的Web开发中,不同的框架具有不同的同步异步特性。
# flask,django3.0以前,都是同步框架
# tornado,sanic,fastAPI 异步框架
3.4 创建进程的两种方式(重点)
第一种方式:使用Process类的target参数
from multiprocessing import Process
import time
def task(n):
print('我是子进程')
time.sleep(n)
print('子进程结束')
if __name__ == '__main__':
# args=(), kwargs={}
# t=Process(task,args=(1,))
t = Process(target=task, kwargs={'n': 1})
t.start() # 通知操作系统,开启进程,执行task函数
print('主')
第二种方式:继承Process类并重写run方法
from multiprocessing import Process
import time
class Task(Process):
def __init__(self, n):
super().__init__()
self.n = n
def run(self):
print('我是子进程')
time.sleep(self.n)
print('子进程结束')
if __name__ == '__main__':
t = Task(1)
# t.run(1) # 不是调用t.run(),而是调用t.start()
t.start()
print('主')
3.5 join方法
join
方法用于等待子进程执行完成。
from multiprocessing import Process
import time
def task(n):
print('我是子进程')
time.sleep(n)
print('子进程结束')
if __name__ == '__main__':
ctime = time.time()
t = Process(target=task, kwargs={'n': 1})
t2 = Process(target=task, kwargs={'n': 2})
t.start()
t2.start()
t.join() # 等待t子进程执行完成
t2.join() # 等待t2子进程执行完成
print('主')
ctime2 = time.time()
print(ctime2 - ctime)
3.6 进程间数据相互隔离
进程之间的数据是相互隔离的,一个进程对数据的修改不会影响其他进程。
from multiprocessing import Process
import time
age = 18
def task(n):
global age # 局部修改全局
age = 99
print('我是子进程')
time.sleep(n)
print('子进程结束')
print(age)
if __name__ == '__main__':
t = Process(target=task, kwargs={'n': 1})
t.start()
t.join() # 等待t子进程执行完成
print('主')
print(age) # 数据没有变,主进程中打印age和子进程的age没有半毛钱关系,数据是隔离的
结语
通过这个21天的Python计划,我们涵盖了并发编程。希望这些内容能帮助你更好地理解和使用Python。继续学习和实践,你将成为一名优秀的Python开发者!