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()

 

 
posted @ 2020-09-04 11:49  Janus_Blog  阅读(156)  评论(0)    收藏  举报