No.35池

No.35

今日概要

    • 互斥锁
    • 递归锁
  • queue 模块
    • 先进先出
    • 后进先出
    • 优先级队列
    • 进程池
    • 线程池

内容回顾

  • 进程

    • IPC机制
      • Queue
        • 生产者消费者模型
      • JoinableQueue
        • 生产者消费者模型
  • 什么是生产者消费者模型?

    • 把生产数据和处理数据的过程解耦
    • 让生产数据和处理数据达到工作效率平衡
    • 中间的容器
      • 在多进程中通过Queue或者JoinableQueue来控制数据量
        • 当数据过剩时,队列的大小会控制生产者的行为。
        • 当数据不足时,对列的大小会控制消费者的行为。
        • 我们还可以通过定期检查队列中的元素个数,来调节生产者和消费者的个数。
    • 案例
      • 爬虫
        • 假设请求网页的平均时间为0.3s,处理网页代码的平均时间为0.003s。
        • 请求网页和处理网页的时间效率差为100倍,那么可以通过生产者消费者模型把整个过程解耦。
        • 每启动100个线程生产数据,就对应启动1个线程处理数据。
      • server
        • 假设每秒有6w条请求,而一个服务每秒只能处理2k条请求。
        • 先写一个web程序,只负责接收请求然后把请求放入队列中。
        • 再写很多个server端,分别从队列中获取请求进行处理然后返回结果。
  • 线程

    • 开销小、数据共享、是进程的一部分不能独立存在。
    • GIL锁
      • 全局解释器锁
      • cpython解释器中的机制
      • 导致了在同一进程中的多个线程不能实现并行
  • threading模块

    • 创建线程的方式

      • 面向函数

        from threading import Thread
        def func(arg):
            print(arg)
            
        t = Thread(target=func, args=(666,))
        t.start
        
      • 面向对象

        from threading import Thread
        class MyThread(Thread):
            def __init__(self, arg):
            	self.arg = arg
                super().__init__()
                
            def run(self):
            	print(self.arg)
                
        t = MyThread(666)
        t.start()     
        
    • 线程中的方法:

      • 线程对象
        • t.start()
        • t.join()
      • 守护线程
        • t.daemon = True
        • 等待所有的非守护子线程都结束之后才会结束
      • 函数
        • current_thread()
          • 在哪个线程中被调用,就返回当前线程的对象
        • enumerate
          • 返回当前活着的线程对象列表
        • active_count
          • 返回当前活着的线程对象个数,相当于len(enumerate())
  • 总结

    • 操作系统

      • 多道:遇到IO会切换
      • 分时:时间片到了会切换
    • 进程特点:

      • 数据隔离

      • IPC机制

      • 开销大

      • 能并行

      • 进程之间共享数据

        • Manager类

        • Lock锁

    • 生产者消费者模型

  • 生成器

    g1 = filter(lambda n: n % 2 == 0, range(10))  # g1 = [0 2 4 6 8]
    g2 = map(lambda n: n * 2, range(3))  # g2 = [0 2 4]
    
    for i in g1:           # i=0         # i=1
        for j in g2:       # 0 2 4       # 此时g2已经为空,for循环不执行。
            print(i * j)
    
    def multipliers():
        return [lambda x:i*x for i in range(4)]  # 列表生成式,存放了4个函数[lambda x:3x,...]
    
    print([m(2) for m in multipliers()]) # [6, 6, 6, 6]
    

内容详细

1.锁

from threading import Thread

a = 0
def add():
    global a
    for i in range(500000):
        a += 1

def sub():
    global a
    for i in range(500000):
        a -= 1

t1 = Thread(target=add)
t1.start()
t2 = Thread(target=sub)
t2.start()
t1.join()
t2.join()
print(a)

# 结果是随机的
# 对于cpython解释器,即便线程有GIL锁,也会出现数据不安全问题。
a = 0
def add():
    global a
    a += 1
    
import dis
dis.dis(add)

# add函数对应cpu中的执行指令
 0 LOAD_GLOBAL          0 (a)    
 2 LOAD_CONST           1 (1)    
 4 INPLACE_ADD                        
 6 STORE_GLOBAL         0 (a)	    '存回结果'
 8 LOAD_CONST           0 (None)
10 RETURN_VALUE

# 线程1执行:获取全局变量a的值,在cpu中做加法运算(a=0+1),还未将结果存入回去就轮转到线程2执行。
# 线程2执行:获取全局变量a的值依旧为0,并未获取到线程1执行后的结果,这样就导致了数据不安全问题。

多线程数据不安全问题:

  • 操作的是全局变量
  • 涉及到先计算再赋值的操作
    • += 、-= 、*= 、/=
    • 包括lst[0] += 1dic['key'] -= 1
