python3-并发编程(下篇)

系列文章目录

一、死锁与递归锁

  • 死锁的概念:

    死锁是一种现象,会使整个程序永远阻塞下去。

    举个例子:当你和你的基友(多个进程)去吃饭,结果老板只给了一双筷子(有限的临界资源),你俩手疾眼快,各自抢到了一根(加锁)。但是,一根筷子没法用来吃饭,而你和你的基友都在等待对方将手里的筷子让给自己,双方进入僵持状态(死锁),这就是一种现实生活中的死锁现象。

  • 递归锁的概念:

    递归锁(Recursive Lock)也称为可重入互斥锁(reentrant mutex),是互斥锁的一种。

    它的特殊之处在于:同一线程对其多次加递归锁不会产生死锁。递归锁会使用引用计数机制,每次加锁时计数加一,释放锁时减一;只要计数不为0,其他线程都无法抢到该锁。

  • 递归锁的使用:

    multiprocessingthreading模块都支持递归锁RLock

    from threading import Thread, Lock, RLock
    import time
    
    
    mutexA = mutexB = RLock()
    
    
    class MyThead(Thread):
        def run(self):
            self.func1()
            self.func2()
    
        def func1(self):
            mutexA.acquire()
            print('%s 抢到A锁'% self.name)  # 获取当前线程名
            mutexB.acquire()
            print('%s 抢到B锁'% self.name)
            mutexB.release()
            mutexA.release()
    
        def func2(self):
            mutexB.acquire()
            print('%s 抢到B锁'% self.name)
            time.sleep(2)
            mutexA.acquire()
            print('%s 抢到A锁'% self.name)  # 获取当前线程名
            mutexA.release()
            mutexB.release()
    
    
    if __name__ == '__main__':
        for i in range(10):
            t = MyThead()
            t.start()
            
    

二、信号量(semaphore)

信号量是一个内置的计数器,可以用来控制并行的线程数/进程数,超出的线程/进程阻塞,直到有线程/进程运行完成,才运行下一个线程/进程。有点像厕所里的坑位。

import time
from threading import Thread, Semaphore
 
# 信号量设置为5,则同时最多有5个线程运行
s1 = Semaphore(5)

def foo():
    s1.acquire()
    time.sleep(2)   #程序休息2秒
    print("ok",time.ctime())
    s1.release()
 
for i in range(20):
    t1 = Thread(target=foo,args=()) #实例化一个线程
    t1.start()  #启动线程
    t1.join()
 
foo()

三、Event事件

Event()类的实例对象用来在某个事件发生后,触发另一个事件。这个类在multiprocessingthreading模块中都有实现。

from threading import Thread, Event
import time

event = Event()

def li_si():
    """等待张三离开的李四"""
    print('李四和张三说话')
    time.sleep(5)
    print('张三走了')  # 事件发生
    
    # 张三走了,给老王发消息
    event.set()

def lao_wang():
    """躲在床下的老王"""
    print('老王躲在床底下')
    
    # 等待李四给他发消息(阻塞,直到event.set被执行)
    event.wait()
    
    print('老王从床下爬出,和李四偷喝张三家的酒')  # 触发另一事件
    

if __name__=='__main__':
    t1 = Thread(target=li_si)
    t2 = Thread(target=lao_wang)
    
    t1.start()
    t2.start() 

四、进程池与线程池

理论上,进程/线程都是可以无限新建的,但因为计算机硬件支持的进程数/线程数是有限的,所以我们需要对进程/线程的数量进行限制。而最常用的解决方案就是进程池/线程池。

  • 池的概念:

    池(pool)是一种技术。它虽然会限制程序的运行效率,但能够保护计算机系统支持运行。上面对并发的限制是池技术应用的一个方面,其他的池有:连接池、半连接池、内存池、对象池等。

1. 进程池的使用

进程池内的进程是固定的,一旦创建就不会销毁,会一直重复使用。即,一个已有进程不会因为自己的任务结束而销毁,解释器会给它重新分派任务去执行。

