python多线程
# 并发与并行
并发和并行是两个非常容易混淆的概念。他们都可以表示两个或者多个任务一起执行,但是偏重点不同。并发偏重于多个任务交替执行,而多个任务之间有可能还是串行的。并发是逻辑上的同时发生,而并行是物理上的同时发生。然而并行的偏重点在于“同时执行”。
并发:指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行。这就好比一个人一把铁锹同时挖三个坑,每个坑都挖一下然后去挖下一个,虽然三个坑都在变大,但实际上同一时间只有一个坑在被挖。
严格意义上讲,并行的多个任务是真实的同时进行,而对于并发来讲,这个过程只是交替的,一会运行任务一,一会运行任务二,系统会不停的在两者之间来回切换。但对于外部观察者来说,即使多个任务是串行并发的,也会造成多个任务并行执行的错觉。
并行:指在同一时刻,有多条指令在多个处理器上同时执行。就好像三个人三把铁锹同时在挖三个坑,三个坑在一起变大。所以无论是从微观还是宏观上,二者都是一起执行的。
# 线程与进程
开个QQ,开了一个进程;开了一个迅雷,开了一个进程
在QQ这个进程里,传输文字开一个线程、传输语音开一个线程、弹出对话框又开了一个线程
所以运行某个软件,相当于开了一个进程。在这个软件运行的过程里(在这个进程里),多个工作同时运转,完成QQ的运行,那么这“多个工作”分别对应一个线程,所以一个进程管着多个线程,一个进程有且至少有一个线程
# 线程的创建与调用 import threading import time def say_hi(num): # 定义每个线程运行的函数 print("running on number:%s" % num) time.sleep(1) if __name__ == "__main__": t1 = threading.Thread(target=say_hi, args=(1,)) # 生成一个线程实例 t2 = threading.Thread(target=say_hi, args=(2,)) # 生成另一个线程实例 t1.start() # 启动线程实例1 t2.start() # 启动线程实例2 print(t1.getName()) # 获取线程名 print(t2.getName())
# join&setDaemon
setDaemon(True):将线程申请为守护线程,必须在start()方法调用之前设置,如果不设置为守护线程程序会被无限挂起。这个方法基本与join相反的。当我们在程序运行中,执行一个主线程,如果主线程又创建了一个子线程,主线程与子线程就兵分两路,分别运行,那么当主线程完成想退出时,会检验子线程是否完成。如果子线程未完成,则主线程会等待子线程完成后再退出。但是有时候需要的是只要主线程完成 了,不管子线程是否完成,都要和子线程一起 退出,这时就要用setDaemon方法了。
join():在子线程完成之前,这个子线程的父线程将一直被阻塞。
import threading import time def music(): for i in range(10): print("听音乐") time.sleep(1) def video(): for i in range(3): print("看电影") time.sleep(1) t1 = threading.Thread(target=music) t2 = threading.Thread(target=video) if __name__ == "__main__": t1.setDaemon(True) t1.start() t2.start() t2.join() print("电影结束了,准备睡觉")
# 并发的优点
要把100M数据写入磁盘,CPU计算的时间只需要0.01s,可是磁盘接受这100M数据却要10s,怎么办呢?有两种方法
1.第一种办法是CPU等着,也就是程序暂停执行后续代码,知道磁盘写入数据完成再继续往下执行
import time begin_time = time.time() def foo1(_str): time.sleep(2) print(_str) # 串行 foo1('磁盘接受100M数据') foo1('CPU去做其它事') end_time = time.time() # 打印主线程运行时间 print(">>>", end_time - begin_time)
2.第二种办法是CPU告诉磁盘:“你老人家慢点写,我去干别的事了。”
import time import threading begin_time = time.time() def foo1(_str): ime.sleep(2) print(_str) # 多线程并发 t1 = threading.Thread(target=foo1, args=('磁盘接受100M数据',)) # 元组形式传参 t2 = threading.Thread(target=foo1, args=('CPU去做其他事了',)) t1.start() t2.start() t1.join() t2.join() end_time = time.time() # 主线程运行时间 print(">>>", end_time - begin_time)
从代码运行时间来看,很明显,第二种方法更节省时间。但是,上述需求有一个特点,即CPU存在等待时间,比如我们调用服务端的端口,是不是一定会有一段等待时间呢,在某个任务阻塞的时候,CPU切换到其它的任务(所以节省了时间),大大的提高了CPU的使用效率,这种任务叫做i/o密集型任务(也可理解为阻塞密集型,)那么问题是,如果是纯计算的需求 ,没有阻塞,CPU一直在运行,不存在等待的情况,多线程并发还能为我们节省时间吗?
# 并发的缺点
看以下代码,脑补下运行结果,上边(串行)和下边(并行)两个程序谁更快呢?
import time begin_time = time.time() def foo1(): _sum = 0 for _i in range(50000000): _sum += _i # 串行 foo1() foo1() end_time = time.time() # 打印主线程运行时间 print(">>>", end_time - begin_time) # >>> 7.696396589279175
import time import threading begin_time = time.time() def foo1(): _sum = 0 for _i in range(50000000): _sum += _i # 多线程并发 t1 = threading.Thread(target=foo1) t2 = threading.Thread(target=foo1) t1.start() t2.start() t1.join() t2.join() end_time = time.time() # 主线程运行时间 print(">>>", end_time - begin_time) # >>> 7.417142629623413
从结果看,差距并不大,甚至比较老的python版本,并发运行的速度反而更慢,这是为什么呢?
因为CPU一直在计算,没有过休息,这种程序叫做计算密集型任务。由于CPU一刻不停的在运算,所以每个任务占用CPU的时间是完整的,所以CPU执行全部任务的时间就等于任务A全部时间+任务B全部时间+CPU切换时间,所以才会产生上述场景。
由此我们可以得出观点:python多线程并发,可以提高i/o密集型程序的运行效率,但对于计算密集型程序,python多线程并不能显著的提升效率(并非不适用,因为现实世界鬼知道会有什么样的应用场景)
# 不安全的并发
多个线程都在同时操作同一共享资源时,所以造成了资源破坏,怎么办呢?
有人会想用join呗,但join会把整个线程给停住,造成串行,失去了多线程的意义,而我们只需要把计算(涉及到操作公共数据)的时候串行执行。
我们可以通过同步锁来解决这类问题。
# 同步锁
我们知道:银行都有保险柜 ,贵重物品比如黄金都会存在保险柜,将保险柜锁上,别人就不能轻易使用了,同样的,我们也可以将一些重要的数据操作上锁。
import threading import time import random account_balance = 500 # 银行卡账户余额 r = threading.Lock() # 一把锁 def option_num2(num): r.acquire() # 锁上 global account_balance balance = account_balance time.sleep(random.randint(0, 10) * 0.1) # 并不知道要计算多久 balance = balance + num account_balance = balance r.release() # 释放资源:开锁 t1 = threading.Thread(target=option_num2, args=(10000, )) t2 = threading.Thread(target=option_num2, args=(-300, )) t1.start() t2.start() t1.join() t2.join() print(account_balance) # 10200
如代码:需要操作账户余额的时候,我们给它上一把锁,同一时间只允许一个线程进行操作,等操作完毕再把锁打开,如此规避了数据安全问题。
# 死锁和递归锁
什么是死锁呢?在线程间共享多个资源的时候,如果两个线程分别占有一部分资源并且同时等待对方的资源,就会造成死锁,因为系统判断这部分资源都正在使用,所以这两个线程在无外力作用下将一直等待下去。
为了支持在同一线程中多次请求同一资源,python提供了“可重入锁”:threading.RLock 内部维护着一个Lock和一个counter变量,counter记录了acquire的次数,从而使得资源可以被多次acquire。直到一个线程所以的acquire都被release,其它线程才能获得资源
看如下代码,上边就是一个死锁的例子,下边就是一个递归锁的例子(递归锁即为可重入锁)
import threading import time lockA = threading.Lock() lockB = threading.Lock() def foo1(): lockA.acquire() print('A1锁') time.sleep(1) lockB.acquire() print('B1锁') lockB.release() print('B1放') lockA.release() print('A1放') def foo2(): lockB.acquire() print('B2锁') time.sleep(1) lockA.acquire() print('A2锁') lockA.release() print('A2放') lockB.release() print('B2放') t1 = threading.Thread(target=foo1) t2 = threading.Thread(target=foo2) t1.start() t2.start() t1.join() t2.join()
为了支持同一线程中多次请求同一资源,python提供了“可冲入锁”:threading.RLock。RLock内部维护着一个Lock和一个counter变量,counter记录了acquire的次数,从而使的资源可以被多次acquire。直到一个线程所以的acquire都被release,其它线程才能获得资源。
import threading import time lockR = threading.RLock() def foo1(): lockR.acquire() print('A1锁') time.sleep(1) lockR.acquire() print('B1锁') lockR.release() print('B1放') lockR.release() print('A1放') def foo2(): lockR.acquire() print('B2锁') time.sleep(1) lockR.acquire() print('A2锁') lockR.release() print('A2放') lockR.release() print('B2放') t1 = threading.Thread(target=foo1) t2 = threading.Thread(target=foo2) t1.start() t2.start() t1.join() t2.join()
# 条件变量同步
有一类线程需要满足条件过后才能继续执行,比如某几条测试用例需要在初始化过后才能继续执行;比如早点铺子的师傅必须蒸好包子,客人才能把包子买来吃掉。
python提供了threading.Condition对象用于条件变量线程的支持,它除了能提供RLock()或Lock()的方法外,还提供了wait()、notify()、notifyAll()方法
# wait(): 条件不满足时调用
# notify(): 条件创造后调用,通知等待池激活一个线程
# notifyAll(): 条件创造后调用,通知等待池激活所有线程
调用方式lock_con = threading.Condition([RLock/Lock]):蓝色部分是参数,传入Lock或RLock,参数是可选项,不传,默认RLock
import threading import random import time lock_con = threading.Condition() # 条件锁对象 num_list = [] def producer(): global num_list # 引用全局变量 while True: # 不停生产包子 if lock_con.acquire(): num_list.append(1) print('生产者:生产了一个包子', num_list) lock_con.notifyAll() lock_con.release() time.sleep(random.randint(0, 10) * 0.1) # 把包子端上桌的时间 def consumers(): global num_list # 引用全局变量 while True: # 不停的吃掉包子 if lock_con.acquire(): if len(num_list) == 0: print('没有包子了,等待生产包子。。。') lock_con.wait() # 线程释放锁进入等待,被唤起重新加锁 num_list.remove(num_list[0]) print('消费者:吃掉一个包子', num_list) time.sleep(random.randint(0, 10) * 0.3) # 大口吃掉包子花掉的时间 lock_con.notifyAll() lock_con.release() t1 = threading.Thread(target=producer) t2 = threading.Thread(target=consumers) t1.start() t2.start() t1.join() t2.join()
# 信号量
信号量是用来控制线程并发数的,BoundedSemaphore或Semaphore管理一个内置的计数器,每当调用acquire()时 -1,调用release()时+1.
计数器不能小于0 ,当计数器为0时,acquire()将阻塞线程至同步锁定状态,直到其它线程调用release().(类似停车位的概念)
BoundedSemaphore或Semaphore的唯一区别在于前者将在调用release()时检查计数器的值是否超过了计数器的初始值,如果超过了就抛出一个异常
import threading import time semaphore = threading.BoundedSemaphore(5) # 同一时间只能有5个线程处于运行状态 def run(ii): semaphore.acquire() print('threading- ', ii) time.sleep(10) semaphore.release() for i in range(10): t = threading.Thread(target=run, args=(i, )) t.start()

浙公网安备 33010602011771号