锁/threadinglocal的应用及原理/线程池/生产者消费者模型

一.关于上篇博客的补充

1.Python的GIL锁

  Python内置的一个全局解释器锁,锁的作用就是保证同一时刻一个进程中只有一个线程可以被CPU调度

2.为什么要有这把锁?

  答:Python语言的创始人在开发这门语言时,目的是快速把语言开发出来,如果加上GIL锁(C语言加锁),切换时按照100条字节指令来进行线程间的切换

3.进程与线程的区别:

  线程:CPU工作的最小单元

  进程:为线程提供一个资源共享的空间

  一个进程中默认是有一个主线程

二.多线程锁,也就是threading模块

引入threading模块

threading用于提供线程相关的操作,线程是应用程序中工作的最小单元。python当前版本的多线程库没有实现优先级、线程组,线程也不能被停止、暂停、恢复、中断。

threading模块提供的类:

  Thread,Lock,RLock,Condition,[Bounded]Semaphore,Event,Time,Local

threading模块提供的常用方法:

  threading.currentThread():返回当前的线程变量

  threading.enumerate():返回一个包含正在运行的线程的list.正在运行指线程启动后,结束前,不包括启动前和终止后的线程

  threading.activeCount():返回正在运行的线程数量,与len(threading.enumerate())有相同的结果

threading模块提供的常量:

  threading.TIMEOUT_MAX设置threading全局超过时间

1.线程锁:Lock/Rlock

由于线程之间随机调度:某线程可能在执行N条后,CPU接着执行其他线程.为了多个线程同时操作一个内存中的资源时不产生混乱,我们使用锁

(1).Lock(指令锁)是可用的最低级的同不指令.Lock处于锁定状态时,不被特定的线程拥有.

  Lock包含两种状态--锁定和非锁定,以及两个基本的方法.

  可以认为Lock有一个锁定池,当线程请求锁定时,将线程至于池中,直到获得锁定出池,池中的线程处于状态图中的同步阻塞状态

(2).RLock(可重入锁)是一个可以被同一个线程请求多次的同步指令.RLock使用了"拥有的线程"和"递归等级"的概念,处于锁定状态时,RLock被某个线程拥有.拥有RLock的线程可以再次调用acquire(),释放锁时需要调用release()相同次数

  可以认为RLock包含一个锁定池和一个初识值为0的计数器,每次成功调用acquire()/release(),计数器将+1/-1,为0时锁处于未锁定状态

简而言之:Lock属于全局,RLock属于线程

构造方法:Lock(),RLock();推荐使用RLock()

实例方法:

  acquire([timeout]): 尝试获得锁定。使线程进入同步阻塞状态。 
  release(): 释放锁。使用前线程必须已获得锁定,否则将抛出异常。

(3).习题:创建10个线程Lock方法

  v = [] : 锁

  把自己的添加列表中

  再读取列表中的最后一个

  解锁

线程安全,多线程操作时,内部会让所有线程排队处理.如:list/dict/Queue

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()

结果:
[0]
[0, 1]
[0, 1, 2]
[0, 1, 2, 3]
[0, 1, 2, 3, 4]
[0, 1, 2, 3, 4, 5]
[0, 1, 2, 3, 4, 5, 6]
[0, 1, 2, 3, 4, 5, 6, 7]
[0, 1, 2, 3, 4, 5, 6, 7, 8]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
View Code

线程不安全 + 人 => 排队处理

import threading
import time

v = []
lock = threading.Lock()

def func(arg):
    lock.acquire()
    v.append(arg)
    time.sleep(0.01)
    # print(v)
    m = v[-1]
    print(arg,m)
    lock.release()

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

结果:
结果:
加锁   不加锁
0 0    2 9
1 1    0 9
2 2    1 9
3 3    7 9
4 4    4 9
5 5    3 9
6 6    5 9
7 7    6 9
8 8    9 9
9 9    8 9
示例

(4).锁:RLock(1次放1个)

import threading
import time