from concurrent.futures import ProcessPoolExecutor

# 创建进程池,括号内为进程数,默认为cpu的内核数
pool = ProcessPoolExecutor()


def foo():
    """任务"""
    return 100


# l用来保存submit的返回值,而submit用来向进程池提交任务
l = []

# windows下需要在main中创建进程
if __name__ == '__main__':
	# 向进程池提交20个任务
    for i in range(20):
        res = pool.submit(foo)  # 异步提交,主进程不会等待子线程完成

        # 因为获取任务返回值的操作,会使for循环阻塞,变成同步串行执行
        # 所以先将submit返回的结果对象保存,之后再获取任务的返回值
        l.append(res)
  
    # 关闭进程池,会等待进程池中所有进程将任务执行完
    pool.shutdown()

    # 获取任务的返回值
    for res in l:
        # 对submint返回的结果对象,调用result方法获取任务返回值
        print(res.result())

2. 线程池的使用

线程池的特性和用法几乎与进程池一样,线程池内的线程也是固定的,也会被重复使用。

上面使用列表获取任务返回值的方法不太合理,实际上,进程池和线程池都有个回调方法,可以更加方便的,异步获取任务返回值:

from concurrent.futures import ThreadPoolExecutor

# 创建线程池,括号内为线程数,默认为cpu内核数的5倍
pool = ThreadPoolExecutor()


def foo():
    """任务"""
    return 100

def call_back(n):  # 接收的参数为submit返回的对象
    """回调方法"""
    # 获取任务返回值
    n.result()
    

for i in range(20):
    # 添加回调方法
    res = pool.submit(foo).add_done_callback(call_back)
  
# 关闭线程池
pool.shutdown()

当一个任务完成时,submit方法就会自动调用call_back方法,并将返回值传递给它。

五、协程(Coroutine)和gevent模块

1. 协程的概念和实现

  • 协程的概念:

    协程不同于进程和线程,它是应用程序员通过代码实现的,而后面二者则是操作系统提供的系统调用。

    协程运行在线程之上,是轻量级的线程。它可以随意切换任务,比如:当某个函数阻塞时,可以转而去执行另一个函数,等执行完或阻塞后,又转回原来的函数继续执行。这样,可以提高单线程下cpu的利用率。

  • 实现协程:

    实现协程最重要的有两点:切换和保存现场,因此可以通过yield关键字来实现。

    下面的例子中,foobar函数会来回切换执行:

    import time
    
    def foo():
        while True:
            1 + 1
            yield  # 会保存现场,去执行bar函数
            
    def bar():
        # 初始化生成器
        g = foo()
        for i in range(100000):
            i + 1
            
            next(g)  # 会切换到foo函数
            
    start_time = time.time()
    bar()
    print(time.time() - start_time)
    

2. gevent模块的基本使用:

通常我们只会在遇到阻塞时,主动切换正在执行的任务,而gevent模块,可以帮助我们,监测阻塞。

该模块是第三方模块,需要手动安装:pip install gevent

from gevent import spawn, monkey
import time

# 猴子补丁,打了这个补丁,gevent模块才会生效
monkey.patch_all()

def foo():
    """任务1"""
    print('任务1开始')
    time.sleep(2)
    print('任务1结束')

def bar():
    """任务2"""
    print('任务2开始')
    time.sleep(3)
    print('任务2结束')

start_time = time.time()

# 监测阻塞,传入要监测的函数
# 这两个函数,一个阻塞,协程就回去执行另一个
f = spawn(foo)
b = spawn(bar)

# 等待被检测的任务执行完毕,再去执行后续代码
f.join()
b.join()

# 打印完成两个任务花费的时长
print(time.time()-start_time)
# 打印:3.012303590774536

使用gevent,可以获得极高的并发性能,但gevent只能在Unix/Linux下运行,在Windows下不保证正常安装和运行。

下一篇

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

花_城

你的鼓励就是我最大的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值