from threading import Thread, Lock

a = 0
def add(lock):
    global a
    for i in range(500000):
        with lock:
            a += 1

def sub(lock):
    global a
    for i in range(500000):
        with lock:
            a -= 1

lock = Lock()
t1 = Thread(target=add, args=(lock,))
t1.start()
t2 = Thread(target=sub, args=(lock,))
t2.start()
t1.join()
t2.join()
print(a)

互斥锁

# 在同一个线程中不能acquire多次,否则会死锁。
'房间相互独立,退出一个房间才能进另一个房间。'
from threading import Lock

lock = Lock()
lock.acquire()
print(666)
lock.acquire()
print(888)

# 分割线
lock = Lock()
lock.acquire()
print(666)
lock.release()
lock.acquire()
print(888)
lock.release()

单例模式

import time
from threading import Thread, Lock

class A:
    __instance = None

    def __new__(cls, *args, **kwargs):
        if not cls.__instance:
            time.sleep(0.1)  # 强制移交GIL锁
            cls.__instance = super().__new__(cls)
            return cls.__instance

    def __init__(self, name, age):
        self.name = name
        self.age = age

def func():
    a = A('alex', 18)
    print(a)

for i in range(10):
    t = Thread(target=func)
    t.start()
# 加锁才安全
import time
from threading import Thread, Lock

class A:
    # from threading import Lock  
    __instance = None
    lock = Lock()

    def __new__(cls, *args, **kwargs):
        with cls.lock:
            if not cls.__instance:
                time.sleep(0.1)
                cls.__instance = super().__new__(cls)
            return cls.__instance

    def __init__(self, name, age):
        self.name = name
        self.age = age

def func():
    a = A('alex', 18)
    print(a)

for i in range(10):
    t = Thread(target=func)
    t.start()

死锁现象

import time
from threading import Thread, Lock

noodle_lock = Lock()
fork_lock = Lock()

def eat1(name, noodle_lock, fork_lock):
    noodle_lock.acquire()
    print('%s抢到面了' % name)
    fork_lock.acquire()
    print('%s抢到叉子了' % name)
    print('%s吃了一口面' % name)
    time.sleep(0.1)
    fork_lock.release()
    print('%s放下叉子了' % name)
    noodle_lock.release()
    print('%s放下面了' % name)

def eat2(name, noodle_lock, fork_lock):
    fork_lock.acquire()
    print('%s抢到叉子了' % name)
    noodle_lock.acquire()
    print('%s抢到面了' % name)
    print('%s吃了一口面' % name)
    time.sleep(0.1)
    noodle_lock.release()
    print('%s放下面了' % name)
    fork_lock.release()
    print('%s放下叉子了' % name)

lst = ['alex', 'wusir', 'taibai', 'yuan']

Thread(target=eat1, args=(lst[0], noodle_lock, fork_lock)).start()
Thread(target=eat2, args=(lst[1], noodle_lock, fork_lock)).start()
Thread(target=eat1, args=(lst[2], noodle_lock, fork_lock)).start()
Thread(target=eat2, args=(lst[3], noodle_lock, fork_lock)).start()

死锁现象发生原因?

  • 多把锁交替使用

解决方案

  • 递归锁
    • 优点:解决问题耗时短
    • 缺点:效率差
  • 优化代码逻辑
    • 优点:效率高(将多把互斥锁变成一把互斥锁)
    • 缺点:解决问题耗时长
  • 总结
    • 死锁现象
      • 多把锁交替使用
        • 无论互斥锁还是递归锁,当创建多把且交替使用都会发生死锁现象。
    • 解决方式
      • 创建递归锁:本质就是快速将多把互斥锁变成了一把递归锁(可连续acquire)。
      • 优化代码逻辑:本质就是将多把交替使用的互斥锁变成一把互斥锁。
      • 两个方式究其根源是一样的。

递归锁

# 在同一个线程中可以acquire多次,不会死锁。
'房间中套着房间,进了多少层房间就要退出多少层房间。'
from threading import RLock
rlock = RLock()
rlock.acquire()
print(1)
rlock.acquire()
print(2)
rlock.acquire()
print(3)
# 通过递归锁解决死锁现象
import time
from threading import Thread, RLock

# noodle_lock = fork_lock = RLock() 
'如果创造两个递归锁对象,还是会发生死锁'
lock = RLock()

# def eat1(name, noodle_lock, fork_lock):
def eat1(name, lock)
    # noodle_lock.acquire()
    lock.acquire()
    print('%s抢到面了' % name)
    # fork_lock.acquire()
    lock.acquire()
    print('%s抢到叉子了' % name)
    print('%s吃了一口面' % name)
    time.sleep(0.1)
    # fork_lock.release()
    lock.release()
    print('%s放下叉子了' % name)
    # noodle_lock.release()
    lock.release()
    print('%s放下面了' % name)

