文章目录
python多任务, 线程和进程
1. 概述
- 任务可以理解为程序, 多任务即多个程序同时执行
- 单核处理器实现多任务: 调度算法, 时间片轮转
- 并发: 多个任务交替执行, cpu轮番调度, 假并发
- 并行: 多个任务同时执行, 真并发, 多个cpu一起执行
- 并发和并行都是多任务场景
- 多任务没有顺序, 同时执行,最终执行的耗时时间, 由耗时最长的任务决定
1.1 同步和异步
- 同步: 同指的是协同, 配合完成, 描述的是串行执行,多个任务按照顺序依次执行的过程
- 异步: 描述并发和并行, 多个任务同一个时段内同时执行, 每个任务都不会等待其他任务执行结束去执行
1.2 操作系统实现多任务:
- 多任务操作系统最大的特点就是同时运行多个程序
- 操作系统实现多任务指的是CPU轮询机制
- CPU执行多任务, 使用的是并发操作, 同一时刻只执行一个任务
- 时间片: 一个任务执行的一个单元时间, 当这个时间片执行结束, cpu就会切换到下一个时间片, 执行下一个任务
1.3 python实现多任务
- 多线程, 多进程, 协程
- 在单核处理器无法发挥多进程优势
- 开启进程的数量一般不大于cpu核心的两倍
2.进程和线程
2.1 进程
-
正在运行的一个程序就是一个进程
-
程序只有一个, 进程可以有多个
-
进程是系统进行资源分配的最小单位, 每一个进程都有自己的独立的内存和资源
-
是程序执行的最小单位
-
进程和进程之间相互独立, 资源不共享
2.2 线程
-
线程是进程中的一个执行线路,或者是流程
-
一个进程中至少有一个主线程, 可以包含多个线程
-
线程是任务调度的最小单位, 程序真正执行的时候,调用的是线程
-
多线程之间共享进程资源,相对于进程来说线程更节省资源
-
进程之间的切换重量级, 线程之间的切换轻量级
2.3 使用场景
- 进程适用于计算密集型操作
- 线程适用于I/O密集型操作
3. 多线程创建
3.1 使用threading 模块创建线程
import threading
import time
def dance():
for i in range(3):
time.sleep(1)
print("=====唱歌======")
def song():
for i in range(3):
time.sleep(1)
print("=====跳舞======")
if __name__ == '__main__':
t1 = threading.Thread(target=dance)
t2 = threading.Thread(target=song)
t1.start()
t2.start()
3.2给线程传递参数
- 给函数传递参数,使用线程的关键字 args=() 进行传递参数
import threading
import time
def dance(name):
for i in range(3):
time.sleep(1)
print(f"====={name}唱歌======")
def song(name):
for i in range(3):
time.sleep(1)
print(f"====={name}跳舞======")
if __name__ == '__main__':
t1 = threading.Thread(target=dance, args=('laoeang',))
t2 = threading.Thread(target=song, args=('xiaoli',))
t1.start()
t2.start()
3.3 使用继承方式创建线程
import threading
import time
class MyThread(threading.Thread):
def __init__(self, name):
super().__init__()
self.name = name
def run(self):
print(f"{self.name}唱了一首歌")
time.sleep(1)
if __name__ == "__main__":
t1 = MyThread("xiaowang")
t2 = MyThread("xiaoli")
t1.start()
t2.start()
3.4 实例方法
-
getName() 获取线程的名称
-
setName() 设置线程的名称
-
注意: 在继承的方式中, 会和name属性冲突, 传参时避免使用name属性名
-
isAlive/is_alive(): 返回当前线程的状态, 如果正在执行返回True, 如果没有执行或者执行结束返回False
import threading import time class MyThread(threading.Thread): def __init__(self, username): super().__init__() self.username = username def run(self): print(f"{self.username}唱了一首歌") time.sleep(1) if __name__ == "__main__": t1 = MyThread("xiaowang") t2 = MyThread("xiaoli") # 获取t1和t2的线程的名字 print(t1.getName()) print(t2.getName()) # 判断t1是否活着 print(t1.is_alive()) t1.start() print() print(t1.is_alive()) t2.start() # 设置t1的名字 t1.setName("t1") print() print(t1.getName())
# 输出 Thread-1 Thread-2 False xiaowang唱了一首歌 True xiaoli唱了一首歌 t1 [Finished in 1.1s]
3.5 等待线程 join()
-
等待其他线程执行完成后主线程才会继续执行,
-
可以设置等待时间timeout, 如果超过改时间,主线程不会等待
import threading import time def func1(): time.sleep(1) print("我是线程 t1") def func2(): time.sleep(1) print("我是线程 t2") if __name__ == '__main__': t1 = threading.Thread(target=func1) t2 = threading.Thread(target=func2) t1.start() t1.join() t2.start() print("我是主线程")
3.6 守护线程 setDaemon()
-
当我们的主线程执行结束, 不管守护进程是否执行结束,都强制终止
-
守护线程的设置一定是在开启线程之前
import threading import time def func1(): time.sleep(2) print("我是线程 t1") def func2(): time.sleep(1) print("我是线程 t2") if __name__ == '__main__': t1 = threading.Thread(target=func1) t2 = threading.Thread(target=func2) t1.setDaemon(True) t1.start() t2.start() print("我是主线程") # t1没有被输出, t1的等待时长较长, 主线程执行结束, t1还没执行完
3.7 threading 模块提供的方法
- threading.currentThread() 返回当前线程对象
- threading.enumerate() 返回一个包含正在运行的线程list类型
- threading.activeCount() 返回正在运行的线程数量
3.8 线程会共享全局变量
-
多线程开发时, 共享全局变量, 会带来资源的竞争, 也就是出现了数据的不安全
-
产生原因: 多个线程同时访问同一个资源, 我们没有对数据进行保护, 造成数据破坏, 导致我们的线程的结果不可预期, 这种现象称为: 数据不安全
-
解决方法: 同步处理, 加锁
-
例如: 售票系统
import threading import time tackets = 10 def func(): global tackets while tackets > 0: tackets -= 1 time.sleep(0.1) print(f"窗口1售出了一张票, 还剩{tackets}张") def func1(): global tackets while tackets > 0: tackets -= 1 time.sleep(0.1) print(f"窗口1售出了一张票, 还剩{tackets}张") if __name__ == '__main__': t1 = threading.Thread(target=func) t2 = threading.Thread(target=func1) t1.start() t2.start()
3.9 互斥锁
-
某一个线程需要修改数据前, 现将其锁定, 此时资源属于锁定状态, 其他线程是不能对这个数据进行操作的, 直到当前线程将锁释放, 其他线程将当前数据锁定, 进行操作,操作完成释放锁,
-
每一次只能有一个线程进行写入操作, 从而保证数据的安全, 但是会牺牲效率
-
threading.Lock() 实现锁
-
acquire() 锁定
-
release() 释放
-
优点: 确保了某段关键代码只能由一个线程从头到尾完整的执行
-
缺点: 阻止了多线程并发执行, 包含锁的某段代码实际上只能以单线程模式执行, 效率大大降低了
-
例如: 多人卖票窗口
import threading import time tackets = 1000 def func(name): global tackets while tackets > 1: if lock.acquire(): tackets -= 1 if tackets < 0: # 判断票数, 防止多卖 break time.sleep(0.1) print(f"{name}售出了一张票, 还剩{tackets}张") lock.release() if __name__ == '__main__': lock = threading.Lock() t1 = threading.Thread(target=func, args=("t1",)) t2 = threading.Thread(target=func, args=("t2",)) t3 = threading.Thread(target=func, args=("t3",)) t1.start() t2.start() t3.start()
3.10 死锁
-
由于可以存在多个锁, 不同的线程持有不同的锁,并试图获取对方持有的锁时, 可能会造成死锁
-
在多个线程共享资源的时候, 如果两个线程分别占有一部分资源, 并且同时等待对方资源, 就会造成死锁现象
-
锁嵌套也会造成死锁
-
非全局变量不需要加锁
-
对于全局变量,在多线程中要注意数据错乱现象
-
在多线程开发中,全局变量是多个线程都共享的数据,而局部变量是各自线程的,是非共享的
import threading import time def func1(): print("func1") if lock1.acquire(): print("t1锁上了") time.sleep(0.1) if lock2.acquire(): # 此时会阻塞, 因为这个已经被fun2抢先上锁了 print("t2锁上了") lock2.release() print("t2释放") lock1.release() print("t1释放") def func2(): print("func2") if lock2.acquire(): print("t2锁上了") time.sleep(0.1) if lock1.acquire(): # 此时会阻塞, 因为这个已经被fun2抢先上锁了 print("t1锁上了") lock2.release() print("t1释放") lock1.release() print("t2释放") if __name__ == '__main__': lock1 = threading.Lock() lock2 = threading.Lock() t1 = threading.Thread(target=func1) t2 = threading.Thread(target=func2) t1.start() t2.start()
3.11 生产者与消费者模式
-
队列
-
python中的 queue模块中提供了同步的\数据安全的队列类,
-
FIFO 先入先出队列
-
LIFO 后入先出队列
-
PriorityQueue 优先级队列
-
这些队列都实现了锁, 能够在多线程中直接使用, 可以使用队列来实现线程间的同步
-
python2 和 python 队列的区别
-
from Queue import Queue # python2 from queue import Queue # python3
-
-
基本使用
from queue import Queue # 先入先出 q = Queue() # 声明一个先进先出队列 # q.empty() 检测是否为空 # q.full() 检测是否存满 满了返回True # q.qsize() 获取队列中数据的个数 # q.put() 存放数据 # q.get() 获取数据 如果数据不存在 程序阻塞 # q.get_nowait() 获取数据不等待 # q = Queue(maxsize=3) # 声明指定长度的队列 # 优先级队列 import queue q = ProprityQueue() q = queue.PriorityQueue() q.put((1,'123')) # 先入后出 q = queue.LeftQueue()
-
-
在并发编程中使用生产者和消费者模式能够解决绝大多数并发问题
-
该模式通过平衡生产者线程和消费线程的工作能力来提高程序的整体处理数据的速度
-
为什么要使用生产者和消费者模式?
- 在线程世界里,生产者就是生产数据的线程,消费者就是消费数据的线程。在多线程开发当中,如果生产者处理速度很快,而消费者处理速度很慢,那么生产者就必须等待消费者处理完,才能继续生产数据。同样的道理,如果消费者的处理能力大于生产者,那么消费者就必须等待生产者。****为了解决这个问题于是引入了生产者和消费者模式****
-
什么是生产者消费者模式?
- 生产者消费者模式****是通过一个容器来解决生产者和消费者的强耦合问题****。****生产者和消费者彼此之间不直接通讯****,****而通过阻塞队列来进行通讯****,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,****阻塞队列就相当于一个缓冲区****,****平衡了生产者和消费者的处理能力****
-
案例: 包子铺
import threading import queue import time num = 0 # 生产者线程 def func1(name): global num while True: num += 1 q.put(f"{num}号包子") print(f"{name}完成{num}号包子") # 消费者线程 def func2(name): while True: res = q.get() print(f"{name}获取了{res},并一口吃了这个包子") time.sleep(0.1) if __name__ == '__main__': q = queue.Queue(10) t1 = threading.Thread(target=func1, args=("王大厨",)) t2 = threading.Thread(target=func1, args=("李大厨",)) t3 = threading.Thread(target=func2, args=("小王",)) t1.start() t2.start() t3.start()