2022年 11月 3日

python多任务编程(进程与线程)

目录

 

多任务的概念

多任务的执行方式

并发

并行

进程和线程概念

在python中多进程的使用

模块中的常用命令

Process()

start()

join()

terminate()

current_process()

例子:

创建子进程:

对进程的目标传参

注意事项

进程之间不共享全局变量

守护主进程

扩展

获取进程和父进程的编号

线程

在python中线程的使用

模块中的常用命令

Thread()

start()

join()

current_thread

Lock()

注意事项

线程之间执行时是无序的

子进程的输出结果发生混乱怎么办:

守护主进程

线程是共享全局变量

线程之间共享全局变量数据出现错误的情况

进程和线程对比

关系对比

区别对比

优缺点对比

进程优缺点:

线程优缺点


 

多任务的概念

简单说就是在同一时间内执行多个任务,现在主流的操作系统都满足这个。也叫做多任务操作系统。

多任务的执行方式

一共有两种。分别是并发和并行

并发

是在一段时间内交替执行任务,因为速度过快,所以肉眼上看是在同时执行,但是这种执行方式多在单核cpu中。现在也被淘汰掉了,只需要了解即可。

并行

是真正意义上的同时执行,现在的双核及以上都是这种执行方式。简单说就是这个核执行一个任务,另一个核执行另一个任务,从而达到同时执行多个任务的效果。

进程和线程概念

进程是操作系统进行资源分配时的基本单位。一个程序运行后至少有一个进程,一个进程默认有一个线程。进程里面可以创建多个线程,线程是依附在进程里面的,没有进程就没有线程。打个比方说可以理解为,window操作系统是一个大的进程,叫做父进程,然后里面运行了浏览器、pycharm等程序,每个程序为子进程,然后这些程序内部又在运行着不同程序实现不同的功能,这些程序就可以称为线程。父进程和子进程是没有绝对性的,根据看的角度不同是否为父进程或者子进程也是不同的。

进程是python程序中实现多任务的一种方式。

在python中多进程的使用

首先需要导入multiprocessing,这个是python内置的进程库。

模块中的常用命令

Process()

用来创建子进程。常用参数有:

  • group=None,.
    • 指定进程组,一般情况下默认即可
  • target=None,
    • 表示执行的目标任务名(函数、方法名)
  • name=None,进程名字
    • 表示给这个进程取一个名字,默认为Process-1,依次递增
  • args=(),
    • 以元组方式给执行任务传参,如果需要为目标函数或者方法传参时,注意一定要是元组,所以当只有一个参数时,需要加一个逗号
  • kwargs={}
    • 以字典方式给执行任务传参,如果需要为目标函数或者方法传参时,注意一定要是字典
  • daemon=None
    • 表示该子线程对主线程进行守护操作,使用时daemo的值输入为True,表示守护主进程

start()

用来启动创建好的子进程

join()

等待子进程执行结束

terminate()

不管任务是否执行完毕,立即终止子进程

current_process()

获取当前的进程名

例子:

创建子进程:

  1. import multiprocessing
  2. def work1():
  3. print('函数1正在工作中')
  4. print('当前进程名为:', multiprocessing.current_process()) # 获取进程名
  5. def work2():
  6. print('函数2正在工作中')
  7. print('当前进程名为:',multiprocessing.current_process()) # 获取进程名
  8. if __name__ == '__main__':
  9. work1_process = multiprocessing.Process(target=work1) #创建子进程
  10. work2_process = multiprocessing.Process(target=work2,name='进程2') #创建子进程
  11. work1_process.start() #运行子进程
  12. work2_process.start() #运行子进程
  13. '''
  14. 函数1正在工作中
  15. 当前进程名为: <Process name='Process-1' parent=7648 started>
  16. 函数2正在工作中
  17. 当前进程名为: <Process name='进程2' parent=7648 started>
  18. '''

对进程的目标传参

  1. import multiprocessing
  2. def work1(a): #位置传参
  3. for i in range(a):
  4. print('函数1正在工作中')
  5. def work2(sum=None): #关键字传参
  6. for i in range(sum):
  7. print('函数2正在工作中')
  8. if __name__ == '__main__':
  9. work1_process=multiprocessing.Process(target=work1,args=(3,)) #位置传参
  10. work2_process=multiprocessing.Process(target=work2,kwargs={'sum':3}) #关键字传参
  11. work1_process.start()
  12. work2_process.start()
  13. '''
  14. 函数1正在工作中
  15. 函数1正在工作中
  16. 函数1正在工作中
  17. 函数2正在工作中
  18. 函数2正在工作中
  19. 函数2正在工作中
  20. '''