v = []
lock = threading.RLock()
def func(arg):
    lock.acquire()
    lock.acquire()

    v.append(arg)
    time.sleep(0.01)
    m = v[-1]
    print(arg,m)

    lock.release()
    lock.release()

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

结果:
0 0
1 1
2 2
3 3
4 4
5 5
6 6
7 7
8 8
9 9
View Code

(5).Lock对比RLock

import threading

lock = threading.Lock() #Lock对象
lock.acquire()
lock.acquire()  #产生了死锁。
lock.release()
lock.release()
print lock.acquire()
 
 
import threading
rLock = threading.RLock()  #RLock对象
rLock.acquire()
rLock.acquire() #在同一线程内,程序不会堵塞。
rLock.release()
rLock.release()

2.信号量(Semaphore)

互斥锁 同时只允许一个线程更改数据,而Semaphore是同时允许一定数量的线程更改数据 ,比如厕所有3个坑,那最多只允许3个人上厕所,后面的人只能等里面有人出来了才能再进去。

import time
import threading

lock = threading.BoundedSemaphore(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()

结果:
0
1
2
...等1秒
3
4
5
...
6
7
8
...
9
10
11
...
12
13
14
...
15
16
17
...
18
19
锁semaphore

3.条件(Condition)

(1).使得线程等待,只有满足某条件时,才释放n个线程

(2).Condition(条件变量)通常与一个锁关联。需要在多个Contidion中共享一个锁时,可以传递一个Lock/RLock实例给构造方法,否则它将自己生成一个RLock实例。

(3)可以认为,除了Lock带有的锁定池外,Condition还包含一个等待池,池中的线程处于等待阻塞状态,直到另一个线程调用notify()/notifyAll()通知;得到通知后线程进入锁定池等待锁定。

(4).构造方法:

  Condition([lock/rlock])

(5).实例方法: 
  acquire([timeout])/release(): 调用关联的锁的相应方法。 

  wait([timeout]): 调用这个方法将使线程进入Condition的等待池等待通知,并释放锁。使用前线程必须已获得锁定,否则将抛出异常。 
  notify(): 调用这个方法将从等待池挑选一个线程并通知,收到通知的线程将自动调用acquire()尝试获得锁定(进入锁定池);其他线程仍然在等待池中。调用这个方法不会释放锁定。使用前线程必须已获得锁定,否则将抛出异常。 
  notifyAll(): 调用这个方法将通知等待池中所有的线程,这些线程都将进入锁定池尝试获得锁定。调用这个方法不会释放锁定。使用前线程必须已获得锁定,否则将抛出异常。

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()
方式一:
import time
import threading

lock = threading.Condition()

def xxxx():
    print('来执行函数了')
    input(">>>")
    # ct = threading.current_thread() # 获取当前线程
    # ct.getName()
    return True

def func(arg):
    print('线程进来了')
    lock.wait_for(xxxx)
    print(arg)
    time.sleep(1)

for i in range(10):
    t =threading.Thread(target=func,args=(i,))
    t.start()
方式二(推荐):

4.Event(事件)

(1).Event(事件)是最简单的线程通信机制之一:一个线程通知事件,其他线程等待事件。Event内置了一个初始为False的标志,当调用set()时设为True,调用clear()时重置为 False。wait()将阻塞线程至等待阻塞状态。

(2).Event其实就是一个简化版的 Condition。Event没有锁,无法使线程进入同步阻塞状态。

(3).构造方法: 
  Event()

(4).实例方法: 
  isSet(): 当内置标志为True时返回True。 

  set(): 将标志设为True,并通知所有处于等待阻塞状态的线程恢复运行状态。 
  clear(): 将标志设为False。 
  wait([timeout]): 如果标志为True将立即返回,否则阻塞线程至等待阻塞状态,等待其他线程调用set()

import time
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,))
    t.start()

input(">>>>")
lock.set() # 绿灯
time.sleep(1)


lock.clear() # 再次变红灯

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

