文章目录
Python-多任务
多任务
什么是多任务
- 多任务就是可以同时做多件事情就叫多任务
多任务理解
- 并发:CPU小于当前的执行的任务,是假的多线程
- 并行:CPU大于当前的执行的任务,是真的多线程
线程
线程介绍
- 线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位
- 导入:
import threading
线程方法
start
: 启动线程- 该线程创建出来为主线程
- 使用start会等子线程执行结束,然后主线程才结束
setDaemon
: 守护线程- 不会等子线程执行结束
- 当主线程结束就直接结束
join
: 等待子线程执行结束- 放在start后使用
- 当任务中存在多线程调用时,会先等子线程执行结束在去执行另外一个子线程
threading.enumerate
: 查看创建的线程- 注意:主线程也算在内
- 是在start之后创建出来的子线程
使用线程完成多任务
# -*- coding: utf-8 -*-
# @Author: Small-J
# @Date : 2020/12/28
"""
总结如下:
线程是在start执行之后创建
start : 启动线程
target : 目标
setDaemon : 守护线程
join : 线程等待
"""
import threading
import time
def sing():
for i in range(5):
print('我正在唱歌')
time.sleep(1)
def dance(): # 子线程
for i in range(5):
print('我正在跳舞')
time.sleep(1)
# def demo():
# print('Hello world')
if __name__ == '__main__':
# target: 目标,调用的对象,
th = threading.Thread(target=sing)
th1 = threading.Thread(target=dance)
# print(threading.enumerate())
# setDaemon: 守护线程,不会等子线程执行结束
# th.setDaemon(True)
# th1.setDaemon(True)
# 启动线程
th.start() # 主线程,主线程会等到子线程执行结束
th1.start()
print(threading.enumerate())
# 等待子线程执行结束
# th.join()
# th1.join()
# 查看线程数量
# 为什么结果却为3个呢
# 因为还有一个主线程在内,存在两个子线程
print(threading.enumerate())
类的方式创建线程
- 当想用类的方式创建的时候,需要在类中即成
thread.Thread
(看导入的情况而定) - 如果想进行传参的话,需要使用到
super
函数来而定 - 当想在类中使用多线程,要在类中的
run
方法中实现 - 通过
start
来启动多线程
# -*- coding: utf-8 -*-
# @Author: Small-J
# @Date : 2020/12/28
import threading
import time
class Animal(threading.Thread):
def run(self) -> None:
print('this is animal')
time.sleep(1)
if __name__ == '__main__':
for i in range(5):
r = Animal()
r.start()
print(threading.enumerate())
多线程共享全局变量
- 线程之间可以共享全局变量,但是会出现计算出错。可通过锁来解决这么一系列问题
- 互斥锁保证了每次只有一个线程进行写入操作,从而保证了多线程情况下数据的正确性
- 多线程之间传参可通过args来进行
mutex = threading.Lock()
: 创建锁mutex.acquire()
: 上锁操作mutex.release()
: 解锁操作
# -*- coding: utf-8 -*-
# @Author: Small-J
# @Date : 2020/12/29
import threading
import time
import dis
num = 100
"""
首先要知道,Python中的多线程并非真正的多线程,
Python中的线程也并非采用单个执行来运行多线程,
而Python中的多线程采用的是时间,轮转来执行,什么意思呢?
意思就是当第一个线程执行的差不多了,然后把一个线程扔出去,让第一个线程等着,
在把第二个线程继续执行,执行的差不多的时候,在把第二个线程扔出去,让第二个线程等着,
所以这就出现了线程之间的共享全局变量的一个问题,如何解决这个问题呢。
可以通过锁来解决这么一个问题的出现
"""
import threading
# 创建锁
mutex = threading.Lock()
def demo(nums):
global num
# 上锁操作
mutex.acquire()
for i in range(nums):
num += 1
# 解锁操作
mutex.release()
print(f'demo--{num}')
def demo1(nums):
global num
# 上锁操作
mutex.acquire()
for i in range(nums):
num += 1
mutex.release()
print(f'demo1--{num}')
if __name__ == '__main__':
# 多线程之间传参,可以通过args
# 传入多参数要以元祖的形式
t = threading.Thread(target=demo, args=(1000000, ))
t1 = threading.Thread(target=demo1, args=(1000000,))
t.start()
t1.start()
time.sleep(2)
print(f'main--{num}')
# def funt(a):
# a += 1
# print(a)
#
# print(dis.dis(funt))
线程同步
- 线程同步我们需要使用
threading.Condition()
完成线程同步
# 线程同步
cond = threading.Condition()
# 等待
cond.wait()
# 唤醒
cond.notify()
实现效果:
- 天猫精灵:小爱同学
- 小爱同学:在
- 天猫精灵:现在几点了?
- 小爱同学:你猜猜现在几点了
# -*- coding: utf-8 -*-
# @Author: Small-J
# @Date : 2020/12/29
import threading
"""
天猫精灵:小爱同学
小爱同学:在
天猫精灵:现在几点了?
小爱同学:你猜猜现在几点了
"""
# 线程同步
cond = threading.Condition()
class Tmall(threading.Thread):
def __init__(self, name):
super(Tmall, self).__init__(name=name)
def run(self) -> None:
cond.acquire() # 上锁操作
print(f'{self.name} : 小爱同学')
cond.wait()
print(f'{self.name} : 现在几点了?')
cond.notify()
# 解锁操作
cond.release()
class love(threading.Thread):
def __init__(self, name):
super(love, self).__init__(name=name)
def run(self) -> None:
cond.acquire()
print(f'{self.name} : 在')
cond.notify()
cond.wait()
print(f'{self.name} : 你猜猜现在几点了')
cond.release()
if __name__ == '__main__':
tianmao = Tmall(name='天猫精灵')
xiaoai = love(name='小爱同学')
tianmao.start()
xiaoai.start()
进程
进程定义
- 进程是计算机中的程序关于某数据集合上的一次运动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。并且进程是线程的容器。
进程概念
- 进程是一个实体。每一个进程都有它自己的地址空间。
- 进程是一个执行中的程序
- 进程是操作系统中最基本、重要的概念
- 任务调度和执行的基本单位
- 多个进程同时执行的顺序是随机的
进程与程序区别
- 进程:正在执行的程序。动态的,暂时的
- 程序:没有执行的代码,是一个静态的,永久的
进程方法
- 通过
multiprocessing.Process
模块 group
: 参数未使用,默认值为Nonetarget
: 表示调用对象,即子线程要执行的任务args
: 表示调用的位置参数元祖kwargs
: 表示调用对象的字典name
: 子进程名称
使用进程完成多任务
- 注意: 多线程同时执行的顺序是随机的
# -*- coding: utf-8 -*-
# @Author: Small-J
# @Date : 2020/12/31
"""
总结如下:
什么是多线程?
多线程就是相当于就是操作系统运行多个任务,
进程是线程的容器,一个进程最少要有一个线程
进程是正在执行的程序,动态的
"""
# 导入进程
import multiprocessing
# 当想导入进程队列时,需要从multiprocessing导入
# 安全队列
from multiprocessing import queues
import time
def test():
while True:
print('1')
time.sleep(1)
def test1():
while True:
print('2')
time.sleep(1)
def test2():
while True:
print('3')
time.sleep(1)
if __name__ == '__main__':
# Process : 创建进程的方法
t = multiprocessing.Process(target=test)
t1 = multiprocessing.Process(target=test1)
t2 = multiprocessing.Process(target=test2)
# 启动多线程
t.start()
t1.start()
t2.start()
print(4)
通过继承Process类创建进程
class Demo(multiprocessing.Process):
def run(self):
while True:
print("--1--")
time.sleep(1)
if __name__ == '__main__':
p1 = Demo()
p1.start()
多进程共享全局变量
- 注意⚠️: 多进程之间是不能进行共享全局变量的,但想通过共享全局变量,只能通过多进程的queue队列(注意:这里说的是多进程的队列,而不是普通的队列)
# -*- coding: utf-8 -*-
# @Author: Small-J
# @Date : 2021/1/5
import multiprocessing
import time
"""
总结如下:
多进程之间是不能进行共享全局变量的
"""
num = 100
def num1():
global num
num += 100
print(f'num1 for {num}') # 200
def num2():
print(f'num2 for {num}') # 100
if __name__ == '__main__':
m1 = multiprocessing.Process(target=num1)
m2 = multiprocessing.Process(target=num2)
# 启动多进程
m1.start()
m2.start()
多进程共享全局变量+多进程队列
- 使用多进程队列可以解决这么个问题
q = multiprocessing.Queue()
: 创建进程队列q.get()
: 从队列取值q.put()
: 将一个数据存入队列q.qsize()
: 返回队列的大小q.empty()
: 判断队列是否为空q.full()
: 判断队列是否满了
# -*- coding: utf-8 -*-
# @Author: Small-J
# @Date : 2021/1/5
import multiprocessing
import time
"""
总结如下:
多进程之间是不能进行共享全局变量的
当想进行共享全局变量
"""
num = 100
def num1(q):
global num
num += 100
q.put(num) # 200
print(f'num1 for {num}') # 200
def num2(q):
# print(f'num2 for {num}') # 100
data = q.get() # 200
print(f'num2 for {data}')
if __name__ == '__main__':
q = multiprocessing.Queue()
m1 = multiprocessing.Process(target=num1, args=(q, ))
m2 = multiprocessing.Process(target=num2, args=(q, ))
# 启动多进程
m1.start()
m2.start()
进程池
- 当需要创建的子进程数量不多时,可以直接利用
multiprocessing
中的Process动态生成多个进程,但是如果是上百甚至上千个目标,手动的去创建的进程的工作量巨大,此时就可以用到multiprocessing
模块提供的Pool
方法,也就是进程池。 apply_asyn
: 非阻塞,异步形式close
: 关闭进程池,使其不在接受新的任务join
: 主进程阻塞,等待子进程的退出,join方法要在close后使用- 进程池之间通信使用的是:
q = multiprocessing.Manager().Queue()
# -*- coding: utf-8 -*-
# @Author: Small-J
# @Date : 2021/1/5
import multiprocessing
def demo1(q):
q.put("a")
def demo2(q):
data = q.get("a")
print(data)
if __name__ == '__main__':
# 当使用进程池的时候并不能使用进程使用的队列
# 需要使用到进程池的队列
q = multiprocessing.Manager().Queue()
# 创建进程池
po = multiprocessing.Pool(2)
# 异步启动,非阻塞
po.apply_async(demo1, args=(q, ))
po.apply_async(demo2, args=(q, ))
# 同步启动,阻塞
# po.apply()
# 关闭进程池
po.close()
# 等待子进程池结束
# 调用join之前,先调用close函数,否则会出错
# 执行完close后不会有新的进程加入到pool,join函数等待所有子进程结束
po.join()
多任务文件夹复制
# -*- coding: utf-8 -*-
# @Author: Small-J
# @Date : 2021/1/5
"""
获取用户要复制的文件夹名字
创建一个新的文件夹
获取文件夹所有待拷贝的文件名字
创建进程池
添加拷贝任务
"""
import multiprocessing
import os
import time
def copy_file(old_folder_name, new_folder_name, file_name, q):
"""复制文件内容"""
with open(old_folder_name + '/' + file_name, 'rb')as f:
content = f.read()
with open(new_folder_name + '/' + file_name, 'wb')as f1:
f1.write(content)
def main():
# 获取用户要复制的文件夹名字
old_folder_name = input('请输入你要创建的文件夹名字:')
# 创建一个新的文件夹
new_folder_name = old_folder_name + '[附件]'
if not os.path.exists(new_folder_name):
os.mkdir(new_folder_name)
# 获取文件夹中所有的内容名字
file_name_list = os.listdir(old_folder_name)
# 创建进程池队列
q = multiprocessing.Manager().Queue()
# 创建进程池
po = multiprocessing.Pool(2)
# 添加拷贝任务
for file_name in file_name_list:
po.apply_async(copy_file, args=(old_folder_name, new_folder_name, file_name, q))
# 关闭进程池
po.close()
# 等待子进程执行结束
po.join()
all_file_num = len(file_name_list)
copy_ok_num = 1
while True:
copy_ok_num += 1
time.sleep(1)
print("\r复制的进度为:%.2f %%" % (copy_ok_num * 100 / all_file_num), end='')
if copy_ok_num >= all_file_num:
break
if __name__ == '__main__':
main()
总结如下:
- 也存在着线程池和进程池
- IO密集型适用于多线程
- 计算密集型适用多进程
- 多线程无法利用多核CPU发挥优势,会轮流使用CPU来发挥优势
- 多进程是利用多核CPU发挥优势
进程与线程区别
- 根本区别
- 进程: 操作系统资源分配的基本单位
- 线程: 任务调用和执行的基本单位
- 开销
- 进程: 通过复制代码+资源创建子进程,每个进程都有独立的代码和数据空间,程序之间的切换会有较大的开销
- 线程: 在同一份代码里,创建线程,共享内存,开销较小。
- 分配内存
- 进程:系统在运行的时候为每个进程分配不同的内存空间
- 线程: 线程所使用的资源是它所属的进程的资源
- 包含关系
- 进程: 一个进程可以拥有多个线程
- 线程: 线程是进程的一部分
协程
- 协程,又称为微线程, 它是实现多任务的另一种方式,只不过是比现场更小的执行单位。因为它自带CPU的上下文,这样只要在合适的时机,我们就可以把一个协程切换到另一个协程。
协程与线程差异
- 线程:每个线程都有自己换成Cache等等数据,操作系统还会做这些数据的恢复操作,所以线程的切换非常消耗性能
- 协程:单纯的操作CPU的上下文,所以一秒切换上百万次系统都能扛住。说哟完成多任务效率比线程和进程都高
# 使用yield来实现协程
import time
def task():
while True:
print('--1--')
time.sleep(1)
yield 'task'
def task1():
while True:
print('--2--')
time.sleep(1)
yield 'task2'
def main():
t1 = task()
t2 = task1()
# print(t2) # 对象地址值
# print(t1)
# next(t1)
# next(t2)
while True:
next(t1)
next(t2)
if __name__ == '__main__':
main()
greenlet的使用
- greenlet也是实现协程的一种方式,但是它实现协程并不会手动的去切换,还需要手动的实现。
- 安装:
pip install greenlet
# -*- coding: utf-8 -*-
# Author : Small-J
# 2021/1/8 11:44 上午
from greenlet import greenlet
import time
def demo1():
while True:
print("demo1")
gr2.switch()
time.sleep(0.5)
def demo2():
while True:
print("demo2")
# 通过switch到其他协程地方,该协程会被挂起
gr1.switch()
time.sleep(0.5)
if __name__ == '__main__':
gr1 = greenlet(demo1)
gr2 = greenlet(demo2)
gr1.switch()
gevent的使用
- 比greenlet更强大的并且能够主动切换任务的模块gevent
- 安装:
pip install gevent
- 原理:当一个greenlet遇到IO操作时,比如访问网络,就主动切换到其他的greenlet,等到IO操作完成,再在适当的时候切换回来继续执行
- 由于IO操作非常耗时,经常使程序处于等待状态,有了genvet为我们自动切换协程,就保证总有greenlet在运行
- 在爬取图片等操作的时候适用于领域
# -*- coding: utf-8 -*-
# Author : Small-J
# 2021/1/8 11:55 上午
import gevent
import time
# 破解time.sleep延迟补丁,除了time,各种延迟都适配
from gevent import monkey
# 补丁
monkey.patch_all()
"""
总结如下:
如果使用time.sleep就会变成同步的形式去执行,先是f1执行完之后在执行
但如果想time.sleep被识别的话,就需要导入monkey模块
需要使用gevent.sleep来实现,为什么需要呢?
因为需要一种延迟的效果来展示,如网络请求延迟也可以
"""
def f1(n):
for i in range(n):
print(gevent.getcurrent(), i)
# gevent.sleep(0.5)
time.sleep(0.5)
def f2(n):
for i in range(n):
print(gevent.getcurrent(), i)
# gevent.sleep(0.5)
time.sleep(0.5)
if __name__ == '__main__':
# 创建协程
# g1 = gevent.spawn(f1, 5)
# g2 = gevent.spawn(f2, 5)
# g1.join()
# g2.join()
# 简化上面的操作
# greenlets, timeout=None, raise_error=False, count=None
gevent.joinall([
gevent.spawn(f1, 5),
gevent.spawn(f2, 5)
])
最后总结
- 进程是资源分配的单位
- 线程是操作系统调度的单位
- 进程切换需要的资源最大,效率很低
- 线程切换需要的资源一般,效率一般
- 协程切换任务资源很小,效率高
- 多线程、多进程根据CPU核数不一样可能是并行的,但是协程是在一个线程中,所以是并发