如果有多个参数的话,方法一样,在Process内的参数中输入即可 

注意事项

进程之间不共享全局变量

  1. import multiprocessing
  2. list1=[] #全局变量
  3. def work1():
  4. for i in range(10):
  5. list1.append(i)
  6. print('函数1中的列表为:',list1)
  7. def work2():
  8. for i in range(0,20,2):
  9. list1.append(i)
  10. print('函数2中的列表为:',list1)
  11. if __name__ == '__main__': #主进程
  12. work1_process=multiprocessing.Process(target=work1)
  13. work2_process=multiprocessing.Process(target=work2)
  14. work1_process.start()
  15. work2_process.start()
  16. print('全局中的列表为:', list1)
  17. '''
  18. 全局中的列表为: []
  19. 函数1中的列表为: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
  20. 函数2中的列表为: [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
  21. '''

上面例子中可以发现,在子进程中的全局变量不会影响到父进程中的全局变量 ,进程之间的变量不会进行一个共享。

守护主进程

在上面的进程之间不共享全局变量的例子中,可以发现主进程中的print(‘全局中的列表为:’, list1)语句已经运行结束了,而子进程却还在运行,就好比你已经关闭了视频播放软件,而里面的视频却还在播放,这个时候就需要守护主程序了,简单说就是当主程序运行结束时,里面的子程序就必须进行销毁或者执行结束。一共有两种方法可以守护主程序。

  • deamo = True

    • 格式是子进程名.deamo = True。必须放在该子进程开始前。也可以在创建时输入该参数

  1. import multiprocessing
  2. def work1():
  3. for i in range(3):
  4. print('函数1:',i)
  5. def work2():
  6. for i in range(3):
  7. print('函数2:',i)
  8. if __name__ == '__main__':
  9. work1_process=multiprocessing.Process(target=work1)
  10. work2_process=multiprocessing.Process(target=work2)
  11. work1_process.daemon=True #只对进程1进行守护主进程
  12. work1_process.start() #进程1执行
  13. work2_process.start() #进程2执行,但是不对主程序守护
  14. print('主进程结束')
  15. '''
  16. 主进程结束
  17. 函数2: 0
  18. 函数2: 1
  19. 函数2: 2
  20. '''

上面的例子中,只对进程1进行了守护主进程操作,因为主进程中的 print(‘主进程结束’)结束过快,所以导致进程1没有输出就直接关闭运行了,而进程2没有进行守护主程序操作,所以在主进程中的语句结束后,仍在运行。

  • terminate()

    • 格式是子进程名.terminate(),放在该子进程运行后即可

  1. import multiprocessing
  2. def work1():
  3. for i in range(3):
  4. print('函数1:',i)
  5. def work2():
  6. for i in range(3):
  7. print('函数2:',i)
  8. if __name__ == '__main__':
  9. work1_process=multiprocessing.Process(target=work1)
  10. work2_process=multiprocessing.Process(target=work2)
  11. work1_process.start() #进程1执行
  12. work1_process.terminate()
  13. work2_process.start() #进程2执行,但是不对主程序守护
  14. print('主进程结束')
  15. '''
  16. 主进程结束
  17. 函数2: 0
  18. 函数2: 1
  19. 函数2: 2
  20. '''

与上面的效果一样,两个方法只需要使用任意一个即可,不需要两个同时使用。

扩展

获取进程和父进程的编号

使用时需要导入os模块,也是python中的内置模块。

  • 获取当前进程的编号
    • os.getpid()
  • 获取当前父进程的编号
    • os.getppid()

  1. import os
  2. import multiprocessing
  3. def work():
  4. print('当前编号名为',os.getpid())
  5. print('当前父进程编号为',os.getppid())
  6. if __name__ == '__main__':
  7. print('主进程的编号为:',os.getpid())
  8. work_process=multiprocessing.Process(target=work)
  9. work_process.start()
  10. '''
  11. 主进程的编号为: 8292
  12. 当前编号名为 11820
  13. 当前父进程编号为 8292
  14. '''

