Python多任务之:进程与线程详解
进程与线程知识点详解
多任务
1.定义
- 指在同一时间内执行多个任务
2.执行方式
-
并发:在一段时间内交替去执行任务
-
并行:真正意义的同时执行多个任务
进程
1.定义
-
一个正在运行的程序或者软件就是一个进程
-
是操作系统进行资源分配的基本单位
2.作用
- 每个进程可以各自运行,执行各自的任务,提高效率
3.编写步骤
-
导入进程包:
import multiprocessing
-
创建子进程并指定执行的任务:
-
multiprocessing.Process(target=任务名)
-
若需传参,参数传递形式分两种:
-
元组方式传参(args): 元组方式传参一定要和参数的顺序保持一致:args=(参数1,)
-
字典方式传参(kwargs): 字典方式传参字典中的key一定要和参数名保持一致:kwargs={"形参名":参数1}
-
-
-
启动进程执行任务
- 进程名.start()
-
简单实例:
import multiprocessing
def fun_1(n):
for i in range(n):
print("这是一个子进程1")
def fun_2(n):
for i in range(n):
print("这是一个子进程2")
if __name__ == '__main__':
process1 = multiprocessing.Process(target=fun_1,args=(5,))
process2 = multiprocessing.Process(target=fun_2,args=(10,))
process1.start()
process2.start()
输出结果:
这是一个子进程1
这是一个子进程2
这是一个子进程1
这是一个子进程2
这是一个子进程1
这是一个子进程2
这是一个子进程1
这是一个子进程2
这是一个子进程1
这是一个子进程2
这是一个子进程2
这是一个子进程2
这是一个子进程2
这是一个子进程2
这是一个子进程2
4.进程之间不共享全局变量
- 创建子进程会对主进程资源进行拷贝,也就是说子进程是主进程的一个副本,好比是一对双胞胎,之所以进程之间不共享全局变量,是因为操作的不是同一个进程里面的全局变量,只不过不同进程里面的全局变量名字相同而已。
import multiprocessing
import time
# 定义全局变量
g_list = list()
# 添加数据的任务
def add_data():
for i in range(5):
g_list.append(i)
print("add:", i)
time.sleep(0.2)
# 代码执行到此,说明数据添加完成
print("add_data:", g_list)
def read_data():
print("read_data", g_list)
if __name__ == '__main__':
# 创建添加数据的子进程
add_data_process = multiprocessing.Process(target=add_data)
# 创建读取数据的子进程
read_data_process = multiprocessing.Process(target=read_data)
# 启动子进程执行对应的任务
add_data_process.start()
# 主进程等待添加数据的子进程执行完成以后程序再继续往下执行,读取数据
add_data_process.join()
read_data_process.start()
print("main:", g_list)
# 总结: 多进程之间不共享全局变量
输出:
add: 0
add: 1
add: 2
add: 3
add: 4
add_data: [0, 1, 2, 3, 4]
main: []
read_data []
主进程及另一子进程的 g_list均为空
进程之间不共享全局变量的解释效果图:

