死锁现象、信号量、event事件、池的概念、协程

死锁现象

死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。

image

在使用互斥锁时就算掌握了如何抢,如何放,也会产生死锁现象。

比如:

from threading import Thread, Lock
import time

mutexA = Lock()
mutexB = Lock()

def f1(name, mutex1, mutex2):
    mutex1.acquire()
    print(f'{name}抢到了A锁')
    mutex2.acquire()
    print(f'{name}抢到了B锁')
    mutex2.release()
    print(f'{name}释放了B锁')
    time.sleep(0.1)
    mutex1.release()
    print(f'{name}释放了A锁')

def f2(name, mutex1, mutex2):
    mutex2.acquire()
    print(f'{name}抢到了B锁')
    mutex1.acquire()
    print(f'{name}抢到了A锁')
    mutex1.release()
    print(f'{name}释放了A锁')
    mutex2.release()
    print(f'{name}释放了B锁')

if __name__ == '__main__':
    # 创建多个线程
    for i in range(5):
        t1 = Thread(target=f1, args=(f'f1线程{i}', mutexA, mutexB))
        t1.start()
    for i in range(5):
        t2 = Thread(target=f2, args=(f'f2线程{i}', mutexA, mutexB))
        t2.start()

执行结果:

f1线程0抢到了A锁
f1线程0抢到了B锁
f1线程0释放了B锁
f2线程0抢到了B锁
f1线程0释放了A锁
f1线程1抢到了A锁

程序执行到一半直接卡住了,这个就是死锁现象,在上述例子中,执行f1的线程0释放了B锁后,执行f2的线程0就抢到了B锁,释放了A锁后,另一个执行f1的线程1就会抢到了A锁,抢到了A锁后他要抢B锁,但此时的B锁已经被抢了,所以在等待,而执行f2的线程0要抢A锁,也在等待,2个线程互相等待结果就造成了死锁。

信号量(了解)

在并发编程中信号量意思是就是多把互斥锁,如果说自定义互斥锁是单间,那么信号量就是宾馆。

Semaphore()方法就是用于创建信号量的。

from threading import Thread, Semaphore
import time

sp = Semaphore(5)  # 创建5个互斥锁

def task(name):
    sp.acquire()  # 抢锁
    print('%s正在占用' % name)
    # 让执行结果更明显
    time.sleep(2)
    sp.release()  # 放锁

if __name__ == '__main__':
    for i in range(20):
        t = Thread(target=task, args=(f'线程{i}',))
        t.start()

此时的线程会5个5个的执行,因为只有有五把互斥锁可以抢。

event事件(了解)

子线程的运行可以由其他子线程决定,这个我们可以用event来实现。

from threading import Thread, Event
import time

event = Event()  # 类似于造了一个红绿灯

def light():
    print('红灯亮着的 所有人都不能动')
    time.sleep(3)
    print('绿灯亮了 油门踩到底 给我冲!!!')
    event.set()

def car(name):
    print('%s正在等红灯' % name)
    event.wait()
    print('%s加油门 飙车了' % name)

if __name__ == '__main__':
    t = Thread(target=light)
    t.start()
    for i in range(5):
        t = Thread(target=car, args=('AE86-%s' % i,))
        t.start()

event.wait()就是等待的命令,event.set()就是让event.wait()取消等待,可以执行了。

进程池与线程池(重点)

学到这里,我们应该知道我们不可能无限制的创建进程和线程,如果无限制的创建进程和线程,会让计算机超过负荷,超载,并可能对计算机硬件造成损害。

为了避免计算机超过负荷,我们可以使用池,池的作用就是在保证计算机硬件安全的情况下提升程序的运行效率,进程池就是提前创建好一定数量的进程,后续反复并且只使用这些进程;线程池就是提前创建好一定数量的线程,后续反复并且只使用这些线程;如果任务超过了池里规定的数量,就会原地等待,所以池也相应的降低了程序的运行效率。

image

线程池演示

from concurrent.futures import ThreadPoolExecutor
import time

# 创建线程池
t_pool = ThreadPoolExecutor(15)  #  提供15个线程,默认为cpu个数*5
'''上面的代码执行之后就会立刻创建15个等待工作的线程'''

def task(name):
    print(f'线程{name}正在执行...')
    time.sleep(2)

if __name__ == '__main__':
    for i in range(90):
        # 执行线程
        t_pool.submit(task, i)

