37 并发编程4 协程

并发编程

1 死锁

操作锁,抢锁之后需要释放锁,可能会产生死锁问题,导致程序卡住。

from threading import Thread, Lock
import time

# 使用互斥锁
mutexA = Lock()
mutexB = Lock()

class CustomThread(Thread):
    def run(self):
        self.func1()
        self.func2()

    def func1(self):
        mutexA.acquire()
        print('{name} 抢到锁A。'.format(name=self.name))

        mutexB.acquire()
        print('{name} 抢到锁B。'.format(name=self.name))

        mutexB.release()
        print('{name} 释放锁B。'.format(name=self.name))

        mutexA.release()
        print('{name} 释放锁A。'.format(name=self.name))
        
    def func2(self):
        mutexB.acquire()
        print('{name} 抢到锁B。'.format(name=self.name))
        time.sleep(2)

        mutexA.acquire()
        print('{name} 抢到锁A。'.format(name=self.name))

        mutexA.release()
        print('{name} 释放锁A。'.format(name=self.name))

        mutexB.release()
        print('{name} 释放锁B。'.format(name=self.name))

if __name__ == '__main__':
    for i in range(5):
        t = CustomThread()
        t.start()
'''
Thread-1 抢到锁A。
Thread-1 抢到锁B。
Thread-1 释放锁B。
Thread-1 释放锁A。
Thread-1 抢到锁B。
Thread-2 抢到锁A。
'''

首先线程1顺利执行func1,之后执行func2抢到锁B,继续执行需要锁A;
线程2通过func1抢到锁A,继续执行需要锁B;
此时两个线程分别拥有锁A和锁B,但需要对方的锁才能继续执行,因此都在等待对方释放锁,
因此程序处于阻塞状态。

2 递归锁

互斥锁抢到锁后在释放这个锁之前,无法再次抢这个锁。
递归锁(Reentrant Lock)
第一个抢到递归锁的线程/进程可以进行连续地抢锁(acquire)和释放锁(release),但是抢到递归锁的进程/线程完全释放这把锁之前,其它进程/线程无法抢到这把锁。
递归锁内部存在一个计数器,抢到锁后计数器加1,释放锁后计数器减1,只要计数器不为0,其它进程/线程都无法抢到这把锁。

将上面例子中的互斥锁换成递归锁,解决了死锁问题。

from threading import Thread, RLock
import time

# 使用递归锁
mutexA = mutexB = RLock()

class CustomThread(Thread):
    def run(self):
        self.func1()
        self.func2()

    def func1(self):
        mutexA.acquire()
        print('{name} 抢到锁A。'.format(name=self.name))

        mutexB.acquire()
        print('{name} 抢到锁B。'.format(name=self.name))

        mutexB.release()
        print('{name} 释放锁B。'.format(name=self.name))

        mutexA.release()
        print('{name} 释放锁A。'.format(name=self.name))

    def func2(self):
        mutexB.acquire()
        print('{name} 抢到锁B。'.format(name=self.name))
        time.sleep(2)

        mutexA.acquire()
        print('{name} 抢到锁A。'.format(name=self.name))

        mutexA.release()
        print('{name} 释放锁A。'.format(name=self.name))

        mutexB.release()
        print('{name} 释放锁B。'.format(name=self.name))

if __name__ == '__main__':
    for i in range(5):
        t = CustomThread()
        t.start()

3 信号量

信号量在不同的领域/阶段表示不同的技术点。
在并发编程中,信号量指的是锁。

from threading import Thread, Semaphore
import time
import random

sm = Semaphore(5)  # 参数代表锁的数量

def task(name):
    sm.acquire()
    print('{name} 抢到锁。'.format(name=name))
    time.sleep(random.randint(1, 3))
    sm.release()
    print('{name} 释放锁。'.format(name=name))

if __name__ == '__main__':
    for i in range(20):
        t = Thread(target=task, args=('任务{i}'.format(i=i),))
        t.start()

4 Event事件

一些进程/线程必须等待其它进程/线程发出信号后才能开始运行。

from threading import Event
e = Event()
e.set()
e.wait()
from threading import Thread, Event
import time

e = Event()  # 事件对象

def get_signals():
    print('红色')
    time.sleep(1)
    print('绿色')
    e.set()  # 发送消息

def run(name):
    print('{name}停止。'.format(name=name))
    e.wait()  # 等待消息
    print('{name}开始。'.format(name=name))

if __name__ == '__main__':
    t = Thread(target=get_signals)
    t.start()
    for i in range(10):
        t = Thread(target=run, args=('任务{i}'.format(i=i),))
        t.start()

5 线程q

同一个进程下的多个线程数据共享。
多个线程操作数据可能导致数据不安全,使用队列是因为 队列 = 管道 + 锁,使用队列是为了保证数据安全。

  1. Queue
    先进先出Queue
import queue

q = queue.Queue(3)
q.put(1)
q.get()
q.get_nowait()
q.get(timeout=3)
q.full()
q.empty()
  1. LifoQueue
    后进先出Queue (Last In First Out)
import queue