5.获取进程编号
-
目的
- 验证主进程和子进程的关系,得知子进程是由那个主进程创建出来的
-
获取方式
先导入os模块
-
获取当前进程编号:
os.getpid()
-
获取当前父进程编号:
os.getppid()
-
6.特别注意事项
-
一个程序运行至少有一个进程
-
一个进程默认有一个线程
-
进程里面可以创建多个线程,线程是依附在进程里面的,没有进程就没有线程
-
进程之间不共享全局变量
-
主进程默认会等待所有的子进程执行结束再结束
-
设置守护主进程的目的是主进程退出子进程销毁,不让主进程再等待子进程去执行
-
设置守护主进程方式:
子进程对象.daemon = True
,实例化子进程中设置参数daemon = True
-
销毁子进程方式:
子进程对象.terminate()
-
-
进程之间执行也是无序的
- 由操作系统调度决定的
7.其他
-
进程名.is_alive() 判断进程是否还活在
-
进程的状态
-
就绪态:运行的条件都已经满足,正在等在cpu执行
-
执行态:cpu正在执行其功能
-
等待态:等待某些条件满足,例如一个程序sleep了,此时就处于等待态
-
线程
1.概念
-
线程是进程执行代码的一个分支
-
线程是CPU调度的基本单位
2.作用
- 多线程可以完成多任务
3.多线程的使用步骤(与多进程基本相同)
-
导入线程模块:
import threading
-
创建子线程并指定执行的任务
-
threading.Thread(target=任务名, args(,), kwargs{})
-
若需传参,参数传递形式分两种:
-
元组方式传参(args): 元组方式传参一定要和参数的顺序保持一致:args=(参数1,)
-
字典方式传参(kwargs): 字典方式传参字典中的key一定要和参数名保持一致:kwargs={"形参名":参数1}
-
-
-
启动线程执行任务:
线程名.start()
4.守护主线程
主线程会等待所以子线程执行结束再结束
*假如我们想让主线程运行特定时间后同时销毁子线程,可以设置子线程守护:
设置主线程守护有两种方式:
-
threading.Thread(target=show_info, daemon=True)
-
线程对象.setDaemon(True)
代码示例:
import threading
import time
# 测试主线程是否会等待子线程执行完成以后程序再退出
def show_info():
for i in range(5):
print("test:", i)
time.sleep(0.5)
if __name__ == '__main__':
# 创建子线程守护主线程
# daemon=True 守护主线程
# 守护主线程方式1
sub_thread = threading.Thread(target=show_info, daemon=True)
# 设置成为守护主线程,主线程退出后子线程直接销毁不再执行子线程的代码
# 守护主线程方式2
# sub_thread.setDaemon(True)
sub_thread.start()
# 主线程延时1秒
time.sleep(1)
print("over")
执行结果:
test: 0
test: 1
over
特别注意:当有多个子线程时,必须设置每个子进程的daemon=True,才能实现主线程守护,这点不同于多进程中的情形,因为线程是依附于进程存在的.
5.线程之间共享全局变量数据出现错误问题
示例代码:
import threading
# 定义全局变量
g_num = 0
# 循环一次给全局变量加1
def sum_num1():
for i in range(1000000):
global g_num
g_num += 1
print("sum1:", g_num)
# 循环一次给全局变量加1
def sum_num2():
for i in range(1000000):
global g_num
g_num += 1
print("sum2:", g_num)
if __name__ == '__main__':
# 创建两个线程
first_thread = threading.Thread(target=sum_num1)
second_thread = threading.Thread(target=sum_num2)
# 启动线程
first_thread.start()
# 启动线程
second_thread.start()
执行结果:
sum1: 1210949
sum2: 1496035
可见数据结果明显发生了错误
错误分析:
两个线程first_thread和second_thread都要对全局变量g_num(默认是0)进行加1运算,但是由于是多线程同时操作,有可能出现下面情况:
-
在g_num=0时,first_thread取得g_num=0。此时系统把first_thread调度为”sleeping”状态,把second_thread转换为”running”状态,t2也获得g_num=0
-
然后second_thread对得到的值进行加1并赋给g_num,使得g_num=1
-
然后系统又把second_thread调度为”sleeping”,把first_thread转为”running”。线程t1又把它之前得到的0加1后赋值给g_num。
-
这样导致虽然first_thread和first_thread都对g_num加1,但结果仍然是g_num=1
全局变量数据错误的解决办法:
线程同步: 保证同一时刻只能有一个线程去操作全局变量 同步: 就是协同步调,按预定的先后次序进行运行。如:你说完,我再说, 好比现实生活中的对讲机
线程同步的方式:
-
线程等待(join)
-
互斥锁
6.注意
-
线程之间执行是无序的 : 由cpu调度决定的
-
主线程会等待所有的子线程执行结束再结束
-
线程之间共享全局变量
-
线程之间共享全局变量数据可能会出现错误问题
解决办法:
-
1.线程等待
线程等待就是使用线程的.join()
函数,他会使当前线程运行完成后
-
1.线程等待
if __name__ == '__main__':
# 创建两个线程
first_thread = threading.Thread(target=sum_num1)
second_thread = threading.Thread(target=sum_num2)
# 启动线程
first_thread.start()
#启动线程等待
first_thread.join()
# 启动线程
second_thread.start()
-
2.互斥锁
互斥锁: 对共享数据进行锁定,保证同一时刻只能有一个线程去操作。
threading模块中定义了Lock变量,这个变量本质上是一个函数,通过调用这个函数可以获取一把互斥锁。
互斥锁使用步骤:
# 创建锁
mutex = threading.Lock()
# 上锁
mutex.acquire()
...这里编写代码能保证同一时刻只能有一个线程去操作, 对共享数据进行锁定...
# 释放锁
mutex.release()
7.使用互斥锁改写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
通过执行结果可以地址互斥锁能够保证多个线程访问共享数据不会出现数据错误问题.
小结:
- 互斥锁的作用就是保证同一时刻只能有一个线程去操作共享数据,保证共享数据不会出现错误问题
- 使用互斥锁的好处确保某段关键代码只能由一个线程从头到尾完整地去执行
- 使用互斥锁会影响代码的执行效率,多任务改成了单任务执行
- 互斥锁如果没有使用好容易出现死锁的情况
8.死锁
**死锁是什么: **
- 当一方未释放锁,另一方一直等待对方释放锁的情景就是死锁。
死锁的危害:
- 会造成应用程序的停止响应,不能再处理其它任务了。
总结:
- 使用互斥锁的时候需要注意死锁的问题,要在合适的地方注意释放锁。
- 死锁一旦产生就会造成应用程序的停止响应,应用程序无法再继续往下执行了。
9.进程与线程的对比
关系对比:
- 线程是依附在进程里面的,没有进程就没有线程。
- 一个进程默认提供一条线程,进程可以创建多个线程。
区别对比:
-
进程之间不共享全局变量
-
线程之间共享全局变量,但是要注意资源竞争的问题,解决办法: 互斥锁或者线程同步
-
创建进程的资源开销要比创建线程的资源开销要大
-
进程是操作系统资源分配的基本单位,线程是CPU调度的基本单位
-
线程不能够独立执行,必须依存在进程中
-
多进程开发比单进程多线程开发稳定性要强
优缺点对比:
- 进程优缺点:
优点:可以用多核
缺点:资源开销大 - 线程优缺点:
优点:资源开销小
缺点:不能使用多核
关于GIL锁:
这里不得不提的是python中全局GIL锁的问题,使得多线程并不能是真正的多线程,而是同一时间只能执行一个线程,其在计算场景及大部分计算情景下的效率提升显得鸡肋,甚至还不如单线程。
参考网址:http://cenalulu.github.io/python/gil-in-python/
引用一段解释:
转一篇关于Python GIL的文章。
归纳一下,CPU的大规模电路设计基本已经到了物理意义的尽头,所有厂商们都开始转向多核以进一步提高性能。Python为了能利用多核多线程的的优势,但又要保证线程之间数据完整性和状态同步,就采用了最简单的加锁的方式(所以说Python的GIL是设计之初一时偷懒造成的!)。Python库的开发者们接受了这个设定,即默认Python是thread-safe,所以开始大量依赖这个特性,无需在实现时考虑额外的内存锁和同步操作。但是GIL的设计有时会显得笨拙低效,但是此时由于内置库和第三方库已经对GIL形成了牢不可破的依赖,想改革GIL反而变得困难了(晕!)。所以目前的现状就是,Python的多线程在多核CPU上,只对于IO密集型计算产生正面效果;而当有至少有一个CPU密集型线程存在,那么多线程效率会由于GIL而大幅下降。虽然Python社区也在不断为此努力改进,但恐怕短时间内不会有改变,所以想规避GIL的,可以使用多进程的multiprocessing或concurrent.futures模块,或者换个Python的解析器。
作者:SeanCheney
链接:https://www.jianshu.com/p/9eb586b64bdb
来源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。
完。