def eat2(name, noodle_lock, fork_lock):
    fork_lock.acquire()
    print('%s抢到叉子了' % name)
    noodle_lock.acquire()
    print('%s抢到面了' % name)
    print('%s吃了一口面' % name)
    time.sleep(0.1)
    noodle_lock.release()
    print('%s放下面了' % name)
    fork_lock.release()
    print('%s放下叉子了' % name)

lst = ['alex', 'wusir', 'taibai', 'yuan']

Thread(target=eat1, args=(lst[0], noodle_lock, fork_lock)).start()
Thread(target=eat2, args=(lst[1], noodle_lock, fork_lock)).start()
Thread(target=eat1, args=(lst[2], noodle_lock, fork_lock)).start()
Thread(target=eat2, args=(lst[3], noodle_lock, fork_lock)).start()
# 互斥锁解决死锁现象
import time
from threading import Thread, Lock
lock = Lock()

def eat1(name, lock):
    lock.acquire()
    print('%s抢到面了' % name)
    print('%s抢到叉子了' % name)
    print('%s吃了一口面' % name)
    time.sleep(0.1)
    print('%s放下叉子了' % name)
    print('%s放下面了' % name)
    lock.release()

def eat2(name, lock):
    lock.acquire()
    print('%s抢到叉子了' % name)
    print('%s抢到面了' % name)
    print('%s吃了一口面' % name)
    time.sleep(0.1)
    print('%s放下面了' % name)
    print('%s放下叉子了' % name)
    lock.release()

lst = ['alex', 'wusir', 'taibai', 'yuan']

Thread(target=eat1, args=(lst[0], lock)).start()
Thread(target=eat2, args=(lst[1], lock)).start()
Thread(target=eat1, args=(lst[2], lock)).start()
Thread(target=eat2, args=(lst[3], lock)).start()

2.线程队列

区别

queue模块

  • 实现线程间安全通信

multiprocessing模块中的Queue

  • 实现进程间安全通信

先进先出队列

from queue import Queue

q = Queue(5) # 先进先出队列
q.put(1)
q.put(2)
q.put(3)
q.put(4)
q.put(5)
print('444')
q.put(6)     # 队列满了,发生阻塞
print('555') # 不会打印 
from queue import Queue

q = Queue(5)
q.put(1)
q.put(2)
q.put(3)
q.put(4)
q.put(5)

print(q.get())
print(q.get())
print(q.get())
print(q.get())
print(q.get())
print(q.get()) # 队列为空,等待取值会阻塞。

后进先出队列

# 栈(last in first out)
from queue import LifoQueue
lfq = LifoQueue(3)
lfq.put(1)
lfq.put(2)
lfq.put(3)

print(lfq.get())
print(lfq.get())
print(lfq.get())

优先级队列

from queue import PriorityQueue

pq = PriorityQueue()
pq.put((3,'yuan'))
pq.put((2, 'wusir'))
pq.put((1,'alex'))

print(pq.get())
print(pq.get())
print(pq.get())
结果:
(1, 'alex')
(2, 'wusir')
(3, 'yuan')
# 优先级序列实现自动排序

应用场景

  • 先进先出
    • 服务相关
      • Server端将所有用户请求放在队列中,先来先服务。
  • 后进先出
    • 算发相关
      • 三级菜单
  • 优先级
    • VIP服务
      • 抢票优先
    • 运维
      • 告警级别

3.池

进程池

  • 预先开启固定个数的进程组成一个进程池,当任务来临的时候直接提交给已经开好的进程池中的进程。
  • 节省进程的开启、关闭、切换时间。
  • 减轻操作系统调度负担。
# submit and shutdown
import os
import time
import random
from concurrent.futures import ProcessPoolExecutor

def func():
    print('start', os.getpid())
    time.sleep(random.randint(1, 3))
    print('end', os.getpid())

if __name__ == '__main__':
    p = ProcessPoolExecutor(5)  # cpu的核心数+1
    for i in range(10):
        p.submit(func)  # 提交任务给进程池
    p.shutdown()  # 关闭池之后,就不能继续提交任务,并且会阻塞直到已经提交的任务完成。
    print('main', os.getpid())
# 参数 + 返回值
import os
import time
import random
from concurrent.futures import ProcessPoolExecutor

def func(i):
    print('start', os.getpid())
    time.sleep(random.randint(1, 3))
    print('end', os.getpid())
    return '%s * %s' %(i, os.getpid())

