python的知识回顾。本章是线程。
python之线程
1)简单的概念
并行是指两个或者多个事件在同一时刻发生,而并发是指两个或多个事件在同一时间间隔发生。. 并行是在不同实体上的多个事件,并发是在同一实体上的多个事件。
(1)直接创建 Thread ,将一个 callable 对象从类的构造器传递进去,这个 callable 就是回调函数,用来处理任务。
(2)编写一个自定义类继承 Thread,然后复写 run() 方法,在 run() 方法中编写任务处理代码,然后创建这个 Thread 的子类。
Thread 的构造方法中,最重要的参数是 target,所以我们需要将一个 callable 对象赋值给它,线程才能正常运行。
import threading
import time
def test():
for i in range(5):
print('test ',i)
time.sleep(1)
thread = threading.Thread(target=test)
thread.start()
(3)thread的生命周期
1.创建对象时,代表 Thread 内部被初始化。
2.调用 start() 方法后,thread 会开始运行。
3.thread 代码正常运行结束或者是遇到异常,线程会终止。
2)Thread 的 is_alive() 方法查询线程是否还在运行
is_alive() 返回 True 的情况是 Thread 对象被正常初始化,start() 方法被调用,然后线程的代码还在正常运行
import threading
import time
def test():
for i in range(5):
print(threading.current_thread().name+' test ',i)
time.sleep(0.5)
thread = threading.Thread(target=test,name='TestThread')
# thread = threading.Thread(target=test)
thread.start()
for i in range(5):
print(threading.current_thread().name+' main ', i)
print(thread.name+' is alive ', thread.isAlive())
time.sleep(1)
***************************输出*********************************
TestThread test 0
MainThread main 0
TestThread is alive True
TestThread test 1
MainThread main 1
TestThread is alive True
TestThread test 2
TestThread test 3
MainThread main TestThread test 24
TestThread is alive True
MainThread main 3
TestThread is alive False
MainThread main 4
TestThread is alive False
3)join()提供线程阻塞
默认的情况是,join() 会一直等待对应线程的结束,但可以通过参数赋值,等待规定的时间就好了。在start()方法后,添加join方法。
thread.start()
thread.join()
join规定时间:
timeout 是一个浮点参数,单位是秒
join(timeout=None):
4)Thread 中的 daemon 属性
TestThread 中 daemon 属性默认是 False,这使得 MainThread 需要等待它的结束,自身才结束
达到MainThread 结束,子线程也立马结束,在子进程中添加:
thread = threading.Thread(target=test,name='TestThread',daemon=True)5
import threading
import time
def test():
for i in range(5):
print(threading.current_thread().name+' test ',i)
time.sleep(2)
thread = threading.Thread(target=test,name='TestThread')
# thread = threading.Thread(target=test,name='TestThread',daemon=True)
thread.start()
for i in range(5):
print(threading.current_thread().name+' main ', i)
print(thread.name+' is alive ', thread.isAlive())
time.sleep(1)
*******************输出***********************
TestThread test 0
MainThread main 0
TestThread is alive True
MainThread main 1
TestThread is alive True
TestThread test 1
MainThread main 2
TestThread is alive True
MainThread main 3
TestThread is alive True
TestThread test 2
MainThread main 4
TestThread is alive True
TestThread test 3
TestThread test 4
5)自定义类继承Thread
import threading
import time
class TestThread(threading.Thread):
def __init__(self,name,age):
threading.Thread.__init__(self)
self.name = name
self.age = age
def run(self):
for i in range(5):
# print(threading.current_thread().name + ' test ', i)
print('{}岁的{}正在进行第{}次奔跑'.format(str(self.age),self.name,i))
time.sleep(1)
thread = TestThread(age = 4,name = 'dog')
thread.start()
thread.join()
thread2 = TestThread(age = 3,name = 'dog2')
thread2.start()
*******************输出***********************
4岁的dog正在进行第0次奔跑
4岁的dog正在进行第1次奔跑
4岁的dog正在进行第2次奔跑
4岁的dog正在进行第3次奔跑
4岁的dog正在进行第4次奔跑
3岁的dog2正在进行第0次奔跑
3岁的dog2正在进行第1次奔跑
3岁的dog2正在进行第2次奔跑
3岁的dog2正在进行第3次奔跑
3岁的dog2正在进行第4次奔跑
6)threading 模块中 Lock 类的用法
1.acquire(blocking=True, timeout=-1):请求对 Lock 或 RLock 加锁,其中 timeout 参数指定加锁多少秒。
2.release():释放锁。
Lock 和 RLock 的区别如下:
- threading.Lock:它是一个基本的锁对象,每次只能锁定一次,其余的锁请求,需等待锁释放后才能获取。
- threading.RLock:它代表可重入锁(Reentrant Lock)。对于可重入锁,在同一个线程中可以对它进行多次锁定,也可以多次释放。如果使用 RLock,那么 acquire() 和 release() 方法必须成对出现。如果调用了 n 次 acquire() 加锁,则必须调用 n 次 release() 才能释放锁。
RLock 锁具有可重入性。也就是说,同一个线程可以对已被加锁的 RLock 锁再次加锁,RLock 对象会维持一个计数器来追踪 acquire() 方法的嵌套调用,线程在每次调用 acquire() 加锁后,都必须显式调用 release() 方法来释放锁。所以,一段被锁保护的方法可以调用另一个被相同锁保护的方法。
Lock 是控制多个线程对共享资源进行访问的工具。通常,锁提供了对共享资源的独占访问,每次只能有一个线程对 Lock 对象加锁,线程在开始访问共享资源之前应先请求获得 Lock 对象。当对共享资源访问完成后,程序释放对 Lock 对象的锁定。
线程安全的类具有如下特征:
- 该类的对象可以被多个线程安全地访问。
- 每个线程在调用该对象的任意方法之后,都将得到正确的结果。
- 每个线程在调用该对象的任意方法之后,该对象都依然保持合理的状态。
7)信号量
互斥锁同时只允许一个线程更改数据,而Semaphore是同时允许一定数量的线程更改数据 ,比如厕所有3个坑,那最多只允许3个人上厕所,后面的人只能等里面有人出来了才能再进去。
import threading
import time
def run(n, semaphore):
semaphore.acquire() #加锁
time.sleep(1)
print("run the thread:%s\n" % n)
semaphore.release() #释放
if __name__ == '__main__':
num = 0
semaphore = threading.BoundedSemaphore(5) # 最多允许5个线程同时运行
for i in range(22):
t = threading.Thread(target=run, args=("t-%s" % i, semaphore))
t.start()
while threading.active_count() != 1:
pass # print threading.active_count()
else:
print('-----all threads done-----')
8)事件
python线程的事件用于主线程控制其他线程的执行,事件是一个简单的线程同步对象,其主要提供以下几个方法:
- clear 将flag设置为“False”
- set 将flag设置为“True”
- is_set 判断是否设置了flag
- wait 会一直监听flag,如果没有检测到flag就一直处于阻塞状态
事件处理的机制:全局定义了一个“Flag”,当flag值为“False”,那么event.wait()就会阻塞,当flag值为“True”,那么event.wait()便不再阻塞。
#利用Event类模拟红绿灯
import threading
import time
event = threading.Event()
def lighter():
count = 0
event.set() #初始值为绿灯
while True:
if 5 < count <=10 :
event.clear() # 红灯,清除标志位
print("\33[41;1mred light is on...\033[0m")
elif count > 10:
event.set() # 绿灯,设置标志位
count = 0
else:
print("\33[42;1mgreen light is on...\033[0m")
time.sleep(1)
count += 1
def car(name):
while True:
if event.is_set(): #判断是否设置了标志位
print("[%s] running..."%name)
time.sleep(1)
else:
print("[%s] sees red light,waiting..."%name)
event.wait()
print("[%s] green light is on,start going..."%name)
light = threading.Thread(target=lighter,)
light.start()
car = threading.Thread(target=car,args=("MINI",))
car.start()
9)全局解释器锁
在非python环境中,单核情况下,同时只能有一个任务执行。多核时可以支持多个线程同时执行。但是在python中,无论有多少核,同时只能执行一个线程。究其原因,这就是由于GIL的存在导致的。
GIL的全称是Global Interpreter Lock(全局解释器锁),来源是python设计之初的考虑,为了数据安全所做的决定。某个线程想要执行,必须先拿到GIL,我们可以把GIL看作是“通行证”,并且在一个python进程中,GIL只有一个。拿不到通行证的线程,就不允许进入CPU执行。GIL只在cpython中才有,因为cpython调用的是c语言的原生线程,所以他不能直接操作cpu,只能利用GIL保证同一时间只能有一个线程拿到数据。而在pypy和jpython中是没有GIL的。
Python多线程的工作过程:
python在使用多线程的时候,调用的是c语言的原生线程。
- 拿到公共数据
- 申请gil
- python解释器调用os原生线程
- os操作cpu执行运算
- 当该线程执行时间到后,无论运算是否已经执行完,gil都被要求释放
- 进而由其他进程重复上面的过程
- 等其他进程执行完后,又会切换到之前的线程(从他记录的上下文继续执行),整个过程是每个线程执行自己的运算,当执行时间到就进行切换(context switch)。
python针对不同类型的代码执行效率也是不同的:
1、CPU密集型代码(各种循环处理、计算等等),在这种情况下,由于计算工作多,ticks计数很快就会达到阈值,然后触发GIL的释放与再竞争(多个线程来回切换当然是需要消耗资源的),所以python下的多线程对CPU密集型代码并不友好。
2、IO密集型代码(文件处理、网络爬虫等涉及文件读写的操作),多线程能够有效提升效率(单线程下有IO操作会进行IO等待,造成不必要的时间浪费,而开启多线程能在线程A等待时,自动切换到线程B,可以不浪费CPU的资源,从而能提升程序执行效率)。所以python的多线程对IO密集型代码比较友好。
使用建议?
python下想要充分利用多核CPU,就用多进程。因为每个进程有各自独立的GIL,互不干扰,这样就可以真正意义上的并行执行,在python中,多进程的执行效率优于多线程(仅仅针对多核CPU而言)。
10)多线程
if __name__ == '__main__':
provinces = ["北京市", "天津市", "上海市", "重庆市", "河北省","河南省", "云南省", "辽宁省", "黑龙江省", "湖南省",
"广西壮族自治区", "广东省", "海南省"]
arr = ["原数据"]
po = Pool(processes=5) # 允许开几个进程
for province in provinces:
print("Add task:", province)
# 开启进程运行workMulti函数,传入参数province,arr。
# 注意最后一个逗号是必须的,不是多余的
po.apply_async(workMulti, args=(province, arr,))
print("AAA****************************")
po.close() # 关闭进程池入口,此后不能再向进程池中添加任务了
print("BBB****************************")
po.join() # 阻塞等待,只有进程池中所有任务都完成了才往下执行
print("CCC****************************")
**********************输出******************************
Add task: 北京市
Add task: 天津市
Add task: 上海市
Add task: 重庆市
Add task: 河北省
Add task: 河南省
Add task: 云南省
Add task: 辽宁省
Add task: 黑龙江省
Add task: 湖南省
Add task: 广西壮族自治区
Add task: 广东省
Add task: 海南省
AAA****************************
BBB****************************
['原数据']
2022-07-15 11:14:14 finish.... 北京市
['原数据']
2022-07-15 11:14:14 finish.... 天津市
['原数据']
2022-07-15 11:14:14 finish.... 上海市
['原数据']
2022-07-15 11:14:14 finish.... 重庆市
['原数据']
2022-07-15 11:14:14 finish.... 河北省
['原数据']
2022-07-15 11:14:24 finish.... 河南省
['原数据']
2022-07-15 11:14:24 finish.... 云南省
['原数据']
2022-07-15 11:14:24 finish.... 辽宁省
['原数据']
2022-07-15 11:14:24 finish.... 黑龙江省
['原数据']
2022-07-15 11:14:24 finish.... 湖南省
['原数据']
2022-07-15 11:14:34 finish.... 广西壮族自治区
['原数据']
2022-07-15 11:14:34 finish.... 广东省
['原数据']
2022-07-15 11:14:34 finish.... 海南省
CCC****************************
11)注意事项:
1、关于进程的开启代码一定要放在if name == ‘main’:代码之下,不能放到函数中或其他地方。
2、po.apply_async(workMulti, args=(province,arr,))开启进程调用workMulti,需要几个参数传几个,最后需要加一个逗号,因为其传递的参数是tuple类型。
3、进程之间的参数变量是不共享的, 在某个进程中修改其函数参数, 在其他进程中是不可见的。这里每次打印的都是[‘原数据’]足以说明。
4、进程池接受任务并非阻塞式。这里进程池虽然只开5个,但它可以一次性接受很多任务, 任务的执行由进程池Pool自行安排,这里打印的Add task是连续的,并不需要等待进程池有空进程。
5、这里为何要调用workMulti而不直接调用work?
答:假如work函数中报错,你会发现程序看起来运行正常,你将发现不了错误。通过在workMulti中加入try:…except:…以及traceback.print_exc(),我们可以打印出进程运行的异常。
6、多进程如何接收计算结果?
答:方法1:使用callback回调方法,我没使用过,不详细说明
方法2:在进程中把计算结果保存下来,如保存到数据库或文件,所有进程计算结束后再提取。我多采用这种方法,这种方法在部分进程计算失败后仍然能保留计算成功的那些结果。
7、开启多少个进程合适?
答:看你跑的是什么类型的任务。如果是计算密集型(耗CPU的),建议开启和CPU核心线程数一样的进程,如果同时你还要操作计算机或进行其他任务,那最好再留出一点CPU,不然将会卡死。如果是耗时型(不耗CPU,如网络请求等),可以考虑开多一些进程,不需要考虑CPU核心线程数。