注意: if __name__ == ‘__main__’:为当前的主进程,所以内部的子进程输出的父进程编号与它的进程编号相同

线程

与进程一样,也是cpu调度的基本单位,是进程中执行代码的一个分支,每一个进程至少会有一个线程。

在python中线程的使用

需要导入threading 库,至少python中内置的线程库

模块中的常用命令

Thread()

用来创建子线程,常用参数有:

  • group=None,.
    • 指定进程组,一般情况下默认即可
  • target=None,
    • 表示执行的目标任务名(函数、方法名)
  • name=None,进程名字
    • 表示给这个进程取一个名字,默认为Thread-1 函数名,依次类推
  • args=(),
    • 以元组方式给执行任务传参,如果需要为目标函数或者方法传参时,注意一定要是元组,所以当只有一个参数时,需要加一个逗号
  • kwargs={}
    • 以字典方式给执行任务传参,如果需要为目标函数或者方法传参时,注意一定要是字典
  • daemon=None
    • 表示该子线程对主线程进行守护操作,使用时daemo的值输入为True,表示守护主进程

start()

运行创建的子进程

join()

等待子线程执行结束之后再执行后续的代码

current_thread

 获取当前的进程名

Lock()

创建互斥锁,可对共享数据进行锁定,保证同一时刻只能有一个线程取操作,具体方法在注意事项中有写。

  • 锁名.threading.Lock()
    • 表示创建的一个叫该名字的互斥锁
  • acquire()
    • 上锁,创建锁后要进行上锁操作
    • 锁名.acquire()
  • release()
    • 释放锁,上锁后等使用完毕后还要有释放锁的操作
    • 锁名.release()

这些常用命令除互斥锁以外使用方法都与进程库中的常用命令方法相同

注意事项

线程之间执行时是无序的

由于线程是由cpu随机调度的,cpu先调度哪个线程,哪个线程优先执行,没有调度的线程不会执行。

  1. import threading
  2. import time
  3. def work():
  4. time.sleep(0.1) #暂停0.1秒使效果更明显
  5. print('当前的进程:', threading.current_thread()) #打印当前进程名
  6. if __name__ == '__main__':
  7. for i in range(5): #循环5次创建5个子进程
  8. work_thread = threading.Thread(target=work)
  9. work_thread.start()
  10. '''
  11. 当前的进程:当前的进程:当前的进程: 当前的进程: <Thread(Thread-4 (work), started 3552)>
  12. <Thread(Thread-5 (work), started 3104)>
  13. <Thread(Thread-1 (work), started 3740)>
  14. 当前的进程:<Thread(Thread-3 (work), started 824)>
  15. <Thread(Thread-2 (work), started 11948)>
  16. '''

观察上面代码的输出结果,可以发现5个子进程的运行顺序是完全无序的,并且可以注意到,打印的结果也发生了混乱。

子进程的输出结果发生混乱怎么办:

这个时候就可以使用库中的join()方法,表示当上一个子线程结束后再运行下一个。

  1. import threading
  2. import time
  3. def work1():
  4. time.sleep(0.1) #暂停0.1秒使效果更明显
  5. print('当前的进程:', threading.current_thread()) # 打印当前进程名
  6. if __name__ == '__main__':
  7. for i in range(10):
  8. work1_thread = threading.Thread(target=work1)
  9. work1_thread.start()
  10. work1_thread.join()
  11. '''
  12. 当前的进程: <Thread(Thread-1 (work1), started 6576)>
  13. 当前的进程: <Thread(Thread-2 (work1), started 2136)>
  14. 当前的进程: <Thread(Thread-3 (work1), started 3632)>
  15. 当前的进程: <Thread(Thread-4 (work1), started 6816)>
  16. 当前的进程: <Thread(Thread-5 (work1), started 4332)>
  17. 当前的进程: <Thread(Thread-6 (work1), started 6692)>
  18. 当前的进程: <Thread(Thread-7 (work1), started 12144)>
  19. 当前的进程: <Thread(Thread-8 (work1), started 6656)>
  20. 当前的进程: <Thread(Thread-9 (work1), started 5476)>
  21. 当前的进程: <Thread(Thread-10 (work1), started 11384)>
  22. '''

