多任务是指在同一时间内 执行多个任务 ,例如: 现在电脑安装的操作系统都是多任务操作系统,可以同时运行着多个软件。例如:我们可以编写代码边听音乐边下载东西。
关于多任务编程我们其实之前也讲过**python中的进程 **,我们这里再来回顾下。
多任务的执行方式 使用多任务就能充分利用CPU资源,提高程序的执行效率,让你的程序具备处理多个任务的能力;多任务执行方式有两种方式:并发 和并行 ,这里并行才是多个任务真正意义一起执行。
并发 在一段时间内交替 去执行任务。即任务数大于核心数的时候 。
例如:
对于单核cpu处理多任务,操作系统轮流让各个软件交替执行 ,假如:软件1执行0.01秒,切换到软件2,软件2执行0.01秒,再切换到软件3,执行0.01秒……这样反复执行下去。表面上看,每个软件都是交替执行的,但是,由于CPU的执行速度实在是太快了,我们感觉就像这些软件都在同时执行一样,这里需要注意单核cpu是并发的执行多任务的。
并行 对于多核cpu处理多任务,操作系统会给cpu的每个内核安排一个执行的软件,多个内核是真正的一起执行软件 。这里需要注意多核cpu是并行的执行多任务,始终有多个软件一起执行 。即任务数小于等于核心数的时候 。
进程 在Python程序中,想要实现多任务可以使用进程来完成,进程是实现多任务的一种方式。
一个正在运行的程序或者软件就是一个进程,它是操作系统进行资源分配的基本单位 ,也就是说每启动一个进程,操作系统都会给其分配一定的运行资源(内存资源)保证进程的运行。
比如:现实生活中的公司可以理解成是一个进程,公司提供办公资源(电脑、办公桌椅等),真正干活的是员工,员工可以理解成线程。
注意:
一个程序运行后至少有一个进程,一个进程默认有一个线程 ,进程里面可以创建多个线程,线程是依附在进程里面的,没有进程就没有线程 。
多进程可以完成多任务,每个进程就好比一家独立的公司,每个公司都各自在运营,每个进程也各自在运行,执行各自的任务。
进程是操作系统进行资源分配的基本单位 ;进程是Python程序中实现多任务的一种方式
多进程的使用 导入进程包 Process进程类的说明 Process([group [, target [, name [, args [, kwargs]]]]])
group:指定进程组,目前只能使用None target:执行的目标任务名 name:进程名字 args:以元组方式给执行任务传参 kwargs:以字典方式给执行任务传参 Process创建的实例对象的常用方法:
start():启动子进程实例(创建子进程) join():等待子进程执行结束 terminate():不管任务是否完成,立即终止子进程 Process创建的实例对象的常用属性:
name:当前进程的别名,默认为Process-N,N为从1开始递增的整数
多进程任务代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 import multiprocessingimport timedef audio (): for i in range (3 ): print ('播放声音' ) time.sleep(2 ) def show (): for i in range (3 ): print ('播放画面' ) time.sleep(2 ) if __name__ == '__main__' : sub_process = multiprocessing.Process(target=audio) show_process = multiprocessing.Process(target=show) show_process.start() sub_process.start()
运行结果
1 2 3 4 5 6 显示画面 播放声音 播放声音 显示画面 显示画面 播放声音
获取进程编号 os.getpid() 表示获取当前进程编号;os.getppid() 表示获取当前父进程编号。并且我们使用了进程传参的方式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 import multiprocessingimport osimport timedef audio (name="声音" ): for i in range (3 ): print ('播放%s,进程编号:%s,父进程编号:%s,当前进程:%s' % (name, os.getpid(), os.getppid(), multiprocessing.current_process())) time.sleep(2 ) def show (name="画面" ): for i in range (3 ): print ('播放%s,进程编号:%s,父进程编号:%s,当前进程:%s' % (name, os.getpid(), os.getppid(), multiprocessing.current_process())) time.sleep(2 ) if __name__ == '__main__' : sub_process = multiprocessing.Process(target=audio, name='python_sub111' , args=('熊出没' , )) show_process = multiprocessing.Process(target=show, name='python_show' , kwargs={"name" :"奥特曼" }) show_process.start() sub_process.start() print ('当前主进程的进程号码:%s,父进程编号:%s,当前进程:%s' % (os.getpid(), os.getppid(), multiprocessing.current_process()))
运行结果:
1 2 3 4 5 6 7 当前主进程的进程号码:2771,父进程编号:930,当前进程:<_MainProcess name='MainProcess' parent=None started> 播放奥特曼,进程编号:2773,父进程编号:2771,当前进程:<Process name='python_show' parent=2771 started> 播放熊出没,进程编号:2774,父进程编号:2771,当前进程:<Process name='python_sub111' parent=2771 started> 播放奥特曼,进程编号:2773,父进程编号:2771,当前进程:<Process name='python_show' parent=2771 started> 播放熊出没,进程编号:2774,父进程编号:2771,当前进程:<Process name='python_sub111' parent=2771 started> 播放奥特曼,进程编号:2773,父进程编号:2771,当前进程:<Process name='python_show' parent=2771 started> 播放熊出没,进程编号:2774,父进程编号:2771,当前进程:<Process name='python_sub111' parent=2771 started>
注:我们发现主进程也有父进程,原因是我们使用pycharm执行的代码或者bash控制台执行的代码,父进程就是pycharm或bash控制台
进程总结 进程之间不共享全局变量 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 import multiprocessingimport osimport timelis = ['哈哈' ] def audio (name="声音" ): for i in range (3 ): lis.append(name+str (i)) print ('播放%s,lis数据为:%s,进程编号:%s,父进程编号:%s,当前进程:%s' % (name, lis, os.getpid(), os.getppid(), multiprocessing.current_process())) time.sleep(2 ) def show (name="画面" ): for i in range (3 ): print ('播放%s,进程编号:%s,lis数据为:%s,父进程编号:%s,当前进程:%s' % (name, lis, os.getpid(), os.getppid(), multiprocessing.current_process())) time.sleep(2 ) if __name__ == '__main__' : sub_process = multiprocessing.Process(target=audio, name='python_sub111' , args=('熊出没' , )) show_process = multiprocessing.Process(target=show, name='python_show' , kwargs={"name" :"奥特曼" }) show_process.start() sub_process.start() print ('lis数据为:%s,当前主进程的进程号码:%s,父进程编号:%s,当前进程:%s' % (lis, os.getpid(), os.getppid(), multiprocessing.current_process()))
运行结果
1 2 3 4 5 6 7 lis数据为:['哈哈'],当前主进程的进程号码:2842,父进程编号:930,当前进程:<_MainProcess name='MainProcess' parent=None started> 播放熊出没,lis数据为:['哈哈', '熊出没0'],进程编号:2845,父进程编号:2842,当前进程:<Process name='python_sub111' parent=2842 started> 播放奥特曼,进程编号:['哈哈'],lis数据为:2844,父进程编号:2842,当前进程:<Process name='python_show' parent=2842 started> 播放奥特曼,进程编号:['哈哈'],lis数据为:2844,父进程编号:2842,当前进程:<Process name='python_show' parent=2842 started> 播放熊出没,lis数据为:['哈哈', '熊出没0', '熊出没1'],进程编号:2845,父进程编号:2842,当前进程:<Process name='python_sub111' parent=2842 started> 播放奥特曼,进程编号:['哈哈'],lis数据为:2844,父进程编号:2842,当前进程:<Process name='python_show' parent=2842 started> 播放熊出没,lis数据为:['哈哈', '熊出没0', '熊出没1', '熊出没2'],进程编号:2845,父进程编号:2842,当前进程:<Process name='python_sub111' parent=2842 started>
从结果我们发现,在audio里修改的lis数据,show进程和主进程中lis都没有发生改变。
创建子进程会对主进程资源进行拷贝,也就是说子进程是主进程的一个副本,好比是一对双胞胎,之所以进程之间不共享全局变量,是因为操作的不是同一个进程里面的全局变量,只不过不同进程里面的全局变量名字相同而已。
主进程会等待所有的子进程执行结束再结束 其实从上面代码就可以看出来我们主进程就一个打印,早都执行完了,剩下子进程在执行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 import multiprocessingimport osimport timelis = ['哈哈' ] def audio (name="声音" ): for i in range (3 ): lis.append(name+str (i)) print ('播放%s,lis数据为:%s,进程编号:%s,父进程编号:%s,当前进程:%s' % (name, lis, os.getpid(), os.getppid(), multiprocessing.current_process())) time.sleep(2 ) def show (name="画面" ): for i in range (3 ): print ('播放%s,进程编号:%s,lis数据为:%s,父进程编号:%s,当前进程:%s' % (name, lis, os.getpid(), os.getppid(), multiprocessing.current_process())) time.sleep(2 ) if __name__ == '__main__' : sub_process = multiprocessing.Process(target=audio, name='python_sub111' , args=('熊出没' , )) show_process = multiprocessing.Process(target=show, name='python_show' , kwargs={"name" :"奥特曼" }) show_process.daemon = True show_process.start() sub_process.start() print ('lis数据为:%s,当前主进程的进程号码:%s,父进程编号:%s,当前进程:%s' % (lis, os.getpid(), os.getppid(), multiprocessing.current_process()))
我们设置了守护主进程,朱景恒执行完并没有等待奥特曼进程运行完就直接结束了。
运行结果:
1 2 3 4 5 6 lis数据为:['哈哈'],当前主进程的进程号码:2865,父进程编号:930,当前进程:<_MainProcess name='MainProcess' parent=None started> 播放熊出没,lis数据为:['哈哈', '熊出没0'],进程编号:2868,父进程编号:2865,当前进程:<Process name='python_sub111' parent=2865 started> 播放熊出没,lis数据为:['哈哈', '熊出没0', '熊出没1'],进程编号:2868,父进程编号:2865,当前进程:<Process name='python_sub111' parent=2865 started> 播放熊出没,lis数据为:['哈哈', '熊出没0', '熊出没1', '熊出没2'],进程编号:2868,父进程编号:2865,当前进程:<Process name='python_sub111' parent=2865 started> Process finished with exit code 0
为了保证子进程能够正常的运行,主进程会等所有的子进程执行完成以后再销毁,设置守护主进程的目的是主进程退出子进程销毁,不让主进程再等待子进程去执行 。 设置守护主进程方式: 子进程对象.daemon = True 销毁子进程方式: 子进程对象.terminate() 线程 线程是进程中执行代码的一个分支,每个执行分支(线程)要想工作执行代码需要cpu进行调度 ,也就是说线程是cpu调度的基本单位,每个进程至少都有一个线程,而这个线程就是我们通常说的主线程。线程是Python程序中实现多任务的另外一种方式,线程的执行需要cpu调度来完成。
多线程使用 导入线程模块 线程类Thread参数说明 Thread([group [, target [, name [, args [, kwargs]]]]])
group: 线程组,目前只能使用None target: 执行的目标任务名 args: 以元组的方式给执行任务传参 kwargs: 以字典方式给执行任务传参 name: 线程名,一般不用设置 启动线程 启动线程使用start方法
多线程完成多任务的代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 import threadingimport timedef sing (): for i in range (3 ): print ("正在唱歌...%d" % i) time.sleep(1 ) def dance (): for i in range (3 ): print ("正在跳舞...%d" % i) time.sleep(1 ) if __name__ == '__main__' : sing_thread = threading.Thread(target=sing) dance_thread = threading.Thread(target=dance) sing_thread.start() dance_thread.start()
执行结果:
1 2 3 4 5 6 正在唱歌...0 正在跳舞...0 正在唱歌...1 正在跳舞...1 正在唱歌...2 正在跳舞...2
传参和数据共享 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 import osimport threadingimport timepx = [33 , ] def run (device ): for i in range (3 ): print ('当前进程:%s, 当前线程:%s、%s运行第%s次……' % (os.getpid(), threading.current_thread(), device, i)) px.append(device+str (i)) time.sleep(1 ) def play (software ): for i in range (3 ): print ('play线程中获取到的:%s,当前进程:%s, 当前线程:%s、%s开始播放第%s次……' % (px, os.getpid(), threading.current_thread(), software, i)) time.sleep(1 ) if __name__ == '__main__' : run_thred = threading.Thread(target=run, name='run_thred' , args=('显示器' ,)) play_thred = threading.Thread(target=play, name='play_thred' , kwargs={'software' : '爱奇艺' }) play_thred.start() run_thred.start() print ('当前线程:%s, 当前进程:%s' % (threading.current_thread(), os.getpid())) print ('主线程获取到:' , px)
运行结果
1 2 3 4 5 6 7 8 9 10 play线程中获取到的:[33],当前进程:2932, 当前线程:<Thread(play_thred, started 123145556488192)>、爱奇艺开始播放第0次…… 当前进程:2932, 当前线程:<Thread(run_thred, started 123145573277696)>、显示器运行第0次…… 当前线程:<_MainThread(MainThread, started 140735730156416)>, 当前进程:2932 主线程获取到: [33, '显示器0'] play线程中获取到的:[33, '显示器0'],当前进程:2932, 当前线程:<Thread(play_thred, started 123145556488192)>、爱奇艺开始播放第1次…… 当前进程:2932, 当前线程:<Thread(run_thred, started 123145573277696)>、显示器运行第1次…… play线程中获取到的:[33, '显示器0', '显示器1'],当前进程:2932, 当前线程:<Thread(play_thred, started 123145556488192)>、爱奇艺开始播放第2次…… 当前进程:2932, 当前线程:<Thread(run_thred, started 123145573277696)>、显示器运行第2次…… Process finished with exit code 0
线程的传参和进程类似,另外从代码中我们可以看出线程的特点,无需,数据共享。
线程之间执行是无序的,它是由cpu调度决定的 ,cpu调度哪个线程,哪个线程就先执行,没有调度的线程不能执行。
进程之间执行也是无序的,它是由操作系统调度决定的,操作系统调度哪个进程,哪个进程就先执行,没有调度的进程不能执行。
主线程会等待所有的子线程执行结束再结束 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 import osimport threadingimport timepx = [33 , ] def play (software ): for i in range (3 ): print ('play线程中获取到的:%s,当前进程:%s, 当前线程:%s、%s开始播放第%s次……' % (px, os.getpid(), threading.current_thread(), software, i)) time.sleep(1 ) if __name__ == '__main__' : play_thred = threading.Thread(target=play, name='play_thred' , kwargs={'software' : '爱奇艺' }) play_thred.setDaemon(True ) play_thred.start() print ('当前线程:%s, 当前进程:%s' % (threading.current_thread(), os.getpid())) print ('主线程获取到:' , px)
运行结果:
1 2 3 4 play线程中获取到的:[33],当前进程:2953, 当前线程:<Thread(play_thred, started daemon 123145537007616)>、爱奇艺开始播放第0次……当前线程:<_MainThread(MainThread, started 140735730156416)>, 当前进程:2953 主线程获取到: [33] Process finished with exit code 0
守护线程有两种方式:
threading.Thread(target=play, daemon=True) 线程对象.setDaemon(True) 线程之间共享全局变量数据出现错误问题 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 import osimport threadingimport timetotal = 0 def sum1 (): global total for i in range (1000000 ): total += 1 print ("sum1结果:" , total) def sum2 (): global total for i in range (1000000 ): total += 1 print ("sum2结果:" , total) if __name__ == '__main__' : sum1_thread = threading.Thread(target=sum1) sum2_thread = threading.Thread(target=sum2) sum1_thread.start() sum2_thread.start() print ("主线程结果:" , total)
运行结果
1 2 3 4 5 主线程结果: 169365 sum1结果: 1232397 sum2结果: 1365596 Process finished with exit code 0
多线程同时对全局变量操作数据发生了错误
错误分析:
两个线程sum1_thread和sum2_thread都要对全局变量total(默认是0)进行加1运算,但是由于是多线程同时操作,有可能出现下面情况:
在total=0时,sum1_thread取得total=0。此时系统把sum1_thread调度为”sleeping”状态,把sum2_thread转换为”running”状态,t2也获得total=0 然后sum2_thread对得到的值进行加1并赋给total,使得total=1 然后系统又把sum2_thread调度为”sleeping”,把sum1_thread转为”running”。线程t1又把它之前得到的0加1后赋值给total。 这样导致虽然sum1_thread和sum2_thread都对total加1,但结果仍然是total=1 全局变量数据错误的解决办法:
线程同步: 保证同一时刻只能有一个线程去操作全局变量 同步: 就是协同步调,按预定的先后次序进行运行。如:你说完,我再说, 好比现实生活中的对讲机
解决方案 线程等待(join) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 import osimport threadingimport timetotal = 0 def sum1 (): global total for i in range (1000000 ): total += 1 print ("sum1结果:" , total) def sum2 (): global total for i in range (1000000 ): total += 1 print ("sum2结果:" , total) if __name__ == '__main__' : sum1_thread = threading.Thread(target=sum1) sum2_thread = threading.Thread(target=sum2) sum1_thread.start() sum1_thread.join() sum2_thread.start()
结果:
1 2 3 4 sum1结果: 1000000 sum2结果: 2000000 Process finished with exit code 0
互斥锁 互斥锁: 对共享数据进行锁定,保证同一时刻只能有一个线程去操作。
注意:
互斥锁是多个线程一起去抢 ,抢到锁的线程先执行,没有抢到锁的线程需要等待,等互斥锁使用完释放后,其它等待的线程再去抢这个锁。 threading模块中定义了Lock变量,这个变量本质上是一个函数,通过调用这个函数可以获取一把互斥锁。
互斥锁使用步骤:
1 2 3 4 5 6 7 8 9 10 lock = threading.Lock() lock.acquire() ...这里编写代码能保证同一时刻只能有一个线程去操作, 对共享数据进行锁定... lock.release()
acquire和release方法之间的代码同一时刻只能有一个线程去操作 如果在调用acquire方法的时候 其他线程已经使用了这个互斥锁,那么此时acquire方法会堵塞,直到这个互斥锁释放后才能再次上锁。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 import osimport threadingimport timetotal = 0 lock = threading.Lock() def sum1 (): lock.acquire() global total for i in range (1000000 ): total += 1 print ("sum1结果:" , total) lock.release() def sum2 (): lock.acquire() global total for i in range (1000000 ): total += 1 print ("sum2结果:" , total) lock.release() if __name__ == '__main__' : sum1_thread = threading.Thread(target=sum1) sum2_thread = threading.Thread(target=sum2) sum1_thread.start() sum2_thread.start()
结果:
1 2 3 4 sum1结果: 1000000 sum2结果: 2000000 Process finished with exit code 0
死锁 死锁: 一直等待对方释放锁的情景就是死锁
死锁的结果
会造成应用程序的停止响应,不能再处理其它任务了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 import threadingimport timelock = 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 (30 ): sub_thread = threading.Thread(target=get_value, args=(i,)) sub_thread.start()
运行结果:
1 2 3 4 5 6 7 8 9 10 11 <Thread(Thread-1, started 123145487867904)> 3 <Thread(Thread-2, started 123145504657408)> 6 <Thread(Thread-3, started 123145521446912)> 8 <Thread(Thread-4, started 123145538236416)> 1 <Thread(Thread-5, started 123145555025920)> 下标越界: 4
我们发现程序停在此处了,下面代码显示了如何避免死锁的发生
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 import threadingimport timelock = 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) lock.release() return value = my_list[index] print (value) time.sleep(0.2 ) lock.release() if __name__ == '__main__' : for i in range (30 ): sub_thread = threading.Thread(target=get_value, args=(i,)) sub_thread.start()
结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <Thread(Thread-1, started 123145410772992)> 3 <Thread(Thread-2, started 123145427562496)> 6 <Thread(Thread-3, started 123145444352000)> 8 <Thread(Thread-4, started 123145461141504)> 1 <Thread(Thread-5, started 123145477931008)> 下标越界: 4 <Thread(Thread-6, started 123145494720512)> 下标越界: 5 <Thread(Thread-7, started 123145511510016)> 下标越界: 6 <Thread(Thread-8, started 123145528299520)> 下标越界: 7 Process finished with exit code 0
线程进程总结 关系对比 线程是依附在进程里面的,没有进程就没有线程。 一个进程默认提供一条线程,进程可以创建多个线程。 区别对比 进程之间不共享全局变量 线程之间共享全局变量,但是要注意资源竞争的问题,解决办法: 互斥锁或者线程同步 创建进程的资源开销要比创建线程的资源开销要大 进程是操作系统资源分配的基本单位,线程是CPU调度的基本单位 线程不能够独立执行,必须依存在进程中 多进程开发比单进程多线程开发稳定性要强 优缺点对比 总结 进程和线程都是完成多任务的一种方式 多进程要比多线程消耗的资源多,但是多进程开发比单进程多线程开发稳定性要强,某个进程挂掉不会影响其它进程。 多进程可以使用cpu的多核运行,多线程可以共享全局变量。 线程不能单独执行必须依附在进程里面 应用场景:计算密集性操作使用多进程;读写等io操作使用多线程。