input(">>>>")
lock.set()

结果:
线程来了
线程来了
线程来了
线程来了
线程来了
线程来了
线程来了
线程来了
线程来了
线程来了
>>>>1
0
1
2
3
7
6
9
4
5
8
...睡1秒
线程来了
线程来了
线程来了
线程来了
线程来了
线程来了
线程来了
线程来了
线程来了
线程来了
>>>>2
0
2
4
5
6
9
8
3
7
1
示例:

5.local类

(1).local是一个小写字母开头的类,用于管理 thread-local(线程局部的)数据。对于同一个local,线程无法访问其他线程设置的属性;线程设置的属性不会被其他线程设置的同名属性替换。

(2).可以把local看成是一个“线程-属性字典”的字典,local封装了从自身使用线程作为 key检索对应的属性字典、再使用属性名作为key检索属性值的细节。

import time
import threading

v = threading.local()

def func(arg):
    # 内部会为当前线程创建一个空间用于存储:phone=自己的值
    v.phone = arg
    time.sleep(2)
    print(v.phone,arg) # 去当前线程自己空间取值

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

结果:
...睡2秒
2 2
1 1
0 0
6 6
4 4
5 5
3 3
8 8
9 9
7 7
View Code

(3).threading.local的原理

import time
import threading

DATA_DICT = {}

def func(arg):
    """
    :param arg: 传过来的i值
    :return: 
    """
    ident = threading.get_ident()   # 获得线程代号
    print(ident)
    DATA_DICT[ident] = arg  # 将线程代号和传过来的i值添加到字典
    time.sleep(1)   # 睡一秒
    print(DATA_DICT[ident],arg)    # 打印线程代号所对应的值和传过来的i值

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

结果:
18356
1388
9440
19216
7300
7888
2500
2100
12404
1000
1 1
0 0
2 2
4 4
3 3
6 6
7 7
5 5
9 9
8 8
View Code

(4).threading.local的原理(可选)

"""
以后: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()

结果:
3 3
0 0
2 2
1 1
7 7
5 5
4 4
6 6
8 8
9 9
View Code

6.线程池

(1).线程池技术为线程创建、销毁的开销问题和系统资源不足问题提供了很好的解决方案。

优点:

       (1)可以控制产生线程的数量。通过预先创建一定数量的工作线程并限制其数量,控制线程对象的内存消耗。

  (2)降低系统开销和资源消耗。通过对多个请求重用线程,线程创建、销毁的开销被分摊到了多个请求上。另外通过限制线程数量,降低虚拟机在垃圾回收方面的开销。

  (3)提高系统响应速度。线程事先已被创建,请求到达时可直接进行处理,消除了因线程创建所带来的延迟,另外多个线程可并发处理。

from concurrent.futures import ThreadPoolExecutor
import time

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

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

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

结果:
每输出5个结果,睡2秒
View Code

7.练习

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

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

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)
    print(arg)

pool = ThreadPoolExecutor(20)
while True:
    num = input('>>>')
    pool.submit(task,num)
练习走起:

8.生产者消费者模型

引入queue模块

概念:queue是python标准库中的线程安全的队列(FIFO)实现,提供了一个适用于多线程编程的先进先出的数据结构,即队列,用来在生产者和消费者线程之间的信息传递

(1).业界用的比较广泛,多线程之间进行同步数据的方法,解决线程之间的堵塞,互相不影响

(2).三部件:

   生产者

    队列:先进先出

    扩展:栈,后进先出

   消费者

(3).问:生产者消费者模型解决了什么问题?不用一直等待的问题

(4).在一个程序中实现又有生产者又有消费者,生产者不断生产,消费者不断消费,达到并行数据安全完整交互的目的。所以会有消息队列的关键字产生,队列是典型的生产者消费者模型

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,))
    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,))
    t.start()

结果:
一直不停循环....
View Code
posted @ 2018-09-11 17:38  骑驴老神仙  阅读(233)  评论(0)    收藏  举报