使用后输出的结果没有混乱,并且子进程的运行的是按顺序依次运行的,但是有一个很严重的问题是,使用后效率降低了,因为这样运行后多任务变成单任务了。

守护主进程

  1. import threading
  2. def work():
  3. print('当前的进程:', threading.current_thread()) #打印当前进程名
  4. if __name__ == '__main__':
  5. work_thread=threading.Thread(target=work)
  6. work_thread.start()
  7. print('主进程结束')
  8. #当前的进程:主进程结束 <Thread(Thread-1 (work), started 5088)>

上面的例子中,可以发现,主进程中的代码已经运行结束了,但是子进程却没有结束,这个时候就需要进行对主进程进行守护操作。共有两种方法:

  • 方法一:daemo
    • 与进程中的daemo使用方法相同。线程名.daemo=True。也可以在创建子进程时加上这句话。
  1. import threading
  2. import time
  3. def work1():
  4. time.sleep(0.1) #暂停0.1秒效果更明显
  5. print('函数1正在工作中')
  6. if __name__ == '__main__':
  7. for i in range(5):
  8. work1_thread=threading.Thread(target=work1)
  9. # work1_thread = threading.Thread(target=work1, daemon=True)
  10. work1_thread.daemon=True
  11. work1_thread.start()
  12. print('主进程结束')
  13. #主进程结束

两种daeomo的方法使用一种即可。因为主进程中的输出语句太快了,线程中的语句还没有输出就已经结束了,所以输出结果只有主进程中的语句 

  • 方法二:setDaemo

 在子进程运行前加上线程名.setDaemon(True)。即可表示守护主进程

  1. import threading
  2. import time
  3. def work1():
  4. time.sleep(0.1) #暂停0.1秒效果更明显
  5. print('函数1正在工作中')
  6. if __name__ == '__main__':
  7. for i in range(5):
  8. work1_thread=threading.Thread(target=work1)
  9. work1_thread.setDaemon(True)
  10. work1_thread.start()
  11. print('主进程结束')
  12. #主进程结束

方法一和方法二中使用其中一种即可,但是我个人推荐使用第一种。因为在最新的python3.10版本中,setDaemon方法可能有bug,有时候会出现不支持该方法的情况

  1. import threading
  2. def work():
  3. print('当前的进程:', threading.current_thread()) #打印当前进程名
  4. if __name__ == '__main__':
  5. work_thread = threading.Thread(target=work)
  6. work_thread.setDaemon(True)
  7. work_thread.start()
  8. print('主进程结束')

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBASEhZWkJD,size_20,color_FFFFFF,t_70,g_se,x_16 这么看是没有大问题的,但是当我多试试几次后就会有几率的发生下面的情况

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBASEhZWkJD,size_20,color_FFFFFF,t_70,g_se,x_16 就会发生 setDaemon方法并没有起到守护主进程的作用,我也不知道是我的问题还是版本问题,老版本我也没有去试过。只能说个人推荐尽可能是使用第一种方法吧。

线程是共享全局变量

  1. import threading
  2. list1=[]
  3. def work1():
  4. for i in range(10):
  5. list1.append(i)
  6. print('函数1中的列表数据为:',list1)
  7. def work2():
  8. for i in range(10,15):
  9. list1.append(i)
  10. print('函数2中的列表数据为:', list1)
  11. if __name__ == '__main__':
  12. work1_thread=threading.Thread(target=work1)
  13. work2_thread=threading.Thread(target=work2)
  14. work1_thread.start()
  15. work2_thread.start()
  16. print('主进程中的列表数据为:',list1)
  17. '''
  18. 函数1中的列表数据为: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
  19. 函数2中的列表数据为:主进程中的列表数据为: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]
  20. [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]
  21. '''

观察上面的例子中可以发现,线程中的全局变量是共同享有的。那么就出现了一个问题,由于线程是共同运行的,线程中的变量又是共同享有的,那么就有可能发生线程与线程之间争抢变量的可能。从而导致全局数据混乱的现象。

