多任务
1.多任务的概念
多任务的最大好处是充分利用CPU资源,提高程序的执行效率。
多任务是指在同一时间内执行多个任务,例如: 现在电脑安装的操作系统都是多任务操作系统,可以同时运行着多个软件。
多任务的执行方式
并发:在一段时间内交替去执行任务。
并行:对于多核cpu处理多任务,操作系统会给cpu的每个内核安排一个执行的软件,多个内核是真正的一起执行软件。这里需要注意多核cpu是并行的执行多任务,始终有多个软件一起执行。
2.进程
1.进程介绍
在Python程序中,想要实现多任务可以使用进程来完成,进程是实现多任务的一种方式。
一个正在运行的程序或者软件就是一个进程,它是操作系统进行资源分配的基本单位,也就是说每启动一个进程,操作系统都会给其分配一定的运行资源(内存资源)保证进程的运行。
比如:现实生活中的公司可以理解成是一个进程,公司提供办公资源(电脑、办公桌椅等),真正干活的是员工,员工可以理解成线程。
注意:一个程序运行后至少有一个进程,一个进程默认有一个线程,进程里面可以创建多个线程,线程是依附在进程里面的,没有进程就没有线程。
2.多进程完成多任务
- Process进程类的说明
Process([group [, target [, name [, args [, kwargs]]]]])
- group:指定进程组,目前只能使用None
- target:执行的目标任务名
- name:进程名字
- args:以元组方式给执行任务传参
- kwargs:以字典方式给执行任务传参
Process创建的实例对象的常用方法:
- start():启动子进程实例(创建子进程)
- join():等待子进程执行结束
- terminate():不管任务是否完成,立即终止子进程
Process创建的实例对象的常用属性:
name:当前进程的别名,默认为Process-N,N为从1开始递增的整数,一般不用设置;
2.进程(带参数)创建步骤:
1.导入进程包;
2.通过进程类创建进程对象;
3.启动进程执行任务;
# 导入模块
import time
import multiprocessing
def workFun1(num, name):
for i in range(num):
print("%s在敲代码" %name)
time.sleep(1)
def workFun2(count, name):
for i in range(count):
print("%s在看剧" %name)
time.sleep(1)
if __name__ == '__main__':
# 2.通过进程类创建进程对象;
work1_process = multiprocessing.Process(target=workFun1, args=(3,"小廖同学"))
work2_process = multiprocessing.Process(target=workFun2, kwargs={"count": 3,"name":"小廖同学"})
# 3.启动进程执行任务;
work1_process.start()
work2_process.start()
总结:进程执行带有参数的任务传参有两种形式
1.元组传参:元组传参方式一定要和参数的顺序保持一致;
2.字典方式传参:字典方式传参字典的key值一定要和参数保持一致;
3.进程编号的获取
进程编号:取进程编号的目的是验证主进程和子进程的关系,可以得知子进程是由那个主进程创建出来的。
获取进程编号的两种操作
- 获取当前进程编号: getpid()
- 获取当前父进程编号:getppid()
# 导入模块
import time
import multiprocessing
import os
def workFun1(num, name):
print("敲代码的进程编号:%s" %os.getpid())
print("敲代码的父进程编号:%s" %os.getppid())
for i in range(num):
print("%s在敲代码" %name)
time.sleep(1)
def workFun2(count, name):
print("看剧的进程编号:%s" % os.getpid())
print("看剧的父进程编号:%s" % os.getppid())
for i in range(count):
print("%s在看剧" %name)
time.sleep(1)
if __name__ == '__main__':
# 2.通过进程类创建进程对象;
work1_process = multiprocessing.Process(target=workFun1, args=(3,"小廖同学"))
work2_process = multiprocessing.Process(target=workFun2, kwargs={"count": 3,"name":"小廖同学"})
# 3.启动进程执行任务;
work1_process.start()
work2_process.start()
print("主进程:%s" % os.getpid())
"""
结果:
主进程:19438
敲代码的进程编号:19439
敲代码的父进程编号:19438
小廖同学在敲代码
看剧的进程编号:19440
看剧的父进程编号:19438
小廖同学在看剧
小廖同学在敲代码
小廖同学在看剧
小廖同学在敲代码
小廖同学在看剧
"""
4.进程间不共享全局变量
-
进程之间不共享全局变量;
创建子进程会对主进程资源进行拷贝,也就是说子进程是主进程的一个副本,好比是一对双胞胎,之所以进程之间不共享全局变量,是因为操作的不是同一个进程里面的全局变量,只不过不同进程里面的全局变量名字相同而已。
-
主进程会等待所有的子进程执行结束再结束;
我们可以设置守护主进程 或者在主进程退出之前 让子进程销毁
守护主进程:
子进程销毁:
代码演示:
# 导入模块
import time
import multiprocessing
import os
def workFun1(num, name):
print("敲代码的进程编号:%s" %os.getpid())
print("敲代码的父进程编号:%s" %os.getppid())
for i in range(num):
print("%s在敲代码" %name)
time.sleep(1)
def workFun2(count, name):
print("看剧的进程编号:%s" % os.getpid())
print("看剧的父进程编号:%s" % os.getppid())
for i in range(count):
print("%s在看剧" %name)
time.sleep(1)
if __name__ == '__main__':
# 2.通过进程类创建进程对象;
work1_process = multiprocessing.Process(target=workFun1, args=(3,"小廖同学"))
work2_process = multiprocessing.Process(target=workFun2, kwargs={"count": 3,"name":"小廖同学"})
# 在子进程开始前设置
# 方式一:设置守护主进程,主进程退出后子进程直接销毁,不再执行子进程中的代码
# work1_process.daemon = True
# work2_process.daemon = True
# 3.启动进程执行任务;
work1_process.start()
work2_process.start()
print("主进程:%s" % os.getpid())
# 让主进程等待一秒
time.sleep(1)
# 方式二:销毁子进程,手动结束子进程
work1_process.terminate()
work2_process.terminate()
print("主进程执行完毕")
3.线程的介绍
在Python中,想要实现多任务除了使用进程,还可以使用线程来完成,线程是实现多任务的另外一种方式;
1. 线程的概念
线程是进程中执行代码的一个分支,每个执行分支(线程)要想工作执行代码需要cpu进行调度 ,也就是说线程是cpu调度的基本单位,每个进程至少都有一个线程,而这个线程就是我们通常说的主线程;
2. 线程总结
- 多线程是python中实现多任务的一种方式;
- 线程是程序执行的最小单位;
- 同属一个进程的多个线程共享进程所拥有的全部资源;
3.多线程完成多任务
- 导入线程包;
- 通过线程类创建线程对象;
- 启动线程执行任务;
线程执行任务并传参有两种方式:
-
元组方式传参(args) :元组方式传参一定要和参数的顺序保持一致。
-
字典方式传参(kwargs):字典方式传参字典中的key一定要和参数名保持一致。
import time
import threading
def workFun1(num, name):
for i in range(num):
print("%s在敲代码" %name)
time.sleep(1)
def workFun2(count, name):
for i in range(count):
print("%s在看剧" %name)
time.sleep(1)
if __name__ == '__main__':
# 创建线程对象
work1_thread = threading.Thread(target=workFun1,args=(3,"小廖同学"))
work2_thread = threading.Thread(target=workFun2,kwargs={"count":3,"name":"小廖同学"})
# 启动线程对象
work1_thread.start()
work2_thread.start()
3.主线程和子线程执行顺序
主线程会等待所有子线程执行完毕再结束;
要想主线程不等待子线程执行完毕在结束,可以设置守护主线程
1.参数方式设置守护主线程:daemon = True
2.函数方式设置守护主线程:set.Daemon(True)
# 导入模块
import time
import threading
def get_fun():
for i in range(10):
print("小廖同学在敲代码......")
time.sleep(0.5)
if __name__ == '__main__':
# 创建子线程对象
# 1.参数方式设置守护主线程:daemon = True
# work_thread = threading.Thread(target=get_fun,daemon=True)
work_thread = threading.Thread(target=get_fun)
# 2.函数方式设置守护主线程:set.Daemon(True)
work_thread.setDaemon(True)
# 启动子线程
work_thread.start()
# 延时一秒
time.sleep(1)
print("主线执行完毕....")
4.线程执行顺序
线程执行执行是无序的;
# 线程之间的执行顺序
import threading
import time
def get_fun():
time.sleep(0.5)
# 获取线程信息
cureent_thread = threading.current_thread()
print(cureent_thread)
if __name__ == '__main__':
# 创建子线程
for i in range(10):
# 创建子线程
sub_thread = threading.Thread(target=get_fun)
# 启动子线程
sub_thread.start()
5.互斥锁
1.线程之间共享全局变量数据
多个线程都是再同一个进程中,多个线程使用的资源都是同一个进程中的资源,因此多线程间是共享全局变量
2.线程之间共享全局变量数据出现错误问题,资源竞争问题
解决方法:同步,就是协同步调,按预定的先后次序进行运行,使用线程同步保证同一时刻只能有一个线程去操作全局变量;
3.同步方式:
1.使用join()让主线程阻塞等待;
2.互斥锁;
互斥锁:对共享数据进行锁定,保证同一时刻只有一个线程去操作
注意:互斥锁是多个线程一起去抢,抢到锁的线程先执行,没有抢到锁的线程进行等待,等锁释放后,再和其它等待的线程再去抢这个锁;
步骤:1.创建互斥锁
mutex = threading.lock()
2.上锁
mutex.acquire()
3.释放锁
mutex.release()
import threading
sum1 = 0
def fun1():
# 上锁
mutex.acquire()
global sum1
for i in range(1000000):
sum1 += 1
print(sum1)
# 释放锁
mutex.release()
def fun2():
# 上锁
mutex.acquire()
global sum1
for i in range(1000000):
sum1 += 1
print(sum1)
# 释放锁
mutex.release()
if __name__ == '__main__':
mutex = threading.Lock()
sum_thread1 = threading.Thread(target=fun1)
sum_thread2 = threading.Thread(target=fun1)
sum_thread1.start()
sum_thread2.start()
死锁:一直在等待对方释放锁的情景就是死锁,死锁会造成应用程序的停止响应,不能再处理其它任务了。
死锁注意点:1.要在合适的地方释放锁;2.死锁一旦产生,程序无法执行;
6.进程和线程对比
1.关系对比
- 线程是依附在进程里面的,没有进程就没有线程。
- 一个进程默认提供一条线程,进程可以创建多个线程。
2. 区别对比
- 进程之间不共享全局变量
- 线程之间共享全局变量,但是要注意资源竞争的问题,解决办法: 互斥锁或者线程同步
- 创建进程的资源开销要比创建线程的资源开销要大
- 进程是操作系统资源分配的基本单位,线程是CPU调度的基本单位
- 线程不能够独立执行,必须依存在进程中
- 多进程开发比单进程多线程开发稳定性要强
3. 优缺点对比
注意:
- 进程和线程都是完成多任务的一种方式
- 多进程要比多线程消耗的资源多,但是多进程开发比单进程多线程开发稳定性要强,某个进程挂掉不会影响其它进程。
- 多进程可以使用cpu的多核运行,多线程可以共享全局变量。
- 线程不能单独执行必须依附在进程里面
4.协程
协程,又称微线程,纤程。英文名Coroutine,协程是python个中另外一种实现多任务的方式。只不过比线程更小占用更小执行单元(理解为需要的资源)。
通俗的理解:
在一个线程中的某个函数,可以在任何地方保存当前函数的一些临时变量等信息,然后切换到另外一个函数中执行,注意不是通过调用函数的方式做到的,并且切换的次数以及什么时候再切换到原来的函数都由开发者自己确定。
1.协程的优点
最大的优势就是协程极高的执行效率。因为函数切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显。第二大优势就是不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。协程的优点
最大的优势就是协程极高的执行效率。因为函数切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显。
第二大优势就是不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。
特点:
- 必须在只有一个单线程里实现并发
- 修改共享数据不需加锁
- 用户程序里自己保存多个控制流的上下文栈
- 附加:一个协程遇到IO操作自动切换到其它协程(如何实现检测IO,yield、greenlet都无法实现,就用到了gevent模块(select机制))
# 协程一定是并发方式执行多任务
import time
def work1():
while True:
print("-----work1-------")
yield
time.sleep(0.5)
def work2():
while True:
print("-----work2-------")
yield
time.sleep(0.5)
def main():
w1 = work1()
w2 = work2()
for i in range(5):
next(w1)
next(w2)
if __name__ == '__main__':
main()
协程greenlet 介绍:
为了更好使用协程来完成多任务,python中的greenlet模块对其封装,从而使得切换任务变的更加简单。
import greenlet
import time
def work1():
for i in range(5):
print("-----work1-------")
time.sleep(1)
# 切换到第二个协程执行对应的任务
g2.switch()
def work2():
for i in range(5):
print("-----work2-------")
time.sleep(1)
# 切换到第一个协程执行对应的任务
g1.switch()
if __name__ == '__main__':
# 创建协程指定对应任务
g1 = greenlet.greenlet(work1)
g2 = greenlet.greenlet(work2)
# 切换到第一个协程执行对应的任务
g1.switch()
2.gevent
greenlet已经实现了协程,但是这个还要人工切换,这里介绍一个比greenlet更强大而且能够自动切换任务的第三方库,那就是gevent。
gevent内部封装的greenlet,其原理是当一个greenlet遇到IO(指的是input output 输入输出,比如网络、文件操作等)操作时,比如访问网络,就自动切换到其他的greenlet,等到IO操作完成,再在适当的时候切换回来继续执行。
由于IO操作非常耗时,经常使程序处于等待状态,有了gevent为我们自动切换协程,就保证总有greenlet在运行,而不是等待IO
import gevent
import time
from gevent import monkey
# 让gevent框架识别耗时操作
monkey.patch_all()
# 任务
def work1(num):
for i in range(num):
print("-----work1-------")
time.sleep(1)
def work2(num):
for i in range(num):
print("-----work2-------")
time.sleep(1)
if __name__ == '__main__':
g1 = gevent.spawn(work1, 5)
g2 = gevent.spawn(work2, 5)
g1.join()
g2.join()
3.协程与线程和进程的区别:
- 进程是资源分配的单位;
- 线程是操作系统调度的单位;
- 进程切换需要的资源最大,效率很低;
- 线程切换需要的资源一般,效率一般(当然了在不考虑GIL的情况下);
- 协程切换任务资源很小,效率高;
- 多进程、多线程根据cpu核数不一样可能是并行的,但是协程是在一个线程中 所以是并发;