锁(线程)/线程安全 + 线程池(ThreadPoolExecutor) + threading.local + 生产者消费者模型

 

锁(线程) / 线程安全

线程安全  

1. 什么是线程安全?

  线程安全是 python 的内置功能, 在多线程操作时,内部会让所有的线程排队处理.

  线程安全,  列表/字典/队列 都是线程安全的.

import threading

v = []
def func(arg):
    v.append(arg) # 线程安全  列表的添加属于线程安全的不需要加锁
    print(v)
for i in range(10):
    t =threading.Thread(target=func,args=(i,))
    t.start()

2. 线程不安全的时候怎么办?  

  线程不安全  +   锁    ==>   线程排队处理(编程安全的了)

3. 为什么要加锁?

  a. 非线程安全情况下 加锁会变得安全

  b. 加锁还可以控制一段代码,保证代码处理数据的完整性

4. 线程越多越好吗?

  不是,线程越多,上下文管理越复杂(来回切换),会降低效率

所有4中情况

1. 锁 Lock     

  1次放1个 / 一个一个的放

  threading.Lock()     

需求:
    a. 创建100个线程,在列表中追加8 
    b. 创建100个线程 
        v = []
        锁 
        - 把自己的添加到列表中。
        - 在读取列表的最后一个。 # 添加进去读取的最后一个有可能不是自己添加的,因为添加的过程中,别的线程也可以添加,为了防止混乱要加锁,可以保证数据的完整性
        解锁

具体代码示例:

import threading
import time
v = []
lock = threading.Lock()   ####     创建一个锁对象  lock 

def func(arg):
    lock.acquire()         ####   加锁
    v.append(arg)  # 数据追加都列表
    time.sleep(0.01)
    m = v[-1]  # 取最后一个值
    print(arg,m)
    lock.release()        ####    解锁

for i in range(10):   
    t =threading.Thread(target=func,args=(i,))   # 循环创建了10个线程 来执行func函数
    t.start()

2. 锁 RLock     

  1次放1个 / 一个一个的放

    threading.RLock()     

    .acquire()        # 加锁

    .release()        # 解锁 

RLock 和 Lock 的作用是一样的,都是一个一个放,用法也类似 Lock.

区别:

   Lock  同步锁: 如果连续两次 .acquire() 加锁, 会变成死锁 解不开. 必须是加一次锁 解一次锁

   RLock  递归锁: 可以连续两次加锁,再解两次

3. 锁 BoundedSemaphpre  (也叫信号量)   

  一次放n个 / n个n个的放     n为固定值  参数

    threading.BoundedSemaphore(n)      # 创建 锁 对象

    .acquire()        # 加锁

    .release()        # 解锁 

import time
import threading

lock = threading.BoundedSemaphore(3)   # 创建锁lock   3个3个放 
def func(arg):
    lock.acquire()    # 加锁
    print(arg)
    time.sleep(1)
    lock.release()    # 解锁

for i in range(20):
    t =threading.Thread(target=func,args=(i,))
    t.start()

4. 锁 Condition 

  1次放n个, n为动态值,  可以变化, 

  BoundedSemaphpre是每次都放 n 个

    lock = threading.Condition()      # 创建 lock 锁

    lock.wait()           #  加锁  夯住,等待另外的进程来释放锁

    lock.notify(3)      # 确定释放线程的个数

 

等价于,  lock.wait_for(func)   # 其中func为函数,当线程满足func时,便放行

方式一:

  给定被同时释放的线程的个数, 也就是执行这几个线程.

  如果.notify(3)   就是执行3个线程, 执行完了也不会接着再执行3个,需要重新给定个数,给定以后,从未执行的线程中,继续执行相应的个数.

import time
import threading

lock = threading.Condition()
def func(arg):
    print('线程进来了')
    lock.acquire()
    lock.wait()     # 加锁
    
    print(arg)
    time.sleep(1)
    
    lock.release()

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

while True:
    inp = int(input('>>>'))

    lock.acquire()
    lock.notify(inp)  # 设置放行个数  为动态的值
    lock.release()

方式二:

  满足 f1 函数条件的 线程可以被释放

import time
import threading