线程之间共享全局变量数据出现错误的情况

  1. import threading
  2. import time
  3. num=0
  4. def work1():
  5. global num
  6. for i in range(1000000): #累加1000000次
  7. num+=1
  8. print('函数1中的num为',num)
  9. def work2():
  10. global num
  11. for i in range(1000000): ##累加1000000次
  12. num+=1
  13. print('函数2中的num为',num)
  14. if __name__ == '__main__':
  15. work1_thread=threading.Thread(target=work1)
  16. work2_thread=threading.Thread(target=work2)
  17. work1_thread.start()
  18. work2_thread.start()
  19. time.sleep(0.5)
  20. print('主进程中的num为',num)
  21. '''
  22. 函数1中的num为 1430285
  23. 函数2中的num为 2000000
  24. 主进程中的num为 2000000
  25. '''

这个例子中更明显的看到了数据混乱的情况 

这种情况一共有两种方法:

  • 第一种方法:线程同步
    • 线程同步就是让线程进行协同步调,按预习的先后次序进行运行。使用上面说过的join进行先后排序即可。上面有使用方法就不举例子了
  • 第二种方法:对共享数据进行锁定,保证同一时刻只能有一个线程去操作
    • 对共享数据进行锁定,就要使用上面常用命令中提到的互斥锁了。互斥锁的作用就是保证多个线程访问共享数据时不会出现数据错误委托
  1. import threading
  2. import time
  3. num=0
  4. suo=threading.Lock() #创建互斥锁
  5. def work1():
  6. global num
  7. suo.acquire() #进行上锁
  8. for i in range(1000000): #累加1000000次
  9. num+=1
  10. print('函数1中的num为',num)
  11. suo.release() #释放锁
  12. def work2():
  13. global num
  14. suo.acquire() # 进行上锁
  15. for i in range(1000000): ##累加1000000次
  16. num+=1
  17. print('函数2中的num为',num)
  18. suo.release() # 释放锁
  19. if __name__ == '__main__':
  20. work1_thread=threading.Thread(target=work1)
  21. work2_thread=threading.Thread(target=work2)
  22. work1_thread.start()
  23. work2_thread.start()
  24. time.sleep(0.5)
  25. print('主进程中的num为',num)
  26. '''
  27. 函数1中的num为 1000000
  28. 函数2中的num为 2000000
  29. 主进程中的num为 2000000
  30. '''

使用互斥锁后,其他子进程必须要等到当前操作全局变量的子进程运行结束后才会运行。但是一定要注意,互斥锁使用完毕后一定要记得释放,否则锁与锁之间会卡在一起成为死锁,严重时会导致软件无响应死机。

  1. import threading
  2. import time
  3. num=0
  4. suo=threading.Lock() #创建互斥锁
  5. def work1():
  6. global num
  7. suo.acquire() #进行上锁
  8. for i in range(1000000): #累加1000000次
  9. num+=1
  10. print('函数1中的num为',num)
  11. def work2():
  12. global num
  13. suo.acquire() # 进行上锁
  14. for i in range(1000000): ##累加1000000次
  15. num+=1
  16. print('函数2中的num为',num)
  17. suo.release() # 释放锁
  18. if __name__ == '__main__':
  19. work1_thread=threading.Thread(target=work1)
  20. work2_thread=threading.Thread(target=work2)
  21. work1_thread.start()
  22. work2_thread.start()
  23. time.sleep(0.5)
  24. print('主进程中的num为',num)

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBASEhZWkJD,size_20,color_FFFFFF,t_70,g_se,x_16

 还是上面的例子,但是在函数1中的使用完互斥锁后并没有释放,从而会导致代码卡住无法结束,时间一长则会导致软件死机。

注意:无论是互斥锁还剩线程同步,当对全局变量操作进行控制时,多任务都会瞬间变成单任务,性能会下降,也就是说同一时刻只能有一个线程去执行。

进程和线程对比

关系对比

  • 线程是依附在进程里面的,没有进程就没有线程

  • 一个进程会默认提供一条线程,进程可以创建多个线程

区别对比

  • 进程之间不共享全局变量

  • 线程之间共享全局变量,但是要注意资源竞争的问题,解决方法:互斥锁或线程同步

  • 创建进程的资源开销要比创建线程的资源开销要大

  • 线程不能够独立执行,必须依存在进程中

  • 多进程开发比单进程多线程开发稳定性要强

优缺点对比

进程优缺点:

  • 优点:可以用多核

  • 缺点:资源开销大

线程优缺点

  • 优点: 资源开销小

  • 缺点:不能使用多核