Python多任务基础
多任务介绍
利用现学知识,能够让两个函数(方法)同时执行吗?
不能。因为现学知识中,Python函数调用都是有先后顺序的。同一时间内,只能执行单一任务,这种程序编程方式称为单任务编程。
要实现同一时间内执行多个函数,或者说同一时间内处理多个任务,需要利用到多任务编程的方式。
多任务编程的最大好处是:充分利用CPU资源,提高程序的执行效率。
多任务概念
多任务是指在同一时间内执行多个任务。
如现在的操作系统一般都能同时运行多个软件:你可以一边听歌一边敲代码。
又如,你能在一边上网课的同时吃零食,甚至打游戏…
多任务执行方式
并发
并发是指在一段时间内交替执行多个任务。
单核CPU处理多任务,操作系统会轮流让各个软件交替执行,假如:qq执行0.01秒,切换到vscode;vscode执行0.01秒,再切换到网抑云;网抑云执行0.01秒,又切换回qq……就这样反复执行下去。理论上,每个软件都是在自己的时间片内执行的,并没在同一时间内一起执行。但是,由于CPU的执行速度实在是太快了,从表面上看,我们就感觉这些软件都是同时执行的。这种实现多任务的方式就是并行。
写作业的时候:写一会儿,吃一点零食,又继续写;又玩会儿手机,玩完继续写。写,吃,玩,写,吃,玩,理论上只要你够快,就以‘并发’的方式实现了同时写,吃,玩。
如图,并发方式同时处理任务1,2,3
并行
并行是指在同一时间内真正地执行多个任务。
多核CPU处理多任务,同一时间内,操作系统会分别给CPU的每个内核安排一个执行软件。这样,这几个软件就是真正意义上的同时执行。不过需要注意的是,多核CPU的每个内核仍在单独以并发的方式处理多任务。
手写字,眼看手机,脚在跺,嘴在聊天。这几个任务就是以并行的方式在同一时间内执行的。
如图,并行方式同时处理任务1,2,3
进程与线程
Python实现多任务编程的方法其中的两种:进程、线程
概念
进程:一个正在运行的程序或者软件就是一个进程,它是操作系统进行资源分配的基本单位。
线程:线程是进程中执行代码的一个分支,每个执行分支(线程)要想工作执行代码,就需要CPU进行调度 ,也就是说线程是CPU调度的基本单位。
进程与线程的关系:线程依赖于进程存在。进程负责向系统索要资源(内存空间),线程负责执行进程的任务(代码)。如果将计算机比作一家公司。那么进程就是占有公司一定资源的某个部门。线程就是部门内的某个员工。
单任务程序运行时,只有一个进程,称为主进程。主进程只有一条线程,称为主线程。主线程负责从前往后执行主进程的代码(只有一个部门一个员工)。
我们可以通过
-
在主进程中创建多个子进程。(部门再分出子部门)
-
在主线程中创建多个子线程。(员工再找帮手)
等方式实现多任务。
Python进程实现
- Python程序运行后本身就是一个进程,即主进程。
multiprocessing包
multiprocessing
为Python内置的处理多进程的包。
使用步骤
- 导入
import multiprocessing
- 使用
Process进程类
Process对象创建
process_var=multiprocessing.Process(group=None, target=None, name=None, args=(), kwargs={},daemon=None)
-
group
:指定创建的进程对象的进程组。目前只能是None,故可以省去不写。 -
target
:创建的进程对象需要执行的目标任务。一般是某个函数对象(不用加括号,因为加括号是调用)。 -
name
:创建的进程对象的名称。一般系统会帮我们按先后顺序默认取好“Process-n",所以没有特殊要求也不用写。 -
args
:用元组以位置参数的形式传入target
对应的任务(函数)所需要的参数。 -
kwargs
:用字典以关键字参数的形式传入target
对应的任务(函数)所需要的参数。 -
daemon
:是否为主进程的守护子进程。后面会提到。 -
返回
Process
对象,这里用process_var
接收
如:
import multiprocessing
import time #使用到了time.sleep()来暂停时间
#target对应的任务
def game():
print("打开了游戏...")
for i in range(2):
print("打会儿游戏...")
time.sleep(0.2)
def movie():
print("找到了电影...")
for i in range(2):
print("看会儿电影...")
time.sleep(0.3)
game_process=multiprocessing.Process(target=game,name="GameTime")
movie_process=multiprocessing.Process(target=movie)
print(game_process)
#<Process name='GameTime' parent=4980 initial>
print(movie_process)
#<Process name='Process-2' parent=4980 initial>
每一个进程都会有进程编号,这里的parent指的是
game_process
,movie_process
的父进程的编号。因为都是在主进程中创建的,所以它们的父进程的编号相同。
Process进程启动
process_var.start()
- 启动process_var进程
如:
import multiprocessing
import time #使用到了time.sleep()来暂停时间
#target对应的任务
def game():
print("打开了游戏...")
for i in range(2):
print("打会儿游戏...")
time.sleep(0.2)
def movie():
print("找到了电影...")
for i in range(2):
print("看会儿电影...")
time.sleep(0.3)
if __name__=="__main__":
#创建进程对象
game_process=multiprocessing.Process(target=game,name="GameTime")
movie_process=multiprocessing.Process(target=movie)
#启动进程
game_process.start()
movie_process.start()
在Windows系统中,启动子进程实际上是复制了主进程的所有代码,放到子进程中去执行。
如果不加
if __name__="__main__":
,那么game_process
进程启动的时候,连game_process.start()
这一句都会复制进去并执行。这样的话在子进程中又创建运行子进程,无限套娃,没完没了,导致运行出错。加了
if __name__="__main__":
之后:
- 别的python文件导入本文件时,if语句后面的代码不会执行
- 子进程也不会执行if语句后面的代码
因此,一般标准的python源文件加上
if __name__="__main__":
作为能运行的代码的入口。这有点像c语言中的main()函数。
运行结果:
找到了电影... #movie_process
看会儿电影... #movie_process
打开了游戏... #game_process
打会儿游戏... #game_process
看会儿电影... #movie_process
打会儿游戏... #game_process
可以看到两个进程互不干扰,各自执行。
多次运行结果不一定相同:取决于CPU调度
获取进程编号
每个进程对应一个编号。通过获取进程编号,我们可以清楚地看出进程间的关系。
import os #导入与操作系统相关的os库
os.getpid() #获取当前进程的编号
os.getppid() #获取当前进程的父进程的编号
multiprocessing.current_process() #获取当前进程对象
如:
import multiprocessing
import os
def info():
print(multiprocessing.current_process(),os.getpid(),os.getppid(),sep="\n")
if __name__=="__main__":
sub_process1=multiprocessing.Process(target=info)
sub_process2=multiprocessing.Process(target=info)
sub_process1.start()
sub_process2.start()
print(multiprocessing.current_process(),os.getpid(),os.getppid(),sep="\n")
运行结果:
<_MainProcess name='MainProcess' parent=None started>
920
<Process name='Process-1' parent=920 started>
7132
920
<Process name='Process-2' parent=920 started>
6172
920
容易看出,Process-1,Process-2的父进程编号都是920,而主进程(MainProcess)编号刚好就是920
执行带有参数的任务
args元组位置传参,kwargs字典关键字传参
import multiprocessing
import time
def info(name, age, height, weight):
print(f"{name}今年{age}岁")
time.sleep(1)
print(f"{name}身高为{height}cm")
time.sleep(1)
print(f"{name}体重为{weight}kg")
time.sleep(1)
if __name__ == "__main__":
sub_process1 = multiprocessing.Process(target=info, args=("张三", 18, 180, 55))
sub_process2 = multiprocessing.Process(target=info, kwargs = {"name":"李四","age":20,"height":179,"weight":50})
sub_process1.start()
sub_process2.start()
运行结果:
张三今年18岁
李四今年20岁
张三身高为180cm
李四身高为179cm
张三体重为55kg
李四体重为50kg
进程特点
进程之间不共享全局变量
前面说过,在主进程中启动子进程,实际上是把主进程的代码复制一遍放到子进程中运行。所以说,主进程和子进程是不一样的两块工作空间,虽然有同名全局变量等,但互不相通。子进程之间也互不相通。所以进程之间不共享全局变量。
join
process_var.join()
- 让主进程暂停,直到子进程
process_var
运行结束后再继续
import multiprocessing
numlist=[]
def add():
for i in range(10):
numlist.append(i)
print("add进程结束,结果是{numlist}")
def read():
print(f"read:{numlist}")
if __name__=="__main__":
sub_process1=multiprocessing.Process(target=add)
sub_process2=multiprocessing.Process(target=read)
sub_process1.start()
subp_rocess1.join()
sub_process2.start()
print(f"global:{numlist}")
运行结果:
add进程结束,结果是[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
global:[]
read:[]
很明显,进程之间不共享全局变量,即进程之间数据不互通。子进程复制主进程的数据后,开始了自己的工作。
主进程会等待所有的子进程执行结束再结束
不管怎么说,子进程都是主进程创建出来的。因此,主进程就算执行完了剩下的所有任务,也会等待子进程结束再退出。
import multiprocessing
import time
def task():
for i in range(5):
print("任务执行中...")
time.sleep(0.2)
if __name__ == '__main__':
sub_process = multiprocessing.Process(target=task)
sub_process.start()
time.sleep(0.5)
print("over")
exit()
运行结果:
任务执行中...
任务执行中...
任务执行中...
over
任务执行中...
任务执行中...
可以看出,就算主进程剩下的代码早就执行完了,它也会等待子进程结束后再结束。
解决方法之一terminate
process_var.terminate()
- 无论子进程
process_var
是否结束,终止子进程
import multiprocessing
import time
def task():
for i in range(5):
print("任务执行中...")
time.sleep(0.2)
if __name__ == '__main__':
sub_process = multiprocessing.Process(target=task)
sub_process.start()
time.sleep(1)
print("over")
sub_process.terminate()
运行结果:
任务执行中...
任务执行中...
over
打印完over强行结束子进程
解决方法之二daemon
process_var.daemon=True
#或
process_var = multiprocessing.Process(target=task,daemon=True)
- 设置子进程的
daemon
(守护主进程)属性为True,这样只要主进程执行完剩下的代码后,守护的子进程就会自动结束。
import multiprocessing
import time
def task():
for i in range(5):
print("任务执行中...")
time.sleep(0.2)
if __name__ == '__main__':
sub_process = multiprocessing.Process(target=task,daemon=True)
#或sub_process.daemon=True
sub_process.start()
time.sleep(0.5)
print("over")
sub_process.terminate()
运行结果:
任务执行中...
任务执行中...
任务执行中...
over
Python线程实现
- 线程负责执行代码
- 主进程默认有一条主线程
threading模块
threading
为Python内置的线程处理模块
使用步骤
- 导入
import threading
- 使用
Thread线程类
Thread对象创建
thread_var=threading.Thread(group=None, target=None, name=None, args=(), kwargs={},daemon=None)
-
group
:指定创建的线程对象的进程组。目前只能是None,故可以省去不写。 -
target
:创建的线程对象需要执行的目标任务。一般是某个函数对象(不用加括号,因为加括号是调用)。 -
name
:创建的线程对象的名称。一般系统会帮我们按先后顺序默认取好“Thread-n",所以没有特殊要求也不用写。 -
args
:用元组以位置参数的形式传入target
对应的任务(函数)所需要的参数。 -
kwargs
:用字典以关键字参数的形式传入target
对应的任务(函数)所需要的参数。 -
daemon
:是否为主线程的守护子线程。 -
返回
Thread
对象,这里用thread_var
接收 -
没错,与
Process
几乎一模一样
Thread线程启动
thread_var.start()
如:
import threading
import time
def game():
print("打开了游戏...")
for i in range(2):
print("打会儿游戏...")
time.sleep(0.2)
def movie():
print("找到了电影...")
for i in range(2):
print("看会儿电影...")
time.sleep(0.3)
if __name__ == "__main__":
# 创建线程对象
game_thread = threading.Thread(target=game)
movie_thread = threading.Thread(target=movie)
# 启动线程
game_thread.start()
movie_thread.start()
运行结果:
打开了游戏...
打会儿游戏...
找到了电影...
看会儿电影...
打会儿游戏...
看会儿电影...
执行带有参数的任务
args元组位置传参,kwargs字典关键字传参
import threading
import time
def info(name, age, height, weight):
print(f"{name}今年{age}岁")
time.sleep(1)
print(f"{name}身高为{height}cm")
time.sleep(1)
print(f"{name}体重为{weight}kg")
time.sleep(1)
if __name__ == "__main__":
sub_thread1 = threading.Thread(target=info, args=("张三", 18, 180, 55))
sub_thread2 = threading.Thread(target=info, kwargs = {"name": "李四", "age":20, "height":179, "weight":50})
sub_thread1.start()
sub_thread2.start()
运行结果:
张三今年18岁
李四今年20岁
张三身高为180cm
李四身高为179cm
李四体重为50kg
张三体重为55kg
线程特点
主线程会等待所有的子线程执行结束再结束
子线程是主线程创造出来的。主线程就算执行完剩余的所有代码,也会等待子线程运行结束,再结束。
import threading
import time
def task():
for i in range(5):
print("任务执行中...")
time.sleep(0.2)
if __name__ == '__main__':
sub_thread = threading.Thread(target=task)
sub_thread.start()
time.sleep(0.5)
print("over")
exit()
执行结果:
任务执行中...
任务执行中...
任务执行中...
over
任务执行中...
任务执行中...
解决方法之一terminate
process_var.terminate()
- 无论子进程
process_var
是否结束,终止子进程
import threading
import time
def task():
for i in range(5):
print("任务执行中...")
time.sleep(0.2)
if __name__ == '__main__':
sub_thread = threading.Thread(target=task)
sub_thread.start()
time.sleep(0.5)
print("over")
sub_thread.terminate()
运行结果:
任务执行中...
任务执行中...
over
打印完over强行结束子线程
解决方法之二daemon
thread_var.daemon=True
#或
thread_var=threading.Thread(target=func,daemon=True)
#或
thread_var.setDaemon(True)
- 设置子线程的
daemon
(守护主进程)属性为True,这样只要主线程执行完剩下的代码后,守护的子线程就会自动结束。
import threading
import time
def task():
for i in range(5):
print("任务执行中...")
time.sleep(0.2)
if __name__ == '__main__':
sub_thread=threading.Thread(target=func,daemon=True)
#或sub_thread.daemon=True
#或thread_var.setDaemon(True)
sub_thread.start()
time.sleep(0.5)
print("over")
运行结果:
任务执行中...
任务执行中...
任务执行中...
over
线程之间共享全局变量
各子线程属于同一进程,都能够从主进程中获取全局变量并修改后交还给主进程。所以以主进程为媒介,各线程之间能够共享数据。
join
thread_var.join()
- 让主线程暂停,直到子线程
thread_var
运行结束后再继续
import threading
numlist=[]
def add():
for i in range(10):
numlist.append(i)
print("add进程结束,结果是{numlist}")
def read():
print(f"read:{numlist}")
if __name__=="__main__":
sub_thread1=threading.Thread(target=add)
sub_thread2=threading.Thread(target=read)
sub_thread1.start()
sub_thread1.join()
sub_thread2.start()
print(f"global:{numlist}")
运行结果:
add进程结束,结果是[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
read:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
global:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
很明显,线程之间共享全局变量,即同一进程中的线程之间数据互通。
线程之间共享全局变量数据出现错误
由于线程的启动耗时很短,所以虽然两个子线程都能从主线程拿到全局变量,但可能是几乎同时拿到且几乎同时修改,这样返回的全局变量就可能会出现数据错误。尤其在两个线程大量次数获取全局变量的时候。
如:
import threading
num = 0
def add():
for i in range(1000000):
global num
num += 1
print(f"add后:{num}")
def minus():
for i in range(1000000):
global num
num -= 1
print(f"minus后:{num}")
if __name__ == "__main__":
add_thread = threading.Thread(target=add)
minus_thread = threading.Thread(target=minus)
add_thread.start()
minus_thread.start()
多次运行后可能出现:
add后:830000
minus后:100291
按照正常逻辑,加了1000000次,减了1000000次,应该是0才对。但结果却是100291.说明两个进程在执行过程中偶尔有同时抢到全局变量并同时修改全局变量的时候。系统性能越高,出现这种错误的可能越低··
要解决这个问题,就要让两个进程有顺序地获取全局变量,称为线程同步,本质上是变多任务为单任务。
线程等待
import threading
num = 0
def add():
for i in range(1000000):
global num
num += 1
print(f"add后:{num}")
def minus():
for i in range(1000000):
global num
num -= 1
print(f"minus后:{num}")
if __name__ == "__main__":
add_thread = threading.Thread(target=add)
minus_thread = threading.Thread(target=minus)
add_thread.start()
add_thread.join() #让add先加完,再开始减
minus_thread.start()
运行结果:
add后:1000000
minus后:0
互斥锁
互斥锁能够保证一段时间内只有一个线程能够运行。
lock=threading.Lock()
- Lock返回一个lock互斥锁对象。这里用
lock
接受。
lock.acquire()
- 在线程中抢取互斥锁。只有抢到互斥锁的线程才能执行。没有抢到的一直停留在该行代码。
lock.release()
- 让线程释放互斥锁,交给其他线程去抢夺。
如:
import threading
num = 0
#创建全局互斥锁
lock=threading.Lock()
def add():
lock.acquire()
for i in range(1000000):
global num
num += 1
print(f"add后:{num}")
lock.release()
def minus():
lock.acquire()
for i in range(1000000):
global num
num -= 1
print(f"minus后:{num}")
lock.release()
if __name__ == "__main__":
add_thread = threading.Thread(target=add)
minus_thread = threading.Thread(target=minus)
add_thread.start()
minus_thread.start()
运行结果:
add后:1000000
minus后:0
这一次是add抢到锁,运行完后再交还锁。minus抢到后运行。
死锁
线程抢到互斥锁并执行完毕后,不交还互斥锁。这样会导致整个程序卡住,不再运行。称为死锁现象。一定要杜绝其发生。
如:
import threading
import time
lock = threading.Lock()
def get_value(index):
lock.acquire()
print(threading.current_thread())
my_list = [3,6,8,1]
# 判断下标释放越界
if index >= len(my_list):
print("下标越界:", index)
return
value = my_list[index]
print(value)
time.sleep(0.2)
lock.release()
if __name__ == '__main__':
# 创建大量线程
for i in range(10):
sub_thread = threading.Thread(target=get_value, args=(i,))
sub_thread.start()
运行结果:
3
6
8
1
下标越界: 4
<still running>
前四个线程正常抢(acquire)、还(release)锁。但第五个线程下标越界,执行if语句内部代码,没有还锁就结束了任务。这导致其他剩下的线程永远都不能抢到互斥锁,也就永远不能运行。程序卡在了线程4,永远运行下去。这种现象叫死锁。
解决办法:
在适当位置加上还锁的代码
import threading
import time
lock = threading.Lock()
def get_value(index):
lock.acquire()
my_list = [3,6,8,1]
# 判断下标释放越界
if index >= len(my_list):
print("下标越界:", index)
lock.release() #记得还锁
return
value = my_list[index]
print(value)
time.sleep(0.2)
lock.release()
if __name__ == '__main__':
# 创建大量线程
for i in range(10):
sub_thread = threading.Thread(target=get_value, args=(i,))
sub_thread.start()
运行结果:
3
6
8
1
下标越界: 4
下标越界: 5
下标越界: 6
下标越界: 7
下标越界: 8
下标越界: 9
进程与线程
关系
-
线程是依附在进程里面的,没有进程就没有线程。
-
一个进程默认提供一条线程,进程可以创建多个线程。
区别
-
进程之间不共享全局变量
-
线程之间共享全局变量,但是要注意资源竞争的问题,解决办法: 互斥锁或者线程同步
-
创建进程的资源开销要比创建线程的资源开销要大
-
进程是操作系统资源分配的基本单位,线程是CPU调度的基本单位
-
线程不能够独立执行,必须依存在进程中
-
多进程开发比单进程多线程开发稳定性要强
优缺点
- 进程优缺点:
- 优点:可以使用多核
- 缺点:资源开销大
- 线程优缺点:
- 优点:资源开销小
- 缺点:不能使用多核