lock = threading.Condition()
def f1():
    print('来执行函数了')
    input(">>>")
    # ct = threading.current_thread() # 获取当前线程
    # ct.getName()
    return True      # 满足return True 就可以往下走

def func(arg):
    print('线程进来了')
    lock.wait_for(f1)   # f1 为True
    print(arg)
    time.sleep(1)

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

5. 锁 Event

  1次释放所有的, 

    threading.Event()  

    wait() 来阻止所有的线程,

    用 set() 来解除阻止 释放所有的.

import threading

lock = threading.Event()   # 创建

def func(arg):
    print('线程来了')
    lock.wait()  # 加锁:红灯 阻止所有的线程
    print(arg)

for i in range(10):
    t =threading.Thread(target=func,args=(i,))  # 创建了10个进程每个进程都在wait()处停止了
    t.start()

input(">>>>")
lock.set() # 绿灯  解除阻止 所有线程都可以通行了

lock.clear()   # 再次变红灯  清除lock.set() 下次创建线程需要 lock.set()
                 # 如果没有clear()的话, 下次创建的线程,可以被执行

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

input(">>>>")

lock.set()  # 前面有clear() 又变成了红灯 这里需要再次解除阻止  变绿灯

总结:

  Lock (同步锁)      1个1个的放

  RLock(递归锁)       1个1个的放

  BoundedSemaphore(n)       n个n个的放 (固定个数的放)

  Contion         1次释放n个,下次输入的值,会接着放未执行的线程

                1次释放满足函数条件的线程

  Event          1次释放所有的

线程池

1. 什么是线程池?

  线程池可以控制最多创建的线程的个数. 

  如果线程池中有5个线程,那么最多有5个线程可以同时被CPU调度.

  如果一个程序需要创建10个线程来完成任务,线程池只设置了5,那么会先有5个线程来工作,当其中有线程结束的时候,才会创建新的线程来继续工作,总之要保证,能够被cpu同时调度的线程数不能超过设置的5. (这种原理类似于下载时,设置同时下载数为3.)

2. 如何控制线程的个数, 线程池的编写?

  用线程池控制:  需要导入模块 

    from concurre.futures import  ThreadPoolExecutor

    pool = ThreadPoolExecutor(5)    # 设置5

    pool.submit(函数名,参数1,参数2)

from concurrent.futures import ThreadPoolExecutor
import time

def task(a1,a2):
    time.sleep(1)
    print(a1,a2)

# 创建了一个线程池(最多5个线程)
pool = ThreadPoolExecutor(5)

for i in range(10):
    # 去线程池中申请一个线程,让线程执行task函数。
    pool.submit(task,i,8)  # (函数名,参数1,参数2)

3. 线程和线程池的比较

   以后创建线程的时候,一定要用线程池,可以控制线程的个数

# # ######################## 线程 ###########################
import time
import threading

def task(arg):
    time.sleep(50)

while True:
    num = input('>>>')
    t = threading.Thread(target=task,args=(num,))    # 一直在循环创建线程
    t.start()

# ######################## 线程池 ###########################
import time
from concurrent.futures import ThreadPoolExecutor

def task(arg):
    time.sleep(50)

pool = ThreadPoolExecutor(20)  # 线程池 设置20   较好
while True:
    num = input('>>>')
    pool.submit(task,num)

threading.local

1. threading.local()

作用:内部自动为每个线程维护一个空间(字典),用于当前存取属于自己的值。

   保证线程之间的数据隔离。

   这样做,资源相对会浪费.
      {
        线程ID: {...}
        线程ID: {...}
        线程ID: {...}
        线程ID: {...}
      }

注意: threading.local()   在py2中不存在, 在 py3 中存在

# 示例
import
time import threading v = threading.local() def func(arg): # 内部会为当前线程创建一个空间用于存储:phone=自己的值 v.phone = arg time.sleep(1) print(v.phone,arg) # 去当前线程自己空间取值 for i in range(10): t =threading.Thread(target=func,args=(i,)) t.start()

2. threadinglocal原理

  threading.get_ident()     # 获取当前的线程的ident

import time
import threading

DATA_DICT = {}  # 创建一个字典用于存放数据