q = queue.LifoQueue(3)
  1. PriorityQueue
    优先级Queue
    优先级数字越小,优先级越高。
import queue

q = queue.PriorityQueue(3)
q.put((2, '2'))  # 参数为元组(priority number, value)
q.put((1, '1'))
q.put((3, '3'))

# 优先级数字越小,越先被取出。
print(q.get())  # (1, '1')
print(q.get())  # (2, '2')
print(q.get())  # (3, '3')

6 进程池与线程池

6.1 什么是池?

池是在保证计算机硬件安全的情况下最大限度地利用计算机,它降低了程序的运行效率,但能保证计算机硬件的安全,防止压力过大造成系统崩溃,从而保证程序的正常运行。

6.2 线程池
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
import time


# 参数为线程池容量,默认为计算机CPU核数的5倍
# 参数设置为5,说明线程池创建完毕后,内部固定存在5个线程,而且这5个线程不会出现重复创建和销毁。
# 这样做能够减少再次创建/销毁线程产生的系统开销。
pool = ThreadPoolExecutor(5)


def task(n):
    print(n)
    time.sleep(2)
    return n ** n


pool.submit(task, 0)  
# 向线程池中提交任务。
# 返回值是Future类的对象。

# 任务的提交方式:同步和异步
# 同步:任务提交后等待任务的返回结果,等待期间不处理任何事;
# 异步:任务提交后不等待任务的返回结果,直接处理其它任务,返回结果由异步回调机制处理。
# 线程池的任务提交方式属于异步提交。

t_list = []
for i in range(20):
    t = pool.submit(task, i)
    print(t)  # Future类的对象

    # 调用对象的result()方法,得到的是任务的结果。
    # result()使程序的运行由并发变成串行,提交方式变为同步提交
    # print(res.result())  
    t_list.append(t)

# 关闭线程池,等待线程池中所有任务运行完毕。
pool.shutdown()


# 保证异步的提交方式
for each_t in t_list:
    print(each_t.result())
6.3 进程池
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
import time
import os

# 参数为进程池容量,默认为计算机CPU核数
pool = ProcessPoolExecutor()


def task(n):
    print(n, os.getpid())
    time.sleep(2)
    return n ** n


def call_back(n):
    print('callback: {n}'.format(n=n))
    print(n.result())


if __name__ == '__main__':
    p_list = []
    for i in range(20):
        # 通过异步回调机制处理异步提交产生的结果
        pool.submit(task, i).add_done_callback(call_back)
6.4 总结
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor

# 1 生成线程/进程池对象
thread_pool = ThreadPoolExecutor()  # 线程池容量默认是CPU核数的5倍
process_pool = ProcessPoolExecutor()  # 进程池容量默认是CPU核数

# 2 提交任务,返回Future类的对象
# 调用对象的add_done_callback方法进行异步回调处理。
# 异步回调函数call_back会在任务函数task执行完毕后自动触发执行,对任务函数task的结果进程处理。
pool_obj.submit(task, i).add_done_callback(call_back)

7 协程

7.1 介绍

进程:资源单位
线程:执行单位

协程是为了在单线程下实现并发的效果
协程需要做到:切换 + 保存状态

通过程序代码监听所有的IO操作,出现IO操作时通过程序代码进行切换,使得CPU觉得这样的程序没有IO操作,一直在运行,从而最大化地利用CPU等系统资源,提升程序的运行效率。
例如TCP的服务端,accept()和recv()都属于IO操作,通过程序代码实现accept()和recv()之间的快速切换。

但是不断执行 切换 + 保存状态 操作也可能会降低程序的执行效率。
对于计算密集型,会降低效率;
对于IO密集型,可以提升效率。

7.2 gevent模块
import time
# from gevent import spawn

# gevent本身无法监听常见的io操作,需要额外导入猴子补丁
# from gevent import monkey
# monkey.patch_all()
# 简写
from gevent import monkey; monkey.patch_all()
from gevent import spawn


def func1():
    print(11)
    time.sleep(2)
    print(12)


def func2():
    print(21)
    time.sleep(3)
    print(22)


start_time = time.time()

# 监听,异步提交
g1 = spawn(func1)
g2 = spawn(func2)

# 等待被监测的任务执行完毕后再继续执行
g1.join()
g2.join()

print(time.time() - start_time)
'''
11
21
12
22
3.01
'''
7.3 TCP服务端(基于协程)

使用协程实现TCP服务端的并发,accept()和recv()都属于IO操作,通过程序代码在accept()和recv()之间实现不断切换。

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


def communicate(conn):
    while True:
        try:
            data = conn.recv(1024)
            if len(data) == 0:
                break
            conn.send(data.upper())
        except ConnectionResetError as e:
            print(e)
            break
    conn.close()


def server(ip_addr, port_num):
    server = socket.socket()
    server.bind((ip_addr, port_num))
    server.listen(5)
    while True:
        conn, addr = server.accept()
        spawn(communicate, conn)


if __name__ == '__main__':
    g1 = spawn(server, '127.0.0.1', 8080)
    g1.join()
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值