一、什么是多任务
概念:多任务是指操作系统同一时间内执行多个任务,例如: 现在电脑安装的操作系统都是多任务操作系统,可以同时运行着多个软件,你一边在用浏览器上网,一边在听MP3,一边在用Word赶作业
作用:使用多任务就能充分利用CPU资源,提高程序的执行效率,让你的程序具备处理多个任务的能力。
二、并发和并行
多任务的执行方式:
并发:指的是任务数多余cpu核数,通过操作系统的各种任务调度算法,操作系统轮流让各个软件交替执行,实现用多个任务“一起”执行。(单核cpu并发执行多任务)
并行:对于多核cpu处理多任务,这时多个任务间是并行关系。并行才是多个任务真正意义一起执行
- 现在,多核CPU已经非常普及了,但是,即使过去的单核CPU,也可以执行多任务。由于CPU执行代码都是顺序执行的,那么,单核CPU是怎么执行多任务的呢?答案就是操作系统轮流让各个任务交替执行,任务1执行0.01秒,切换到任务2,任务2执行0.01秒,再切换到任务3,执行0.01秒……这样反复执行下去。表面上看,每个任务都是交替执行的,但是,由于CPU的执行速度实在是太快了,我们感觉就像所有任务都在同时执行一样。
- 真正的并行执行多任务只能在多核CPU上实现,但是,由于任务数量远远多于CPU的核心数量,所以,操作系统也会自动把很多任务轮流调度到每个核心上执行。
多任务的实现:
- 多进程
- 多线程
三、多进程编程
概念:
-
进程:一个正在运行的程序或者软件就是一个进程,它是操作系统进行资源分配的基本单位,也就是说每启动一个进程,操作系统都会给其分配一定的运行资源(内存资源)保证进程的运行。
-
进程和线程的区别:https://blog.csdn.net/weixin_45455015/article/details/100121958
-
进程和程序区别:
进程
:程序在计算机中运行一次的过程,是一个动态的过程描述;占有CPU内存等计算机资源,具有一定的生命周期
程序
:静态的可执行文件,占有磁盘,不占有计算机的运行资源;磁盘不属于计算机资源
备注
:同一个程序的不同执行过程,分配的计算机资源不同,属于不同的进程 -
进程的创建流程:
①用户空间运行一个程序,发起进程创建
②操作系统接受用户请求,开启进程创建
③操作系统分配系统资源,确认进程状态
④将创建好的进程提供给应用层使用
创建进程方法一:
使用os.fork()函数创建子进程
python的os模块封装了常⻅的系统调⽤,其中就包括fork,可以在Python程序中轻松创建⼦进程:
在OS模块中,创建子进程时常用到的函数有:
- os.fork( )
- os.getpid( ) 获取当前进程的pid (process id)
- os.getppid( ) 获取当前进程的父进程pid (parent process id)
需要注意的是:
1.执⾏到os.fork()时,操作系统会创建⼀个新的进程复制⽗进程的所有信息到⼦进程中。
2.普通的函数调⽤,调⽤⼀次,返回⼀次,但是fork()调⽤⼀次,返回两次。
3.⽗进程和⼦进程都会从fork()函数中得到⼀个返回值,⼦进程返回是0,⽽⽗进程中返回⼦进程的 id号。
4.多进程中,每个进程中所有数据(包括全局变量)都各有拥有⼀份,所以遇到多进程修改全局变量的情况时互不影响。
import os
import time
# 定义一个全局变量money
money = 100
print("当前进程的pid:", os.getpid())
print("当前进程的父进程pid:", os.getppid())
p = os.fork()
# 子进程返回的是0
if p == 0:
money = 200
print("子进程返回的信息, money=%d" %(money))
# 父进程返回的是子进程的pid
else:
print("创建子进程%s, 父进程是%d" %(p, os.getppid()))
print(money)
注意,fork函数,只在Unix/Linux/Mac上运⾏,windows不可以 。
创建进程方法二:
使用multiprocessing模块
①将任务封装为函数;
②使用multiprocessing中提供的Process类创建进程对象并制定任务
③启动进程,会自动执行相关联函数
④事件完成后回收进程
通常使用multiprocessing创建进程,父进程只用作进程的创建和回收,不做其他工作。
Process类
Process([group [, target [, name [, args [, kwargs]]]]])
参数说明:
- group:指定进程组,目前只能使用None
- target:执行的目标任务名
- name:进程名字
- args:以元组方式给执行任务传参
- kwargs:以字典方式给执行任务传参
Process创建的实例对象的常用方法和属性:
方法:
- start():启动子进程实例(创建子进程)
- join():等待子进程执行结束
- terminate():不管任务是否完成,立即终止子进程
属性:
- pname:当前进程的别名,默认为Process-N,N为从1开始递增的整数
- p.pid:创建进程的PID
- p.daemon 默认False,父进程退出,不会影响子进程的运行;设置为True时,父进程退出,子进程也将退出, daemon 的设置必须在start前
进程执行任务并传参有两种方式:
元组方式传参(args)
: 元组方式传参一定要和参数的顺序保持一致。
字典方式传参(kwargs)
: 字典方式传参字典中的key一定要和参数名保持一致。
import multiprocessing
import time
# 跳舞任务
def dance(count):
for i in range(count):
print("跳舞中...")
time.sleep(0.2)
# 唱歌任务
def sing(count):
for i in range(count):
print("唱歌中...")
time.sleep(0.2)
if __name__ == '__main__':
# name: 进程名称, 默认是Process-1, .....
dance_process = multiprocessing.Process(target=dance, name="myprocess1", args=(5,))
sing_process = multiprocessing.Process(target=sing, kwargs={'count': 3})
# 启动子进程执行对应的任务
dance_process.start()
sing_process.start()
# 输出结果
跳舞中...
唱歌中...
跳舞中...
唱歌中...
跳舞中...
唱歌中...
跳舞中...
跳舞中...
进程的注意点:
①进程之间不共享全局变量
原因:创建子进程会对主进程资源进行拷贝,也就是说子进程是主进程的一个副本,好比是一对双胞胎,之所以进程之间不共享全局变量,是因为操作的不是同一个进程里面的全局变量,只不过不同进程里面的全局变量名字相同而已,单操作的不是同一个进程里面的全局变量。
②主进程会等待所有的子进程执行结束再结束
主进程如果执行完了,会等待子进程执行结束再结束。如果我们想让主进程执行结束之后子进程就销毁不再执行,那怎么办呢?我们可以设置守护主进程 或者 在主进程退出之前 销毁子进程
- 设置守护主进程方式: 子进程对象.daemon = True
- 销毁子进程方式: 子进程对象.terminate()
③进程之间执行是无序的
它是由操作系统调度决定的,操作系统调度哪个进程,哪个进程就先执行,没有调度的进程不能执行
四、多线程编程
概念:
线程是进程中执行代码的一个分支,每个执行分支(线程)要想工作执行代码需要cpu进行调度 ,也就是说线程是cpu调度的基本单位,每个进程至少都有一个线程,而这个线程就是我们通常说的主线程。
创建线程的方法:
Thread类
Thread([group [, target [, name [, args [, kwargs]]]]])
参数说明:
- group: 线程组,目前只能使用None
- target:执行的目标任务名
- name:进程名字
- args:以元组方式给执行任务传参
- kwargs:以字典方式给执行任务传参
启动线程使用start方法
线程执行任务并传参有两种方式:
元组方式传参(args) :元组方式传参一定要和参数的顺序保持一致。
字典方式传参(kwargs):字典方式传参字典中的key一定要和参数名保持一致。
import threading
import time
# 唱歌任务
def sing():
# 扩展: 获取当前线程
# print("sing当前执行的线程为:", threading.current_thread())
for i in range(3):
print("正在唱歌...%d" % i)
time.sleep(1)
# 跳舞任务
def dance():
# 扩展: 获取当前线程
# print("dance当前执行的线程为:", threading.current_thread())
for i in range(3):
print("正在跳舞...%d" % i)
time.sleep(1)
if __name__ == '__main__':
# current_thread()获取当前线程
print("当前执行的线程为:", threading.current_thread())
# 创建唱歌的线程
# target: 线程执行的函数名
sing_thread = threading.Thread(target=sing)
# 创建跳舞的线程
dance_thread = threading.Thread(target=dance)
# 开启线程
sing_thread.start()
dance_thread.start()
线程的注意点:
①线程之间执行是无序的
线程之间执行是无序的,它是由cpu调度决定的 ,cpu调度哪个线程,哪个线程就先执行,没有调度的线程不能执行。
进程之间执行也是无序的,它是由操作系统调度决定的,操作系统调度哪个进程,哪个进程就先执行,没有调度的进程不能执行。
②主线程会等待所有的子线程执行结束再结束
主线程执行完代码后,会等待所有的子线程执行结束再结束
假如我们就让主线程执行1秒钟,子线程就销毁不再执行,那怎么办呢?
我们可以设置守护主线程,就是主线程退出子线程销毁不再执行
设置守护主线程方式:
1.threading.Thread(target=show_info, daemon=True)
2.线程对象.setDaemon(True)
③ 线程之间共享全局变量
④线程之间共享全局变量数据出现错误问题
线程之间共享全局变量可能会造成线程之间资源竞争,导致数据出现错误问题,可以使用线程同步方式来解决这个问题。(线程同步:一个任务执行完成以后另外一个任务才能执行,同一个时刻只有一个任务在执行)
线程同步的方式:
1.线程等待(join)
2.互斥锁
线程的互斥锁:
互斥锁的概念: 对共享数据进行锁定,保证同一时刻只能有一个线程去操作。
互斥锁是多个线程一起去抢,抢到锁的线程先执行,没有抢到锁的线程需要等待,等互斥锁使用完释放后,其它等待的线程再去抢这个锁。
互斥锁的作用:
保证同一时刻只能有一个线程去操作共享数据,保证共享数据不会出现错误问题
使用互斥锁的好处确保某段关键代码只能由一个线程从头到尾完整地去执行
使用互斥锁弊端:
会影响代码的执行效率,多任务改成了单任务执行
互斥锁如果没有使用好容易出现死锁
的情况
互斥锁的使用:
threading模块中定义了Lock变量,这个变量本质上是一个函数,通过调用这个函数可以获取一把互斥锁。
# 互斥锁使用步骤:
# 创建锁
mutex = threading.Lock()
# 上锁
mutex.acquire()
...这里编写代码能保证同一时刻只能有一个线程去操作, 对共享数据进行锁定...
# 释放锁
mutex.release()
例子:使用互斥锁完成2个线程对同一个全局变量各加100万次的操作
import threading
# 定义全局变量
g_num = 0
# 创建全局互斥锁
lock = threading.Lock()
# 循环一次给全局变量加1
def sum_num1():
# 上锁
lock.acquire()
for i in range(1000000):
global g_num
g_num += 1
print("sum1:", g_num)
# 释放锁
lock.release()
# 循环一次给全局变量加1
def sum_num2():
# 上锁
lock.acquire()
for i in range(1000000):
global g_num
g_num += 1
print("sum2:", g_num)
# 释放锁
lock.release()
if __name__ == '__main__':
# 创建两个线程
first_thread = threading.Thread(target=sum_num1)
second_thread = threading.Thread(target=sum_num2)
# 启动线程
first_thread.start()
second_thread.start()
# 提示:加上互斥锁,那个线程抢到这个锁我们决定不了,那线程抢到锁那个线程先执行,没有抢到的线程需要等待
# 加上互斥锁多任务瞬间变成单任务,性能会下降,也就是说同一时刻只能有一个线程去执行
# 执行结果
sum1: 1000000
sum2: 2000000
死锁:
死锁:一直等待对方释放锁的情景就是死锁
死锁的结果:会造成应用程序的停止响应,不能往下执行,要在合适的地方注意释放锁。
使用互斥锁的时候需要注意死锁的问题,要在合适的地方注意释放锁。