python多进程多线程及协程
1.操作系统知识
1.1 LINUX五大子系统
# linux系统的五大子系统:
# cpu:进程调度 进程通信
# 磁盘:文件系统
# 内存:内存管理
# 网络:网络接口
1.2 计算机组成及资源分类
# 计算机组成:cpu+存储+IO
# 资源包括:计算资源+存储资源
1.3 cpu时间片概念
# cpu的时间片(抽象概念):
# 时间片即CPU分配给各个程序的时间,每个线程被分配一个时间段,称作它的时间片,
# 即该进程允许运行的时间,使各个程序从表面上看是同时进行的。
# 如果在时间片结束时进程还在运行,则CPU将被剥夺并分配给另一个进程。
# 如果进程在时间片结束前阻塞或结束,则CPU当即进行切换。而不会造成CPU资源浪费。
# 在宏观上:我们可以同时打开多个应用程序,每个程序并行不悖,同时运行。
# 但在微观上:由于只有一个CPU,一次只能处理程序要求的一部分,如何处理公平,
# 一种方法就是引入时间片,每个程序轮流执行
# 程序的上下文切换:上下文保存上一次进程的的运行记录,当从一个程序切到另一个程序时,是切换到另一个程序的上下文
1.4 并发和并行
# 并发:某段时间内交替进行
# 并行:同时执行(某一时刻,多个任务执行) --多核 多个cpu
1.5 南北桥
# 南桥芯片(South Bridge)是主板芯片组的重要组成部分,一般位于主板上离CPU插槽较远的下方,
# 这种布局是考虑到它所连接的I/O总线较多,离处理器远一点有利于布线。
# 一块电脑主板,以CPU插座为北的话,靠近CPU插座的一个起连接作用的芯片称为“北桥芯片”,
# 英文名:North Bridge Chipset。北桥芯片就是主板上离CPU最近的芯片,这主要是考虑到北桥芯片与
# 处理器之间的通信最密切,为了提高通信性能而缩短传输距离。
# 南北桥芯片的差异:
# 相对于北桥芯片来说,其数据量并不算大,所以南桥芯片一般都没有覆盖散热片,但高档的主板的南桥也覆盖散
# 热片。南桥芯片不与处理器直接相连,而是通过一定的方式与北桥芯片相连
1.6 cpu的两种状态及内存空间
# cpu两种状态:用户态(运行用户程序) 内核态(运行内核程序)
# 内存空间:用户空间(内存专门分出给用户使用)和内核空间
# (内存专门分出给内核使用)
# 优点:安全 稳定
1.7 线程和进程
# 线程和进程
# 线程是操作系统进行调度的最小单位
# 进程是操作系统进行资源分配的最小的单位
# 进程包含线程,一个进程之上可以包含多个线程
# 进程组成
# PCB(process control block) 它是进程的唯一标识 --本质上是数据结构
# PCB包含:pid(process identification)、fd(file description)、有效用户、文件描述符的指针、数据段、代码段等
# 进程和线程的关系
# 一个进程可以有一个以上的线程 这些线程都是共享这个进程的内存空间
# 不同进程之间的内存空间都是独立的
# 创建新的线程很简单 创建一个新的进程都需要对其父进程进行一次克隆
# 一个线程可以控制和操作同一个进程里的其他线程 进程只能操作子进程
# 一个主线程改变 可能会影响其他线程 但改变父进程不会影响子进程
# 进程三态模型 --就绪态 运行态 阻塞态
# 就绪态 通过调度算法-->运行态
# 运行态 当进程的cpu时间片到了-->就绪态
# 运行态 等待某事件如I/O请求-->阻塞态
# 阻塞态 当I/O结束或者等待的事情发生了-->就绪态
# 阻塞态--如磁盘加载速度同内存和cpu速度不是同一个数量级,导致的进程阻塞
# 进程五态模型 --就绪态 运行态 阻塞态 新建态 结束态
# 新建态 创建进程
# 结束态 杀死进程
# linux进程状态五种状态
# R TASK_RUNNING 可执行状态
# S TASK_INTERRUPTIBLE 可中断的睡眠状态
# D TASK_UNINTERRUPTIBLE 不可中断的睡眠状态
# 不响应异步信号,但是对cpu的信号还是会响应的
# T stopped 停止状态
# Z zombie 僵尸状态
2.线程
2.1 thread模块使用及join()方法阻塞线程的现象实验
# 考虑代码优化空间-- 速度和存储方面
# 线程被称为轻量级进程(Lightweight Process,LWP),是cpu调度的基本单位
# 组成:线程ID、当前指令指针(PC)、寄存器集合、堆栈组成
# 多线程:在单个程序中同时运行多个线程完成不同的工作
# Thread构造方法
# 构造方法: Thread(group=None, target=None, name=None, args=(), kwargs={})
# group: 线程组,目前还没有实现,库引用中提示必须是None;
# target: 要执行的方法;
# name: 线程名;
# args/kwargs: 要传入方法的参数。
# Thread实例方法
# t.name:获取或设置线程的名称
# t.getName()/setName(name): 获取/设置线程名。
# t.is_alive()、t.isAlive():判断线程是否为激活状态。返回线程是否在运行。正在运行指启动后、终止前。
# t.ident :获取线程的标识符。线程标识符是一个非零整数,只有在调用了start()方法之后该属性才有效,否则它只返回None
# t.run() :线程被cpu调度后自动执行线程对象的run方法
# t.start(): 线程准备就绪,等待CPU调度,start会自动调用t.run()
# t.join([timeout]): 阻塞当前上下文环境的线程,直到调用此方法的线程终止或到达指定的timeout(可选参数)。
# setDaemon(bool): 设置是后台线程(默认前台线程(False))。(在start之前设置)
# 如果是后台线程,主线程执行过程中,后台线程也在进行,主线程执行完毕后,后台线程不论成功与否,主线程和后台线程均停止
# 如果是前台线程,主线程执行过程中,前台线程也在进行,主线程执行完毕后,等待前台线程也执行完成后,程序停止
# t.isDaemon:判断是否为后台线程
import requests
import time
from threading import Thread # 从threading模块导入Thread类
# 定义检测函数执行时间的装饰器
def outer(f): # f为传入的函数,必须是callable对象
def inner():
start = time.time()
result = f()
end = time.time()
print(f"执行{f.__name__}花费{end-start}s")
return end - start
return inner
# 同过requests模块来获取百度网页文本内容
def get_content(url):
text = requests.get(url).text
time.sleep(0.5)
print("get context")
# 使用装饰器来装饰main()函数
@outer # 底层实现是:main = outer(main)
def main():
t_list = []
for i in range(5):
# 创建子线程
# target --> 指定要传入的方法名字 要做什么
# args -->指定方法需要传入的参数 元组类型
t = Thread(target=get_content, args=("https://www.baidu.cn",))
# get_content("https://www.baidu.com")
t_list.append(t)
# 设置后台线程 主线程退出 子线程也退出
t.setDaemon(True) # 默认为前台进程,即False,即此时主线程退出,子线程没有退出,则主线程等待子线程完成后再退出
t.start() # 启动子线程 自动执行t.run()
# t.join() # 阻塞主线程 等待1号线程完成 再去创建2号线程...
[t.join() for t in t_list] # 阻塞当前环境 即阻塞主线程
# 阻塞当前环境上下文 直到为t的5个子线程都执行完成
print('end.......')
main()
2.2 使用自定义线程类
import threading # 导入threading线程生成模块
class MyThread(threading.Thread): # 自定义线程类,继承threading.Thread类
def __init__(self, num): # 重写类的初始化方法,定义num属性,及继承super的初始化方法
super().__init__()
self.num = num
def run(self): # 定义线程类的run方法
print(f"running on number:{self.num}")
t1 = MyThread(1) # 初始化类即生成线程t1
t2 = MyThread(2) # 初始化类即生成线程t2
t1.start() # 启动线程,自动指向run()方法
t2.start() # 启动线程,自动指向run()方法
# 结果:
# running on number:1
# running on number:2
2.3 各种锁
# 锁的作用:在多线程中使用lock可以让多个线程在共享资源的时候遵循一定的规则。
# 常见锁类型:
# Lock()/RLock:普通锁(互斥锁)
# 解决资源争用,数据读取不一致等
# Semaphore :信号量
# 最多允许同时N个线程执行内容
# Event: 事件锁
# 根据状态位,决定是否通过事件
# Condition: 条件锁
2.3.1 互斥锁
# 互斥锁 LOCK/RLOCK
# 互斥锁就是解决资源征用,数据读取不一致等问题
# 当出现公共资源争用时,使用互斥锁来解决资源征用问题
# 互斥锁原理:锁只有一把,每次只能一个线程获得锁,然后进入执行区,最后释放锁给下一个线程实现排队效果,解决资源抢占问题
# 实例:
import time
import threading
num = 0 # 公共资源
def sum_num(i):
print(f"lock start ... {i}") # 提示锁开始的位置
# lock.acquire() # 获得锁
with lock: # 获得锁,并自动释放锁
global num
time.sleep(0.5)
num += i
print(num)
# lock.release() # 释放锁
print(f"lock end ... {i}") # 提示锁结束的位置
# lock = threading.Lock() # 创建互斥锁对象
lock = threading.RLock() # 创建互斥锁对象
for i in range(5):
t = threading.Thread(target=sum_num, args=(i,))
t.start()
# 结果:
"""
lock start ... 0
lock start ... 1
lock start ... 2
lock start ... 3
lock start ... 4
0
lock end ... 0
1
lock end ... 1
3
lock end ... 2
6
lock end ... 3
10
lock end ... 4
"""
2.3.2 死锁
# LOCK 原始锁 获取锁之前不做判断 直到获取锁为止
# RLOCK 重入锁 获取锁之前先判断 如果自己有了锁,那就立即放回锁
# 第一种 死锁
# 死锁--Lock 获取锁之前不做判断,直到获取锁为止
import threading # 当如threading模块
lock = threading.Lock() # 生成一把锁
lock.acquire() # 获得锁
print("lock acquire...1")
lock.acquire() # 再次获得锁,当时由于是原始锁,导致无法获得锁时,只会干耗时间,导致死锁
print("lock acquire...2")
lock.release() # 释放锁
print("lock release")
lock.release() # 释放锁
print("lock release")
# RLock 获取锁之前判断当前主线程是否有锁,自己有锁则把锁返回
import threading
lock = threading.RLock() # 生成一把锁
lock.acquire() # 获得锁
print("lock acquire...1")
lock.acquire() # 再次获得锁,此时是重入锁,则会事前判断当前线程是否有锁,若有则返回锁再获取锁,若无则直接获取锁,不会死锁
print("lock acquire...2")
lock.release()
print("lock release")
lock.release()
print("lock release")
# # 结果:
# # lock acquire...1
# # lock acquire...2
# # lock release
# # lock release
# 第二种 死锁
from threading import Thread, Lock # 从threading模块中导入Thread和Lock类
from time import sleep
class Account: # 定义账户类
# 每个账户自带一把锁
# 当balance发生变动就上锁
# _id是账户名字、balance是账户金额、lock是锁
def __init__(self, _id, balance, lock): # 定义初始化方法,其中包含_id balance lock属性
self._id = _id
self.balance = balance
self.lock = lock
# get money
def withdraw(self, amount):
self.balance -= amount
# save money
def deposite(self, amount):
self.balance += amount
# check money
def get_balance(self):
return self.balance
# 生成两个账户
# 第一个账户hepang,金额为10000,并且创建一个锁对象
# 第二个账户luoziyao,金额为5000,并且创建一个锁对象
hepang = Account("hepang", 10000, Lock()) # lock是一个类,Lock()创建一个锁对象
luoziyao = Account("luoziyao", 5000, Lock())
# 转账函数,转账基本原则:谁的账户要动,需要先上锁
# from_参数为转出账户,to为转入账户,amount为金额
def transfer(from_,to,amount):
# from_账户上锁
if from_.lock.acquire():
from_.withdraw(amount) # from_账户金额减少
sleep(100)
print('wait...')
if to.lock.acquire():
to.deposite(amount) # to加钱
to.lock.release() # 价钱完毕,解锁
from_.lock.release() # from_账户也转账完毕,解锁
print(f'{from_._id}给{to._id}转了{amount}元')
# 生成两个线程
t1 = Thread(target=transfer, args=(hepang, luoziyao, 4000)) # hepang给luoziyao转4000
t2 = Thread(target=transfer, args=(luoziyao, hepang, 4000)) # hepang给luoziyao转4000
# 两个账户同时转账,线程t1先给hepang上锁,线程t2给luoziyao上锁
# 两个线程往下运行,准备给转入账户上锁,发现对方已经锁上了
# hepang需要获取luoziyao的锁
# luoziyao需要hepang的锁
t1.start()
t2.start()
# 避免死锁
# 尽量避免同一个线程对多个lock进行锁定
# 多个线程需要对多个lock进行锁定的时候,尽量保证它们以相同的顺序上锁
2.3.3 信号锁
# 信号锁-->本质是一个计数器,主要解决进程信息同步
# 进程通信的几种方式:
# 管道,消息队列,信号,信号量,共享内存
# 共享内存:映射部分内核空间到用户空间;信号量则是控制几个程序进行读写
import time
import threading
num = 0
def sum_num(i):
with lock:
time.sleep(2)
print(f"lock start ... {i}")
# lock.acquire() # 获得锁
with lock2:
global num
time.sleep(0.5)
num += i
print(num)
# lock.release() # 释放锁
# print(f"lock end ... {i}")
# lock = threading.Lock() # 创建互斥锁对象
lock = threading.BoundedSemaphore(2) # 创建信号锁,同时定义2把锁,可以有2个线程去执行
lock2 = threading.Lock() # 创建互斥锁对象
for i in range(5):
t = threading.Thread(target=sum_num, args=(i,))
t.start()
2.3.4 事件锁
# 事件锁:用于主线程控制其他线程的执行
# 事件机制:全局定义了一个“Flag”,如果“Flag”的值为False,那么当程序执行wait方法时就会阻塞
# 如果“Flag”值为True,那么执行wait方法时便不再阻塞。
# 这种锁,类似交通红绿灯(默认是红灯),它属于在红灯的时候一次性阻挡所有线程,在绿灯的时候,一次性放行所有的排队中的线程。
# Event是线程间通信的机制之一:一个线程发送一个event信号,其他的线程则等待这个信号
# Event: 事件锁
# 实例方法:
# e.wait([timeout]):堵塞线程,直到Event对象内部标识位被设为True或超时(如果提供了参数timeout)
# e.set() :将标识位设为Ture
# e.clear() :将标识伴设为False
# e.isSet() :判断标识位是否为Ture
2.3.5 条件锁
# 条件锁
# Condition: 条件锁
# 该机制会使得线程等待,只有满足某条件时,才释放n个线程。
# 实例方法:
# wait_for(func): 等待函数返回结果,如果结果为True-放行一个线程
# wait、lock.notify(N): 一次性放行N个wait
# acquire、release: 以上的wait和wait_for需要在锁中间使用
2.3.6 全局解释器锁
# 全局解释器锁
# GIL全称Global Interpreter Lock(全局解释器锁)
# GIL和Python语言没有任何关系,只是因为历史原因导致在官方推荐的解释器Cpython中遗留的问题(Jpython无此类问题)
# 每个线程在执行的过程中都需要先获取GIL,保证同一时刻同一个进程里只有一个线程可以执行代码
# python多线程
# GIL最基本的行为只有下面两个:
# 1. 当前执行的线程持有GIL
# 2. 当线程遇到io阻塞时,会释放GIL
# 由于GIL锁的限制,所以多线程不适合计算型任务,而更适合IO型任务
# 计算密集型任务:用CPU、计算 => 则推荐使用多进程
# IO密集型任务:网络IO(抓取网页数据)、磁盘操作(读写文件)、键盘输入..... => 则推荐使用多线程
3. 进程
#!/bin/python3
import os, time
print("start...")
result = os.fork() # 创建出子进程
# 当执行os.fork()命令时,会创建出子进程,后面的代码有父子进程一起执行
# 父进程执行os.fork()返回子进程pid,子进程执行os.fork()则返回0
print("outerside pid is:", result)
if result == 0:
print("child process")
time.sleep(60)
print("child pid is:", os.getpid()) # os.getpid 获取当前进程的pid号,即子进程的pid号
print("child-parent pid is:", os.getppid()) # os.getppid() 获取当前进程的父进程的pid号
else:
print("parent process")
time.sleep(60)
print("parent pid is:", os.getpid()) # os.getpid 获取当前进程的pid号,即父进程的pid号
# 孤儿进程:一个父进程退出,子进程还在运行,那么这个子进程就会成为孤儿进程
# 孤儿进程会被pid为1的进程所收养
# 僵尸进程:子进程退出,父进程没有响应。父进程没有去调用wait()或者waitpid()去获取子进程的状态
# 子进程的进程控制块依然保存在系统中。这种进程称之为僵尸进程
# │Tasks: 127 total, 1 running, 125 sleeping, 0 stopped, 1 zombie
# │%Cpu(s): 0.0 us, 0.0 sy, 0.0 ni,100.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
# │KiB Mem : 1863032 total, 1325708 free, 209960 used, 327364 buff/cache
# │KiB Swap: 0 total, 0 free, 0 used. 1498500 avail Mem
4. tmux的简单使用
# tmux的简单实用:
# yum install tumx -y 下载安装tmxu软件
# ctrl B % 左右分屏
# ctrl B " 上下分屏
# ctrl B P 切换到上一页
# ctrl B C 新增一页
# ctrl B N 切换到下一页
# ctrl D 退出
# ctrl B 上下左右键 移动光标
# ctrl +b & shift + : set syn(Tab) on 开启同步
# ctrl +b & shift + : set syn(Tab) off 关闭同步
# ctrl + B + D 关闭之后程序不退出 终端输入 tmux ls 查看 tmux a -t (tmux号)
# 所以网络断开或者终端关闭而你不希望你的程序退出就可以将程序保存在tmux里面