if __name__ == '__main__':
    p = ProcessPoolExecutor(5)
    ret_l = []
    for i in range(10):
        ret = p.submit(func, i)  # 提交任务时可以传入参数
        print(ret)
        ret_l.append(ret)
    for ret in ret_l:
        print('>>>', ret.result())  # result 同步阻塞
    print('main', os.getpid())

线程池

  • 传统多进程和多线程方案会使用“即时创建, 即时销毁”的策略。尽管与创建进程相比,创建线程的时间已经大大的缩短,但是如果提交给线程的任务是执行时间较短,而且执行次数极其频繁,那么服务器将处于不停的创建线程,销毁线程的状态。
  • 一个线程的运行时间可以分为3部分:线程的启动时间、线程体的运行时间和线程的销毁时间。在多线程处理的情景中,如果线程不能被重用,就意味着每次创建都需要经过启动、销毁和运行3个过程。这必然会增加系统相应的时间,降低了效率。
  • 使用线程池:
    由于线程预先被创建并放入线程池中,同时处理完当前任务之后并不销毁而是被安排处理下一个任务,因此能够避免多次创建线程,从而节省线程创建和销毁的开销,能带来更好的性能和系统稳定性。
import os
import time
import random
from concurrent.futures import ThreadPoolExecutor

def func(i):
    print('start', os.getpid())
    time.sleep(random.randint(1, 3))
    print('end', os.getpid())
    return '%s * %s' % (i, os.getpid())

tp = ThreadPoolExecutor(20)  # cpu的核心数*5

lst = []
for i in range(10):i
    obj = tp.submit(func, i) # ret是对象
    lst.append(obj) # 对象加入列表
tp.shutdown() # 阻塞
for ret in lst:
    print('>>>', obj.result()) # 对象.result() 获取返回值
import os
import time
import random
from concurrent.futures import ThreadPoolExecutor

def func(i):
    print('start', os.getpid())
    time.sleep(random.randint(1, 3))
    print('end', os.getpid())
    return '%s * %s' % (i, os.getpid())

tp = ThreadPoolExecutor(20)  # cpu的核心数*5
ret = tp.map(func, range(10))  # 提交任务时迭代传入参数
for i in ret: # ret是生成器,每个元素是返回值。
    print(i)

回调函数

import requests
from concurrent.futures import ThreadPoolExecutor

def get_page(url):
    res = requests.get(url)
    return {'url':url, 'content':res.text}

def parserpage(ret):
    dic = ret.result()
    print(dic['url'])


tp = ThreadPoolExecutor(5)
url_lst = [
    'http://www.baidu.com',
    'http://www.cnblogs.com',
    'http://www.douban.com',
    'http://www.tencent.com',
    'https://www.cnblogs.com/elliottwave/'
]

for url in url_lst:
     ret = tp.submit(get_page, url)
     ret.add_done_callback(parserpage) # 哪个线程先结束就先调用parserpage函数
import requests
from concurrent.futures import ThreadPoolExecutor

def get_page(url):
    res = requests.get(url)
    return {'url':url, 'content':res.text}

def parserpage(dic):
    print(dic['url'])


tp = ThreadPoolExecutor(5)
url_lst = [
    'http://www.baidu.com',
    'http://www.cnblogs.com',
    'http://www.douban.com',
    'http://www.tencent.com',
    'https://www.cnblogs.com/elliottwave/'
]

ret_lst = []
for url in url_lst:
     ret = tp.submit(get_page, url)
     ret_lst.append(ret) # 顺序添加对象到列表
for ret in ret_lst:
    dic = ret.result() # 如果先添加的对象在线程池中对应的线程未结束,这里就会阻塞等待返回结果。
    parserpage(dic)  # 效率低于使用回调函数的方法

4.总结

  • 创建进程池、线程池
    • 导入concurrent.futures模块中的ProcessPoolExcutor类ThreadPoolExcutor类
    • p = ProcessPoolExcutor(池中进程个数)
    • tp = ThreadPoolExcutor(池中线程个数)
  • 异步提交任务
    • ret = p.submit(函数名, 参数1, 参数2, ...)
    • ret = tp.submit(函数名, 参数1, 参数2, ...)
  • 获取返回值
    • ret.result()
  • 阻塞直到所有任务执行完毕
    • p.shutdown()
    • tp.shutdown()
  • map方法
    • ret = tp.map(func, iterable)
      • 迭代获取iterable中的元素作为func的参数,让池中线程执行对应的任务。
    • for i in ret:每一个i都是func的返回值
  • 回调函数
    • ret.add_done_callback(函数名)
    • 在ret对应的任务执行结束后直接继续执行add_done_callback绑定的函数,且ret作为参数传入绑定函数。
posted @ 2020-04-07 21:29  Sco_Lunatic  阅读(76)  评论(0)    收藏  举报