Python 并发编程系列之多线程
2 创建线程
2.1 函数的方式创建线程
2.2 类的方式创建线程
3 Thread 类的常用属性和方法
3.1 守护线程: Deamon
3.2 join()方法
4 线程间的同步机制
4.1 互斥锁: Lock
4.2 递归锁: RLock
4.3 Condition
4.4 信号量: Semaphore
4.5 事件: Event
4.6 定时器: Timer
5 线程间的通行
5.1 队列: Queue
6 线程池
7 总结
上一篇博文详细总结了 Python 进程的用法, 这一篇博文来所以说 Python 中线程的用法. 实际上, 程序的运行都是以线程为基本单位的, 每一个进程中都至少有一个线程(主线程), 线程又可以创建子线程. 线程间共享数据比进程要容易得多(轻而易举), 进程间的切换也要比进程消耗 CPU 资源少.
线程管理可以通过 thead 模块 (Python 中已弃用) 和 threading 模块, 但目前主要以 threading 模块为主. 因为更加先进, 有更好的线程支持, 且 threading 模块的同步原语远多于 thread 模块. 另外, thread 模块中的一些属性会和 threading 模块有冲突. 故, 本文创建线程和使用线程都通过 threading 模块进行.
threading 模块提供的类: Thread, Lock, Rlock, Condition, [Bounded]Semaphore, Event, Timer, local.
threading 模块提供的常用方法:
threading.currentThread(): 返回当前的线程变量.
threading.enumerate(): 返回一个包含正在运行的线程的 list. 正在运行指线程启动后, 结束前, 不包括启动前和终止后的线程.
threading.activeCount(): 返回正在运行的线程数量, 与 len(threading.enumerate())有相同的结果.
threading 模块提供的常量:
threading.TIMEOUT_MAX 设置 threading 全局超时时间.
2 创建线程
无论是用自定义函数的方法创建线程还是用自定义类的方法创建线程与创建进程的方法极其相似的, 不过, 创建线程时, 可以不在 "if __name__=="__main__:"语句下进行". 无论是哪种方式, 都必须通过 threading 模块提供的 Thread 类进行. Thread 类常用属性和方法如下.
Thread 类属性:
name: 线程名
ident: 线程的标识符
daemon: 布尔值, 表示这个线程是否是守护线程
Thread 类方法:
__init__(group=None,target=None,name=None,args=(),kwargs={},verbose=None,daemon=None): 实例化一个线程对象, 需要一个可调用的 target 对象, 以及参数 args 或者 kwargs. 还可以传递 name 和 group 参数. daemon 的值将会设定 thread.daemon 的属性
start(): 开始执行该线程
run(): 定义线程的方法.(通常开发者应该在子类中重写)
join(timeout=None): 直至启动的线程终止之前一直挂起; 除非给出了 timeout(单位秒), 否则一直被阻塞
isAlive: 布尔值, 表示这个线程是否还存活(驼峰式命名, python2.6 版本开始已被取代)
isDaemon(): 布尔值, 表示是否是守护线程
setDaemon(布尔值): 在线程 start()之前调用, 把线程的守护标识设定为指定的布尔值
在下面两小节我们分别通过代码来演示.
2.1 自定义函数的方式创建线程importos
importtime
importthreading
deffun(n):
print('子线程开始运行......')
time.sleep(1)
my_thread_name=threading.current_thread().name# 获取当前线程名称
my_thread_id=threading.current_thread().ident# 获取当前线程 id
print('当前线程为:{}, 线程 id 为:{}, 所在进程为:{}, 您输入的参数为:{}'.format(my_thread_name,my_thread_id,os.getpid(),n))
print('子线程运行结束......')
t=threading.Thread(target=fun,name='线程 1',args=('参数 1',))
t.start()
time.sleep(2)
main_thread_name=threading.current_thread().name# 获取当前线程名称
main_thread_id=threading.current_thread().ident# 获取当前线程 id
print('主线程为:{}, 线程 id 为:{}, 所在进程为:{}'.format(main_thread_name,main_thread_id,os.getpid()))
2.2 类的方式创建线程importos
importtime
importthreading
classMyThread(threading.Thread):
def__init__(self,n,name=None):
super().__init__()
self.name=name
self.n=n
defrun(self):
print('子线程开始运行......')
time.sleep(1)
my_thread_name=threading.current_thread().name# 获取当前线程名称
my_thread_id=threading.current_thread().ident# 获取当前线程 id
print('当前线程为:{}, 线程 id 为:{}, 所在进程为:{}, 您输入的参数为:{}'.format(my_thread_name,my_thread_id,os.getpid(),self.n))
print('子线程运行结束......')
t=MyThread(name='线程 1',n=1)
t.start()
time.sleep(2)
main_thread_name=threading.current_thread().name# 获取当前线程名称
main_thread_id=threading.current_thread().ident# 获取当前线程 id
print('主线程为:{}, 线程 id 为:{}, 所在进程为:{}'.format(main_thread_name,main_thread_id,os.getpid()))
输出结果:
子线程开始运行......
当前线程为: 线程 1, 线程 id 为: 11312, 所在进程为: 4532, 您输入的参数为: 1
子线程运行结束......
主线程为: MainThread, 线程 id 为: 10868, 所在进程为: 4532
上述两块代码输出结果是一样的(id 不一样), 观察输出结果可以发现, 子线程和主线程所在的进程都是一样的, 证明是在同一进程中的进程.
3 Thread 的常用方法和属性
3.1 守护线程: Deamon
Thread 类有一个名为 deamon 的属性, 标志该线程是否为守护线程, 默认值为 False, 当为设为 True 是表示设置为守护线程. 是否是守护线程有什么区别呢? 我们先来看看 deamon 值为 False(默认)情况时:importos
importtime
importthreading
deffun():
print('子线程开始运行......')
foriinrange(6):#运行3秒,每秒输出一次
time.sleep(1)
my_thread_name=threading.current_thread().name
print('{}已运行 {} 秒......'.format(my_thread_name,i+1))
print('子线程运行结束......')
print('主线程开始运行......')
t=threading.Thread(target=fun,name='线程 1')
print('daemon 的值为:{}'.format(t.daemon))
t.start()
foriinrange(3):
time.sleep(1)
my_thread_name=threading.current_thread().name
print('{}已运行 {} 秒......'.format(my_thread_name,i+1))
print('主线程结束运行......')
输出结果:
主线程开始运行......
daemon 的值为: False
子线程开始运行......
MainThread 已运行 1 秒......
线程 1 已运行 1 秒......
MainThread 已运行 2 秒......
线程 1 已运行 2 秒......
MainThread 已运行 3 秒......
主线程结束运行......
线程 1 已运行 3 秒......
线程 1 已运行 4 秒......
线程 1 已运行 5 秒......
线程 1 已运行 6 秒......
子线程运行结束......
代码中, 主线程只需要运行 3 秒即可结束, 但子线程需要运行 6 秒, 从运行结果中可以看到, 主线程代码运行结束后, 子线程还可以继续运行, 这就是非守护线程的特征.
再来看看 daemon 值为 True 时:importtime
importthreading
deffun():
print('子线程开始运行......')
foriinrange(6):#运行3秒,每秒输出一次
time.sleep(1)
my_thread_name=threading.current_thread().name
print('{}已运行 {} 秒......'.format(my_thread_name,i+1))
print('子线程运行结束......')
print('主线程开始运行......')
t=threading.Thread(target=fun,name='线程 1')
t.daemon=True#设置为守护线程
print('daemon 的值为:{}'.format(t.daemon))
t.start()
foriinrange(3):
time.sleep(1)
my_thread_name=threading.current_thread().name
print('{}已运行 {} 秒......'.format(my_thread_name,i+1))
print('主线程结束运行......')
输出结果:
主线程开始运行......
daemon 的值为: True
子线程开始运行......
MainThread 已运行 1 秒......
线程 1 已运行 1 秒......
MainThread 已运行 2 秒......
线程 1 已运行 2 秒......
MainThread 已运行 3 秒......
主线程结束运行......
从运行结果中可以看出, 当 deamon 值为 True, 即设为守护线程后, 只要主线程结束了, 无论子线程代码是否结束, 都得跟着结束, 这就是守护线程的特征. 另外, 修改 deamon 的值必须在线程 start()方法调用之前, 否则会报错.
3.2 join()方法
join()方法的作用是在调用 join()方法处, 让所在线程 (主线程) 同步的等待被 join 的线程(下面的 p 线程), 只有 p 线程结束. 我们尝试在不同的位置调用 join 方法, 对比运行结果. 首先在 p 线程一开始的位置进行 join:importtime
importthreading
deffun():
print('子线程开始运行......')
foriinrange(6):#运行3秒,每秒输出一次
time.sleep(1)
my_thread_name=threading.current_thread().name
print('{}已运行 {} 秒......'.format(my_thread_name,i+1))
print('子线程运行结束......')
print('主线程开始运行......')
t=threading.Thread(target=fun,name='线程 1')
t.daemon=True#设置为守护线程
t.start()
t.join()#此处进行 join
foriinrange(3):
time.sleep(1)
my_thread_name=threading.current_thread().name
print('{}已运行 {} 秒......'.format(my_thread_name,i+1))
print('主线程结束运行......')
输出结果:
主线程开始运行......
子线程开始运行......
线程 1 已运行 1 秒......
线程 1 已运行 2 秒......
线程 1 已运行 3 秒......
线程 1 已运行 4 秒......
线程 1 已运行 5 秒......
线程 1 已运行 6 秒......
子线程运行结束......
MainThread 已运行 1 秒......
MainThread 已运行 2 秒......
MainThread 已运行 3 秒......
主线程结束运行......
可以看出, 等子线程运行完之后, 主线程才继续 join 下面的代码. 然后在主线程即将结束时进行 join:importtime
importthreading
deffun():
print('子线程开始运行......')
foriinrange(6):#运行3秒,每秒输出一次
time.sleep(1)
my_thread_name=threading.current_thread().name
print('{}已运行 {} 秒......'.format(my_thread_name,i+1))
print('子线程运行结束......')
print('主线程开始运行......')
t=threading.Thread(target=fun,name='线程 1')
t.daemon=True#设置为守护线程
t.start()
foriinrange(3):
time.sleep(1)
my_thread_name=threading.current_thread().name
print('{}已运行 {} 秒......'.format(my_thread_name,i+1))
t.join()
print('主线程结束运行......')
输出结果:
主线程开始运行......
子线程开始运行......
MainThread 已运行 1 秒......
线程 1 已运行 1 秒......
MainThread 已运行 2 秒......
线程 1 已运行 2 秒......
MainThread 已运行 3 秒......
线程 1 已运行 3 秒......
线程 1 已运行 4 秒......
线程 1 已运行 5 秒......
线程 1 已运行 6 秒......
子线程运行结束......
主线程结束运行......
上面代码中, 子线程是设置为守护线程的, 如果没有调用 join()方法主线程 3 秒结束, 子线程也会跟着结束, 但是从运行结果中我们可以看出, 主线程 3 秒后, 陷入等待, 等子线程运行完之后, 才会继续下面的代码.
4 线程间的同步机制
在默认情况在, 多个线程之间是并发执行的, 这就可能给数据代码不安全性, 例如有一个全局变量 num=10, 线程 1, 线程 2 每次读取该变量后在原有值基础上减 1. 但, 如果线程 1 读取 num 的值 (num=10) 后, 还没来得及减 1,CPU 就切换去执行线程 2, 线程 2 也去读取 num, 这时候读取到的值也还是 num=10, 然后让 num=9, 这是 CPU 有切换回线程 1, 因为线程 1 读取到的值是原来的 num=10, 所以做减 1 运算后, 也做出 num=9 的结果. 两个线程都执行了该任务, 但最后的值可不是 8. 如下代码所示:importtime
importthreading
deffun():
globalnum
temp=num
time.sleep(0.2)
temp-=1
num=temp
print('主线程开始运行......')
t_lst=[]
num=10# 全局变量
foriinrange(10):
t=threading.Thread(target=fun)
t_lst.append(t)
t.start()
[t.join()fortint_lst]
print('num 最后的值为:{}'.format(num))
print('主线程结束运行......')
输出结果:
主线程开始运行......
num 最后的值为: 9
主线程结束运行......
最后结果为 9, 不是 0. 这就造成了数据混乱. 所以, 就有了线程同步机制.
4.1 互斥锁: Lock
线程同步能够保证多个线程安全访问竞争资源, 最简单的同步机制是引入互斥锁. 互斥锁为资源设置一个状态: 锁定和非锁定. 某个线程要更改共享数据时, 先将其锁定, 此时资源的状态为 "锁定", 其他线程不能更改; 直到该线程释放资源, 将资源的状态变成 "非锁定", 其他的线程才能再次锁定该资源. 互斥锁保证了每次只有一个线程进行写入操作, 从而保证了多线程情况下数据的正确性.importtime
importthreading
deffun(lock):
lock.acquire()
globalnum
temp=num
time.sleep(0.2)
temp-=1
num=temp
lock.release()
print('主线程开始运行......')
t_lst=[]
num=10# 全局变量
lock=threading.Lock()
foriinrange(10):
t=threading.Thread(target=fun,args=(lock,))
t_lst.append(t)
t.start()
[t.join()fortint_lst]
print('num 最后的值为:{}'.format(num))
print('主线程结束运行......')
输出结果:
主线程开始运行......
num 最后的值为: 0
主线程结束运行......
可以看到, 最后输出结果为 0, 值正确. 当然, 如果你运行了上述两块代码, 你就会发现, 使用了锁之后, 代码运行速度明显降低, 这是因为线程由原来的并发执行变成了串行, 不过数据安全性得到保证.
使用 Lock 的时候必须注意是否会陷入死锁, 所谓死锁是指两个或两个以上的进程或线程在执行过程中, 因争夺资源而造成的一种互相等待的现象, 若无外力作用, 它们都将无法推进下去. 此时称系统处于死锁状态或系统产生了死锁, 这些永远在互相等待的进程称为死锁进程. 关于死锁一个著名的模型是 "科学家吃面" 模型:importtime
fromthreadingimportThread
fromthreadingimportLock
defeatNoodles_1(noodle_lock,fork_lock,scientist):
noodle_lock.acquire()
print('{} 拿到了面'.format(scientist))
fork_lock.acquire()
print('{} 拿到了叉子'.format(scientist))
time.sleep(1)
print('{} 吃到了面'.format(scientist))
fork_lock.release()
noodle_lock.release()
print('{} 放下了面'.format(scientist))
print('{} 放下了叉子'.format(scientist))
defeatNoodles_2(noodle_lock,fork_lock,scientist):
fork_lock.acquire()
print('{} 拿到了叉子'.format(scientist))
noodle_lock.acquire()
print('{} 拿到了面'.format(scientist))
print('{} 吃到了面'.format(scientist))
noodle_lock.release()
print('{} 放下了面'.format(scientist))
fork_lock.release()
print('{} 放下了叉子'.format(scientist))
scientist_list1=['霍金','居里夫人']
scientist_list2=['爱因斯坦','富兰克林']
noodle_lock=Lock()
fork_lock=Lock()
foriinscientist_list1:
t=Thread(target=eatNoodles_1,args=(noodle_lock,fork_lock,i))
t.start()
foriinscientist_list2:
t=Thread(target=eatNoodles_2,args=(noodle_lock,fork_lock,i))
t.start()
输出结果:
霍金 拿到了面
霍金 拿到了叉子
霍金 吃到了面
霍金 放下了面
霍金 放下了叉子
爱因斯坦 拿到了叉子
居里夫人 拿到了面
霍金吃完后, 爱因斯坦拿到了叉子, 把叉子锁住了; 居里夫人拿到了面, 把面锁住了. 爱因斯坦就想: 居里夫人不给我面, 我就吃不了面, 所以我不给叉子. 居里夫人就想: 爱因斯坦不给我叉子我也吃不了面, 我就不给叉子. 所以就陷入了死循环.
为了解决 Lock 死锁的情况, 就有了递归锁: RLock.
4.2 递归锁: RLock
所谓的递归锁也被称为 "锁中锁", 指一个线程可以多次申请同一把锁, 但是不会造成死锁, 这就可以用来解决上面的死锁问题.importtime
fromthreadingimportThread
fromthreadingimportRLock
defeatNoodles_1(noodle_lock,fork_lock,scientist):
noodle_lock.acquire()
print('{} 拿到了面'.format(scientist))
fork_lock.acquire()
print('{} 拿到了叉子'.format(scientist))
time.sleep(1)
print('{} 吃到了面'.format(scientist))
fork_lock.release()
noodle_lock.release()
print('{} 放下了面'.format(scientist))
print('{} 放下了叉子'.format(scientist))
defeatNoodles_2(noodle_lock,fork_lock,scientist):
fork_lock.acquire()
print('{} 拿到了叉子'.format(scientist))
noodle_lock.acquire()
print('{} 拿到了面'.format(scientist))
print('{} 吃到了面'.format(scientist))
noodle_lock.release()
print('{} 放下了面'.format(scientist))
fork_lock.release()
print('{} 放下了叉子'.format(scientist))
scientist_list1=['霍金','居里夫人']
scientist_list2=['爱因斯坦','富兰克林']
noodle_lock=fork_lock=RLock()
foriinscientist_list1:
t=Thread(target=eatNoodles_1,args=(noodle_lock,fork_lock,i))
t.start()
foriinscientist_list2:
t=Thread(target=eatNoodles_2,args=(noodle_lock,fork_lock,i))
t.start()
上面代码可以正常运行到所有科学家吃完面条.
RLock 内部维护着一个 Lock 和一个 counter 变量, counter 记录了 acquire 的次数, 从而使得资源可以被多次 require. 直到一个线程所有的 acquire 都被 release, 其他的线程才能获得资源. 上面的例子如果使用 RLock 代替 Lock, 则不会发生死锁, 二者的区别是: 递归锁可以连续 acquire 多次, 而互斥锁只能 acquire 一次
4.3 Condition
Condition 可以认为是一把比 Lock 和 RLOK 更加高级的锁, 其在内部维护一个琐对象(默认是 RLock), 可以在创建 Condigtion 对象的时候把琐对象作为参数传入. Condition 也提供了 acquire, release 方法, 其含义与琐的 acquire, release 方法一致, 其实它只是简单的调用内部琐对象的对应的方法而已. Condition 内部常用方法如下:
1)acquire(): 上线程锁
2)release(): 释放锁
3)wait(timeout): 线程挂起, 直到收到一个 notify 通知或者超时 (可选的, 浮点数, 单位是秒 s) 才会被唤醒继续运行. wait()必须在已获得 Lock 前提下才能调用, 否则会触发 RuntimeError.
4)notify(n=1): 通知其他线程, 那些挂起的线程接到这个通知之后会开始运行, 默认是通知一个正等待该 condition 的线程, 最多则唤醒 n 个等待的线程. notify()必须在已获得 Lock 前提下才能调用, 否则会触发 RuntimeError.notify()不会主动释放 Lock.
5)notifyAll(): 如果 wait 状态线程比较多, notifyAll 的作用就是通知所有线程
需要注意的是, notify()方法, notifyAll()方法只有在占用琐 (acquire) 之后才能调用, 否则将会产生 RuntimeError 异常.
用 Condition 来实现生产者消费者模型:importthreading
importtime
# 生产者
defproduce(con):
# 锁定线程
globalnum
con.acquire()
print("工厂开始生产......")
whileTrue:
num+=1
print("已生产商品数量:{}".format(num))
time.sleep(1)
ifnum>=5:
print("商品数量达到 5 件, 仓库饱满, 停止生产......")
con.notify()# 唤醒消费者
con.wait()#生产者自身陷入沉睡
# 释放锁
con.release()
# 消费者
defconsumer(con):
con.acquire()
globalnum
print("消费者开始消费......")
whileTrue:
num-=1
print("剩余商品数量:{}".format(num))
time.sleep(2)
ifnum<=0:
print("库存为 0, 通知工厂开始生产......")
con.notify()# 唤醒生产者线程
con.wait()# 消费者自身陷入沉睡
con.release()
con=threading.Condition()
num=0
p=threading.Thread(target=produce,args=(con,))
c=threading.Thread(target=consumer,args=(con,))
p.start()
c.start()
输出结果:
工厂开始生产......
已生产商品数量: 1
已生产商品数量: 2
已生产商品数量: 3
已生产商品数量: 4
已生产商品数量: 5
商品数量达到 5 件, 仓库饱满, 停止生产......
消费者开始消费......
剩余商品数量: 4
剩余商品数量: 3
剩余商品数量: 2
剩余商品数量: 1
剩余商品数量: 0
库存为 0, 通知工厂开始生产......
已生产商品数量: 1
已生产商品数量: 2
已生产商品数量: 3
已生产商品数量: 4
4.4 信号量: Semaphore
锁同时只允许一个线程更改数据, 而信号量是同时允许一定数量的进程更改数据 . 继续用上篇博文中用的的吃饭例子, 加入有一下应用场景: 有 10 个人吃饭, 但只有一张餐桌, 只允许做 3 个人, 没上桌的人不允许吃饭, 已上桌吃完饭离座之后, 下面的人才能抢占桌子继续吃饭, 如果不用信号量, 肯定是 10 人一窝蜂一起吃饭:fromthreadingimportThread
importtime
importrandom
deffun(i):
print('{}号顾客上座, 开始吃饭'.format(i))
time.sleep(random.random())
print('{}号顾客吃完饭了, 离座'.format(i))
if__name__=='__main__':
foriinrange(20):
p=Thread(target=fun,args=(i,))
p.start()
输出结果:
0 号顾客上座, 开始吃饭
1 号顾客上座, 开始吃饭
2 号顾客上座, 开始吃饭
3 号顾客上座, 开始吃饭
4 号顾客上座, 开始吃饭
5 号顾客上座, 开始吃饭
6 号顾客上座, 开始吃饭
7 号顾客上座, 开始吃饭
8 号顾客上座, 开始吃饭
9 号顾客上座, 开始吃饭
3 号顾客吃完饭了, 离座
4 号顾客吃完饭了, 离座
2 号顾客吃完饭了, 离座
0 号顾客吃完饭了, 离座
8 号顾客吃完饭了, 离座
5 号顾客吃完饭了, 离座
1 号顾客吃完饭了, 离座
6 号顾客吃完饭了, 离座
9 号顾客吃完饭了, 离座
7 号顾客吃完饭了, 离座
使用信号量之后:fromthreadingimportThread
importtime
importrandom
fromthreadingimportSemaphore
deffun(i,sem):
sem.acquire()
print('{}号顾客上座, 开始吃饭'.format(i))
time.sleep(random.random())
print('{}号顾客吃完饭了, 离座'.format(i))
sem.release()
if__name__=='__main__':
sem=Semaphore(3)
foriinrange(10):
p=Thread(target=fun,args=(i,sem))
p.start()
输出结果:
0 号顾客上座, 开始吃饭
1 号顾客上座, 开始吃饭
2 号顾客上座, 开始吃饭
2 号顾客吃完饭了, 离座
3 号顾客上座, 开始吃饭
0 号顾客吃完饭了, 离座
4 号顾客上座, 开始吃饭
1 号顾客吃完饭了, 离座
5 号顾客上座, 开始吃饭
3 号顾客吃完饭了, 离座
6 号顾客上座, 开始吃饭
5 号顾客吃完饭了, 离座
7 号顾客上座, 开始吃饭
4 号顾客吃完饭了, 离座
8 号顾客上座, 开始吃饭
8 号顾客吃完饭了, 离座
9 号顾客上座, 开始吃饭
9 号顾客吃完饭了, 离座
6 号顾客吃完饭了, 离座
7 号顾客吃完饭了, 离座
4.5 事件: Event
如果程序中的其他线程需要通过判断某个线程的状态来确定自己下一步的操作, 这时候就可以用 threading 为我们提供的 Event 对象, Event 对象主要有一下几个方法:
isSet(): 返回 event 的状态值;
wait(): 如果 isSet()==False 将阻塞线程;
set(): 设置 event 的状态值为 True, 所有阻塞池的线程激活进入就绪状态, 等待操作系统调度;
clear(): 恢复 event 的状态值为 False.
有如下需求: 获取当前时间的秒数的个位数, 如果小于 5, 设置子线程阻塞, 如果大于 5 则设置子进程非阻塞. 代码如下:fromthreadingimportEvent,Thread
importtime
fromdatetimeimportdatetime
deffunc(e):
print('子线程: 开始运行......')
whileTrue:
print('子线程: 现在事件秒数是{}'.format(datetime.now().second))
e.wait()# 阻塞等待信号 这里插入了一个 Flag 默认为 False
time.sleep(1)
e=Event()
p=Thread(target=func,args=(e,))
p.daemon=True
p.start()
foriinrange(10):
s=int(str(datetime.now().second)[-1])#获取当前秒数的个位数
ifs<5:
print('子线程线入阻塞状态')
e.clear()# 使插入的 flag 为 False 线程线入阻塞状态
else:
print('子线程取消阻塞状态')
e.set()# 线程线入非阻塞状态
time.sleep(1)
e.set()
print("主线程运行结束......")
输出结果:
子线程: 开始运行......
子线程: 现在事件秒数是 43
子线程线入阻塞状态
子线程线入阻塞状态
子线程取消阻塞状态
子线程取消阻塞状态
子线程: 现在事件秒数是 46
子线程取消阻塞状态
子线程: 现在事件秒数是 47
子线程取消阻塞状态
子线程: 现在事件秒数是 48
子线程取消阻塞状态
子线程: 现在事件秒数是 49
子线程线入阻塞状态
子线程: 现在事件秒数是 50
子线程线入阻塞状态
子线程线入阻塞状态
主线程运行结束......
4.6 定时器: Timer
如果想要实现每隔一段时间就调用一个函数的话, 就要在 Timer 调用的函数中, 再次设置 Timer.Timer 是 Thread 类的一个子类.
如果是多长时间后只执行一次:importthreading
importtime
defsayTime(name):
print('你好,{}为您报时, 现在时间是:{}'.format(name,time.ctime()))
if__name__=="__main__":
timer=threading.Timer(2.0,sayTime,["Jane"])
timer.start()
输出结果:
你好, Jane 为您报时, 现在时间是: Thu Dec 6 15:03:41 2018
如果要每个多长时间执行一次:importthreading
importtime
defsayTime(name):
print('你好,{}为您报时, 现在时间是:{}'.format(name,time.ctime()))
globaltimer
timer=threading.Timer(3.0,sayTime,[name])
timer.start()
if__name__=="__main__":
timer=threading.Timer(2.0,sayTime,["Jane"])
timer.start()
输出结果:
你好, Jane 为您报时, 现在时间是: Thu Dec 6 15:04:30 2018
你好, Jane 为您报时, 现在时间是: Thu Dec 6 15:04:33 2018
你好, Jane 为您报时, 现在时间是: Thu Dec 6 15:04:36 2018
你好, Jane 为您报时, 现在时间是: Thu Dec 6 15:04:39 2018
......
5 进程间的通行
5.1 队列: Queue
python 中 Queue 模块提供了队列都实现了锁原语, 是线程安全的, 能够在多线程中直接使用. Queue 中的队列包括以下三种:
1)FIFO(先进先出)队列, 第一加入队列的任务, 被第一个取出;
2)LIFO(后进先出)队列, 最后加入队列的任务, 被第一个取出;
3)PriorityQueue(优先级)队列, 保持队列数据有序, 最小值被先取出.
Queue 模块中的常用方法如下:
qsize() 返回队列的规模
empty() 如果队列为空, 返回 True, 否则 False
full() 如果队列满了, 返回 True, 否则 False
get([block[, timeout]])获取队列, timeout 等待时间
get_nowait() 相当 get(False)
put(item) 写入队列, timeout 等待时间, 如果队列已满再调用该方法会阻塞线程
put_nowait(item) 相当 put(item, False)
task_done() 在完成一项工作之后, task_done()函数向任务已经完成的队列发送一个信号
join() 实际上意味着等到队列为空, 再执行别的操作.importqueue
importthreading
deffun():
whileTrue:
try:
data=q.get(block=True,timeout=1)#不设置阻塞的话会一直去尝试获取资源
exceptqueue.Empty:
print('{}结束......'.format(threading.current_thread().name))
break
print('{}取得数据:{}'.format(threading.current_thread().name,data))
q.task_done()
print('{}结束......'.format(threading.current_thread().name))
print("主线程开始运行......")
q=queue.Queue(5)
# 往队列里面放 5 个数
foriinrange(5):
q.put(i)
foriinrange(0,3):
t=threading.Thread(target=fun,name='线程'+str(i))
t.start()
q.join()#等待所有的队列资源都用完
print("主线程结束运行......")
输出结果:
主线程开始运行......
线程 0 取得数据: 0
线程 0 结束......
线程 0 取得数据: 1
线程 0 结束......
线程 1 取得数据: 2
线程 0 取得数据: 3
线程 0 结束......
线程 0 取得数据: 4
线程 0 结束......
线程 1 结束......
主线程结束运行......
线程 1 结束......
线程 2 结束......
线程 0 结束......
6 线程池
在我们上面执行多个任务时, 使用的线程方案都是 "即时创建, 即时销毁" 的策略. 尽管与创建进程相比, 创建线程的时间已经大大的缩短, 但是如果提交给线程的任务是执行时间较短, 而且执行次数极其频繁, 那么服务器将处于不停的创建线程, 销毁线程的状态. 一个线程的运行时间可以分为 3 部分: 线程的启动时间, 线程体的运行时间和线程的销毁时间. 在多线程处理的情景中, 如果线程不能被重用, 就意味着每次创建都需要经过启动, 销毁和运行 3 个过程. 这必然会增加系统相应的时间, 降低了效率. 所以就有了线程池的诞生,
由于线程预先被创建并放入线程池中, 同时处理完当前任务之后并不销毁而是被安排处理下一个任务, 因此能够避免多次创建线程, 从而节省线程创建和销毁的开销, 能带来更好的性能和系统稳定性.fromconcurrent.futuresimportThreadPoolExecutor
importtime
deffunc(n):
time.sleep(2)
print(n)
returnn*n
t=ThreadPoolExecutor(max_workers=5)# 最好不要超过 CPU 核数的 5 倍
t_lst=[]
foriinrange(20):
r=t.submit(func,1)#执行任务,传递参数
t_lst.append(r.result())#获取任务返回结果
t.shutdown()#相当于close()+join()
print(t_lst)
print('主线程运行结束......')
7 总结
关于 Python 并发编程中的多线程部分就介绍完了, 其中诸多描述略带仓储, 后续博文中再来补充.
参考资料:https://www.cnblogs.com/linshuhui/p/9704128.html
https://www.cnblogs.com/yoyoketang/p/8337118.html
https://www.cnblogs.com/chengd/articles/7770898.html
来源: https://www.cnblogs.com/chenhuabin/p/10082249.html