def func(arg):
    ident = threading.get_ident()   # 获取当前线程ident 作为key
    DATA_DICT[ident] = arg
    time.sleep(1)
    print(DATA_DICT)
    print(DATA_DICT[ident],arg)

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

因为在打印之前要停留1秒,此时每个线程都已经将自己ident 和参数值 添加到了字典内,所以打印的是完整的字典.

# {20444: 0, 10340: 1, 15368: 2, 15404: 3, 19096: 4, 15312: 5, 17896: 6, 10492: 7, 17432: 8, 16584: 9}
# 0 0
# {20444: 0, 10340: 1, 15368: 2, 15404: 3, 19096: 4, 15312: 5, 17896: 6, 10492: 7, 17432: 8, 16584: 9}
# 4 4
# {20444: 0, 10340: 1, 15368: 2, 15404: 3, 19096: 4, 15312: 5, 17896: 6, 10492: 7, 17432: 8, 16584: 9}
# 2 2
# {20444: 0, 10340: 1, 15368: 2, 15404: 3, 19096: 4, 15312: 5, 17896: 6, 10492: 7, 17432: 8, 16584: 9}
# {20444: 0, 10340: 1, 15368: 2, 15404: 3, 19096: 4, 15312: 5, 17896: 6, 10492: 7, 17432: 8, 16584: 9}
# 3 3
# {20444: 0, 10340: 1, 15368: 2, 15404: 3, 19096: 4, 15312: 5, 17896: 6, 10492: 7, 17432: 8, 16584: 9}
# 5 5
# 1 1
# {20444: 0, 10340: 1, 15368: 2, 15404: 3, 19096: 4, 15312: 5, 17896: 6, 10492: 7, 17432: 8, 16584: 9}
# 8 8
# {20444: 0, 10340: 1, 15368: 2, 15404: 3, 19096: 4, 15312: 5, 17896: 6, 10492: 7, 17432: 8, 16584: 9}
# 9 9
# {20444: 0, 10340: 1, 15368: 2, 15404: 3, 19096: 4, 15312: 5, 17896: 6, 10492: 7, 17432: 8, 16584: 9}
# 6 6
# {20444: 0, 10340: 1, 15368: 2, 15404: 3, 19096: 4, 15312: 5, 17896: 6, 10492: 7, 17432: 8, 16584: 9}
# 7 7
结果

2. threadinglocal 原理  (可选, 了解)

"""
以后:Flask框架内部看到源码 上下文管理

"""

import time
import threading
INFO = {}
class Local(object):

    def __getattr__(self, item):
        ident = threading.get_ident()
        return INFO[ident][item]

    def __setattr__(self, key, value):
        ident = threading.get_ident()
        if ident in INFO:
            INFO[ident][key] = value
        else:
            INFO[ident] = {key:value}

obj = Local()

def func(arg):
    obj.phone = arg # 调用对象的 __setattr__方法(“phone”,1)
    time.sleep(2)
    print(obj.phone,arg)


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

生产者和消费者模型   (队列)

1. 该模型有三部件

  生产者 :  制造任务,制造的任务保存在队列中

  队列 : 先进先出

  消费者 : 处理任务,从队列中取出任务处理

2. 生产者和消费者模型解决了什么问题?

  解决了不用的等待的问题. 例如 订票原理

示例:

import time
import queue
import threading

q = queue.Queue() # 队列是线程安全的

def producer(id):
    """
    生产者
    :return:
    """
    while True:
        time.sleep(2)
        q.put('包子')
        print('厨师%s 生产了一个包子' %id )

for i in range(1,4):
    t = threading.Thread(target=producer,args=(i,))  # 创建3个线程 即3个厨师生产包子
    t.start()

def consumer(id):
    """
    消费者
    :return:
    """
    while True:
        time.sleep(1)
        v1 = q.get()
        print('顾客 %s 吃了一个包子' % id)

for i in range(1,3):
    t = threading.Thread(target=consumer,args=(i,))   # 创建2个线程 即2个顾客来吃包子
    t.start()

 

posted @ 2018-09-11 14:55  葡萄想柠檬  Views(1385)  Comments(0)    收藏  举报
目录代码