进程池演示

from concurrent.futures import ProcessPoolExecutor
import time

# 创建进程池
p_pool = ProcessPoolExecutor(3)  #  提供3个进程,默认为cpu个数
'''上面的代码执行之后就会立刻创建3个等待工作的进程'''

def task(name):
    print(f'进程{name}正在执行...')
    time.sleep(2)

if __name__ == '__main__':
    for i in range(15):
        # 执行进程
        p_pool.submit(task, i)

池在使用时是由返回值的,返回的是一个对象,需要调用result()才能取出,这里拿进程池举例:

from concurrent.futures import ProcessPoolExecutor
import time

# 创建进程池
p_pool = ProcessPoolExecutor(3)  #  提供3个进程,默认为cpu个数
'''上面的代码执行之后就会立刻创建3个等待工作的进程'''

def task(name):
    print(f'进程{name}正在执行...')
    time.sleep(2)
    return '返回值:%d' % name ** 2

if __name__ == '__main__':
    for i in range(15):
        # 执行进程,并获取返回值
        res = p_pool.submit(task, i)
        print(res.result())

但这时候的程序变成同步执行了,因为执行任务时需要等待返回值,所以我们可以使用add_done_callback(func)方法,只要调用add_done_callback(func)的对象有结果了,就会把返回的对象当成参数传入函数func中。

from concurrent.futures import ProcessPoolExecutor
import time

# 创建进程池
p_pool = ProcessPoolExecutor(3)  #  提供3个进程,默认为cpu个数
'''上面的代码执行之后就会立刻创建3个等待工作的进程'''

def task(name):
    print(f'进程{name}正在执行...')
    time.sleep(2)
    return '返回值:%d' % name ** 2

# 由于不知道有几个参数,使用可变形参
def func(*args, **kwargs):
    # 从这里获取返回值
    print(args[0].result())

if __name__ == '__main__':
    for i in range(15):
        # 执行进程,添加异步回调机制
        p_pool.submit(task, i).add_done_callback(func)
        """
        只要p_pool.submit(task, i)执行完返回一个对象
        就会自动调用括号内的func函数处理
        会把p_pool.submit(task, i)的返回的对象当成参数传入
        """

协程

协程就是自己通过代码来检测程序的IO操作并自己处理,让CPU感觉不到IO的存在从而最大幅度的占用CPU,简单的来说就是一个线程实现并发的效果。

image

基本使用

from gevent import monkey;monkey.patch_all()  # 用于检测所有的IO操作
from gevent import spawn
import time

def play(name):
    print('%s play 1' % name)
    time.sleep(5)
    print('%s play 2' % name)

def eat(name):
    print('%s eat 1' % name)
    time.sleep(3)
    print('%s eat 2' % name)

start_time = time.time()
g1 = spawn(play, 'tom')  # 第一个参数为任务,后面的参数为任务需要的参数
g2 = spawn(eat, 'tony')  # 第一个参数为任务,后面的参数为任务需要的参数
g1.join()  # 等待检测任务执行完毕
g2.join()  # 等待检测任务执行完毕
print('运行时间:', time.time() - start_time)
# 输出结果:运行时间: 5.0176708698272705

主要方法:

  • monkey;monkey.patch_all():固定搭配,用于检测所有的IO操作
  • spawn():第一个参数为任务,后面的参数为任务需要的参数
  • join():等待检测任务执行完毕

基于协程实现TCP服务端并发

服务端(Server)

import socket
from gevent import monkey;monkey.patch_all()
from gevent import spawn


def task(sock):
    while True:
        msg = sock.recv(1024)
        print(msg.decode('utf8'))
        sock.send(b'from server')

def run():
    server = socket.socket()
    server.bind(('127.0.0.1', 8080))
    server.listen(5)
    while True:
        client, addr = server.accept()
        spawn(task, client)

if __name__ == '__main__':
    s = spawn(run)
    s.join()

客户端(Client)

import socket

client = socket.socket()
client.connect(('127.0.0.1', 8080))

while True:
    s = input('发送给服务端的消息:')
    send_msg = 'from client: %s' % s
    client.send(send_msg.encode('utf8'))
    msg = client.recv(1024)
    print(msg.decode('utf8'))
posted @ 2022-04-21 18:06  Yume_Minami  阅读(118)  评论(0编辑  收藏  举报