threading 模块

先理解一下进程与线程的概念和区别,然后通过threading模块来学习理解线程。进程要下次讲了。
之后看一下两种调用线程的方式,效果和实现都一样。貌似也没有什么时候用哪种,反正爱用哪种用哪种。一般的话直接调用就好了。

线程与进程

线程,是操作系统能够进行运算调度的最小单位。
进程,是对各种资源管理的集合。
进程就是一个执行中的程序。程序并不能单独运行,只有将程序装载到内存中,系统为它分配资源才能运行,而这种执行的程序就称之为进程。程序和进程的区别就在于:程序是指令的集合,它是进程运行的静态描述文本;进程是程序的一次执行活动,属于动态概念。
进程要进行运算,必须要先创建一个线程。因为进程不具备执行的动作,但是他包含线程,通过线程来进行运算。所有在同一个进程里的线程,是共享同一块内存空间的。

直接调用

import threading
import time

def task(num):
    print("running on task", num)
    time.sleep(3)
    print("task over", num)

if __name__ == '__main__':
    t1 = threading.Thread(target=task, args=(1,))  # 生成一个线程实例
    t2 = threading.Thread(target=task, args=(2,))  # 再生成一个实例
    t1.start()  # 启动线程
    t2.start()  # 再启动一个
    print(t1.getName())  # 获取线程名
    print(t2.getName())

参数注意:函数名和参数要分开写。并且参数要写成元组的形式,这里只有一个参数,所以也必须用括号括起来后面加个逗号,表示这是一个元组。

继承式调用

上面是直接实例化了 threading.Thread这个类,我们也可以像下面这样先继承这个类,然后重构它的run方法。

import threading
import time

class MyThread(threading.Thread):
    def __init__(self, num):
        super(MyThread, self).__init__()  # 继承父类的构造函数
        self.num = num
    def run(self):
        "每个线程要运行的函数,必须写到run方法里"
        print("running on task", self.num)
        time.sleep(3)
        print("task over", self.num)

if __name__ == '__main__':
    t1 = MyThread(1)
    t2 = MyThread(2)
    t1.start()  # 这里就会执行run方法
    t2.start()
    print(t1.getName())
    print(t2.getName())

在直接调用中,就是将你的函数名和运行参数,在实例化的时候,通过类的构造函数传递给了threading.Thread类的run方法。而这里我们是重构了这个run方法

使用for循环调用多个线程

如果需要一次调用多个线程,就不能再像上面那样一个一个写了。可以用一个for,来循环调用启动

import threading
import time

def task(num):
    print("running on task", num)
    time.sleep(3)
    print("task over", num)

if __name__ == '__main__':
    for i in range(50):
        t = threading.Thread(target=task, args=('t%s' % i,))
        t.start()
    print("全部运行结束?")  # 注意这句print执行的时间

程序主函数的线程

在上面的例子中,最后的print并没有等待之前的sleep运行结束,而是直接执行了。这里主函数是一个线程,其他使用threading启动的都是这个主线程启动的子线程。所有线程都是独立执行的,主线程启动了子线程之后两者就相互独立了,相互独立并行执行。
我们可以通过 threading.current_thread() 获取到当前的线程名:

import threading
import time

def task(num):
    print("running on task", num, threading.current_thread())
    time.sleep(3)
    print("task over", num)

if __name__ == '__main__':
    print("running on Main", threading.current_thread())
    for i in range(50):
        t = threading.Thread(target=task, args=('t%s' % i,))
        t.start()
    print("全部运行结束?")  # 注意这句print执行的时间

可以看到,主函数是MainThread。每一个线程都有线程名和线程号。

join方法

如果我们希望所有的子线程都是并行的,但是主函数需要等待所有子线程都执行完毕后再统一继续执行,就需要join方法。
join方法,是等待这个线程执行完毕后才会继续执行之后的语句。每一个线程都要有一个join,否则就不会等待这个线程执行完毕。

import threading
import time

def task(num):
    print("running on task", num)
    time.sleep(3)
    print("task over", num)

if __name__ == '__main__':
    t1 = threading.Thread(target=task, args=(1,))  # 生成一个线程实例
    t2 = threading.Thread(target=task, args=(2,))  # 再生成一个实例
    t1.start()  # 启动线程
    t2.start()  # 再启动一个
    t1.join()  # 为每个线程加一个join
    t2.join()
    print(t1.getName())
    print(t2.getName())

上面是2个子线程的情况,如果是之前那样的50个子线程,那么还需要再写一个for循环来执行join。这次可以来计算一下程序的运行时间。

import threading
import time

def task(num):
    print("running on task", num)
    time.sleep(3)
    print("task over", num)

if __name__ == '__main__':
    t_objs = []  # 先定义一个空列表
    start_time = time.time()
    for i in range(50):
        t = threading.Thread(target=task, args=('t%s' % i,))
        t.start()
        t_objs.append(t)  # 保存每一个实例,否则跳出当前for循环后无法调用
    for j in t_objs:
        j.join()
    print("总共运行时间:", time.time()-start_time)  # 计算总共的运行时间
    print("全部运行结束?")  # 注意这句print执行的时间

活动的线程个数

通过 threading.active_count() 可以获取到活动的线程个数

import threading
import time

def task(num):
    print("running on task", num)
    time.sleep(3)
    print("task over", num)

if __name__ == '__main__':
    print(threading.active_count())  # 运行前是1,因为有一个主函数的线程
    t_objs = []  # 先定义一个空列表
    start_time = time.time()
    for i in range(50):
        t = threading.Thread(target=task, args=('t%s' % i,))
        t.start()
        t_objs.append(t)  # 保存没一个实例,否则跳出当前for循环后无法调用
    print(threading.active_count())  # 所有子线程都起来了,一个50个,再加上一个主函数
    for j in t_objs:
        j.join()
    print(threading.active_count())  # 所有子线程都结束了,又只剩一个主函数了
    print("总共运行时间:", time.time()-start_time)  # 计算总共的运行时间

守护线程

之前的进程互相之间都是独立的,虽然有父线程创建子线程,但是创建完之后,这2个线程也就互相独立了。这个是默认的设置。
守护线程,在创建完线程还没有运行之前,可以将线程设置为守护线程。守护线程依赖主线程而存在,主线程一旦运行完毕,守护线程无论是什么情况,都会停止。
两者的差别就是,之前的情况,所有线程都是独立运行的。如果没有使用join,所有的线程包括主线程都是独立运行,当所有线程全部运行结束后,我们的程序才会结束。如果将线程设为守护线程后,那么当主线程和其他线程运行完之后,不会等待守护线程运行结束,程序会直接结束。

import threading
import time

def task(num):
    print("running on task", num)
    time.sleep(3)  # 设为守护线程后,不会等待守护线程运行结束了
    print("task over", num)  # 所以这里也不会打印了

if __name__ == '__main__':
    for i in range(50):
        t = threading.Thread(target=task, args=('t%s' % i,))
        # 将线程设置为守护线程,当主线程退出时,守护线程也会退出
        # 并且由这个守护线程启动的其它子线程也是守护线程,也会会同时退出,不管是否执行完任务
        t.setDaemon(True)  # 必须在创建线程后,但是在运行前才能将线程设置为守护线程
        t.start()
    print("运行结束,进程数量", threading.active_count())  # 这里不会等待子线程运行完毕

小结

线程类的方法:

  • start :线程准备就绪,等待CPU调度
  • setName :为线程设置名称
  • getName :获取线程名称
  • setDaemon:设置守护线程,设为True就是守护线程。默认False
  • join :逐个执行每个线程,执行完毕后继续往下执行
  • run :线程被cpu调度后自动执行线程对象的run方法
  • threading.current_thread : 查看当前的线程名
  • threading.active_count : 查看活动线程的数量

Python GIL

Python GIL(Global Interpreter Lock),全程解释器锁。无论你启多少个线程,你的cpu是多少核,Python在执行的时候只能是单核运行。这个是使用Python解析器(CPython)时会有的情况。Python除了(CPython)还可以通过CPython,PyPy,Psyco等不同的Python执行环境来执行,但是CPython是大部分环境下默认的Python执行环境。
所以我们用CPython就会有GIL,有GIL就不是真正的多线程,只能单核运行。我们还是会继续在CPython下学习和运行Python,GIL还是会继续存在。目前只要知道这么多就行了,怎么利用多核是下次的内容。

线程锁

这段上课演砸了,不过没关系,我大概搞明白了。
一个进程下可以启动多个线程,多个线程共享父进程的内存空间,也就意味着每个线程可以访问同一份数据,此时,如果2个线程同时要修改同一份数据,这是数据就混乱了。下面模拟没有线程锁造成数据混乱的情况。

import threading
import time

gl_num = 0
def show():
    global gl_num  # 声明全局变量
    gl_num += 1  # 先执行自增1,然后停顿一会
    # 把下面的sleep注释掉,可能是数据处理到输出之间几乎没有间隔,看不到数据混乱的情况
    time.sleep(0.1)  # sleep的这段时间,其他线程也会操作这个变量
    print(gl_num)  # 最后输出的时候,就是所有线程操作后的结果

for i in range(10):
    t = threading.Thread(target=show)
    t.start()
print("运行结束")

所以,出现了线程锁,同一时刻只允许一个线程修改数据。

import threading
import time

gl_num = 0
lock = threading.Lock()  # 申请一把锁,生成一个实例
def show():
    global gl_num
    lock.acquire()  # 修改数据前加锁,此时别的线程就无法操作了。
    gl_num += 1
    time.sleep(0.1)
    print(gl_num)
    lock.release()  # 上面打印出结果了,释放锁允许别的线程继续操作

for i in range(10):
    t = threading.Thread(target=show)
    t.start()
print("运行结束")

上面加了锁之间的内容其实就变成了串行执行了。

递归锁

在使用线程锁的时候,如果你需要用到多把锁嵌套使用,可能会导致程序锁死,永远无法release。下面演示一个会出现锁死的情况。说白了,就是大锁中还要再包含子锁。

import threading

def run1():
    print("grab the first part data")
    lock.acquire()
    global num
    num += 1
    lock.release()
    return num
def run2():
    print("grab the second part data")
    lock.acquire()
    global num2
    num2 += 1
    lock.release()
    return num2
def run3():
    lock.acquire()
    res = run1()
    print('--------between run1 and run2-----')
    res2 = run2()
    lock.release()
    print(res, res2)

if __name__ == '__main__':
    num, num2 = 0, 0
    # 下面使用了递归锁RLock,可以正常执行,如果换成之前的Lock,就会出现锁死的情况
    lock = threading.RLock()
    # 其实这里都不是线程数量的问题,起一个子线程就会锁死了
    for i in range(10):
        t = threading.Thread(target=run3)
        t.start()
while threading.active_count() != 1:  # 这里没用join,而是通过判断活跃线程数来确认子线程是否执行完毕
    print("活动线程数量:", threading.active_count())
else:
    print('----all threads done---')
    print(num, num2)

在上面的代码中,程序会先进入run3,run3中有第一道锁。然后在run3中会分别去执行run1和run2,而这里面又都会有第二道锁。此时可能是程序吧两道锁搞混了,所以导致再也release不出来了。使用RLock来代替Lock就避免了这种情况。
也没讲RLock和Lock的其他区别,既然RLock没问题,貌似不用Lock就好了。简单的场景都OK,复杂的场景下一定要用递归锁避免程序被锁死。

信号量

线程锁,同时只允许一个线程更改数据。
信号量(Semaphore),是同时允许一定数量的线程更改数据。比如厕所有3个坑,那最多只允许3个人上厕所,后面的人只能等里面有人出来了才能再进去。
信号量的使用就和线程锁一样,实例化的时候类名变了,多一个int参数。之后都是一样的acquire上锁和release释放。

import threading,time

# 一般应该是写到if里的,不过这里演示,从上到下按顺序执行逻辑比较清晰
semaphore  = threading.BoundedSemaphore(5)  # 最多允许5个线程同时运行
def run(n):
    semaphore.acquire()
    time.sleep(1)
    print("run the thread: %s" % n)
    semaphore.release()

if __name__ == '__main__':
    for i in range(30):
        t = threading.Thread(target=run, args=(i,))
        t.start()
    while threading.active_count() != 1:
        pass  # 继续用活动进程数量等待子程序结束
    else:
        print("所有进程执行完毕")

上面代码执行后的效果就是,每次只蹦5条结果出来。因为通过信号量限制了同一时间只允许一定数量的线程操作数据。
这里的代码比较简单,而且都是一样的,所以看上去是每次执行5个。实际是这里面每一个都是独立的。一旦有一个执行完释放了之后,就会让下一个继续执行。就是线程都是一个一个放行的,一旦一个执行完毕就放行下一个,而不是一批一批放行的。

定时器 timer

从线程被start方法调用开始,定时器开始计时。计时完毕后才会开始执行

import threading
def hello(name):
    print("你好,%s" % name)
t = threading.Timer(2, hello, args=("世界",))
t.start()  # 需要等待上面指定的秒数后才会真正执行

事件 event

事件(event),用于线程之间数据同步的。通过 event 来实现两个或多个线程间的交互。
下面把用于控制的线程称为服务端,被控制的线程称为客户端。
服务端,设置 event 的状态,只有 set 和 clear 两个方法
客户端,检查 event 的状态,如果是 set 就继续执行,否则就阻塞等待 set

  • event = threading.Event() :使用前,先生成一个实例
  • event.set() :服务端线程,将 event 设为 set ,客户端就可以继续执行
  • event.clear() :服务端线程,将 event 的 set 清除,客户端会阻塞直到再次设为 set
  • event.wait() :客户端线程,等待 event 变成 set ,如果 set 就继续,否则就阻塞直到 set
  • event.is_set() :布尔值,当前event的状态。客户端线程也可以用它来做控制。但是如果不是 set 和 clear时都需要执行的话,还是用 wait 来控制比较好。wait是用阻塞来控制的,而这里是每次都要做一下判断。

课上举例了一个红绿灯的例子,起一个红绿灯的线程,如果绿灯就 set ,红灯就 clear。然后可以起几个车的线程,判断event,只在set的时候执行:

import threading, time

event = threading.Event()  # 这句应该写到if __name__里面,先放这里看看清楚
def light():
    "模拟红绿灯"
    count = 0
    while True:
        if count < 10:
            event.set()  # 绿灯将标志位设为 set
            print("\033[42;1m**\033[0m绿灯", 10-count, 'event状态:', event.is_set())
        elif count < 20:
            event.clear()  # 红灯清除标志位
            print("\033[41;1m**\033[0m红灯", 20-count, 'event状态:', event.is_set())
        else:
            count = 0
            continue
        time.sleep(1)
        count += 1

def car(name):
    "在红灯的时候wait,绿灯的时候执行"
    while True:
        event.wait()  # 只有在绿灯的时候才会继续执行
        print(name, '正在行驶...')
        time.sleep(1)

if __name__ == '__main__':
    light1 = threading.Thread(target=light)
    light1.start()
    # 你也可以用for循环多起几个车
    car1 = threading.Thread(target=car, args=("特斯拉",))
    car1.start()
    car2 = threading.Thread(target=car, args=("保时捷",))
    car2.start()

虽然不用 event 也可以通过检查某个变量的状态来实现控制。但是由于变量是进程间所有线程共享的,客户端直接访问控制变量也可以修改它,虽然你程序里可能不会这么写,但不是不可以。这里使用了 event 将这个过程封装了,避免客户端直接访问这个变量。

queue模块-队列

队列,可以理解为一个有顺序的容器,里面存放数据。数据来的时候先将数据存入,使用数据的时候再按一定顺序从容器中取出。
其实列表也能实现,用pop方法。简单的话,还真的用列表就好了,不过模块封装了更多高级的设置。
有3种队列:

import queue
# 使用之前也是要先生成一个实例
q1 = queue.Queue()  # 先进先出
q2 = queue.LifoQueue()  # 后进先出,就是堆栈。last in first out (LIFO)
q3 = queue.PriorityQueue()  # 可以设置优先级的队列

上面再实例化的时候,都没有参数。有一个参数 maxsize=0 ,默认的队列大小是没有限制。可以设置 maxsize 来设置队列大小。

存取数据

put(item) :存入一个数据
get(item) :取出一个数据
put_nowait(item) :存入数据的另外一个方法,不等待,直接抛出异常
get_nowait(item) :取出数据的另外一个方法,不等待,直接抛出异常
可选的参数,用于 put 和 get 方法:
block=False :默认为True,就是阻塞模式。设为False,则直接抛出异常,下面的 timeout 也就无效了。
timeout=1 :默认为None,就是一直等着。设置后为阻塞多少秒,如果这段时间内可以继续了,就马上继续。否则还是抛出异常。
put在存入数据的时候,如果队列设置了大小,并且队列已满,就会阻塞。直到队列里有数据被取出空出了位置,那么再将这个数据存入继续。可以使用 put_nowait 存入,那么满的时候,就不直接抛出一个错误。
也可以设置参数 block=False 也是一样直接抛出错误,默认是True。
还可以设置参数 timeout=1 这个是等待的时间,比如这里等待1秒,如果1秒内队列空出来,就存入,否则还是抛出异常。如果 block 已经设置了 False 这个 timeout 就没有用了,会直接抛出错误。
get也是一样,用上面的两个参数控制,在队列为空的情况下继续要取数据,是阻塞,还是抛出异常,或者阻塞多久后再抛出异常。
下面的列子,生成2个线程,一个存入数据,一个用户控制来取出数据,演示put和get的用法:

import threading, queue

q1 = queue.Queue(maxsize=2)  # 队列大小只有2
def put_item():
    "存入数据的线程"
    for i in range(10):
        q1.put(i, timeout=2)  # 如果队列已满,最长等待2秒钟,否则抛出异常
        print("存入数据:", i)  # 存入数据的时候,你会看到
    print("数据已经全部存入...")
def control():
    "用户控制取出数据"
    while True:
        input("按回车取出一个数据,如果等待超过2秒会抛出异常... ")
        print("取出数据:", q1.get_nowait())  # 如果队列空了,再要取就抛出异常

if __name__ == '__main__':
    put1 = threading.Thread(target=put_item)
    put1.start()
    control1 = threading.Thread(target=control)
    control1.start()

其他方法

qsize() :队列中元素的个数,0就是空队列,等于maxsize就是队列满了
empty() :布尔值,队列是否为空。空的话就get不到数据了
full() :布尔值,队列是否已满。满的话就put不进数据了

import queue
q = queue.LifoQueue(3)  # 来一个堆栈,大小只有3
def show():
    "每次打印这3个数据"
    print('队列中的数据数量:', q.qsize(),
          '队列是否为空:', q.empty(),
          '队列是否满了', q.full())
show()  # 什么都还没有存入,大小是0,并且是空的
q.put(1)
show()  # 存入数据后,大小就变了,已经不是空堆栈了,但是还没满
q.put(2)
show()
q.put(3)
show()  # 现在已经满了
print(q.get())  # 看看取出数据的顺序,是不是最后存入的最先取出
print(q.get())
print(q.get())
show()  # 都取完了,现在又是一个空堆栈了

优先级队列

优先级队列例子1:

import queue

q1 = queue.PriorityQueue()

q1.put('Jack')
q1.put('Perter')
q1.put('Alice')
q1.put('1Zoey')

while not q1.empty():  # empty队列是否为空
    print(q1.get())

从上面可以看到,最后数据的结果给按ASCII顺序取出来的。一般可能不这么用,而是会单独设置一个优先级,那么就把优先级和数据已元组的形式存入
优先级队列例子2:

import queue

q1 = queue.PriorityQueue()

q1.put((10, 'Jack'))  # 注意数据只占1个参数位置,所以这里得2层括号,存入一个元组
q1.put((-1, 'Zoey'))
q1.put((10, 'Perter'))
q1.put((-1, 'Alice'))
q1.put((-1, 'Bob'))

while not q1.empty():
    print(q1.get())

重构put方法,优化排序的规则

这里还有一个问题,虽然将数据写成了元组,但是其实排序的时候是按整个元组的内容来排序的。但是我们需要的是按照元组的第一个元素来排序,而同样优先级的数据,仍然按照进入的顺序输出。貌似模块本身并没有提供这样的方法。我们重构一个自己的优先级排序。
源码中调用put方法,最终会调用_put方法将数据导入;
然后在_put方法中调用了heapq模块(import queque的时候导入的)的heappush方法将数据传递给_siftdown方法;
最终在_siftdown方法中重新排列存储数据的列表。问题就是这个方法,直接从列表中取出元素进行比较,而我们需要的是取出的元素是个元组,用元组的第一个元素进行比较。
所以只要修改上面3个方法就好了。

import queue

# 继承优先级排序的类,只重构我们需要修改的方法
class MyPriorityQueue(queue.PriorityQueue):
    "根据元组的第一个元素进行优先级排序"
    def _put(self, item):
        "源码是调用heapq模块里的方法,这里我们就在下面重构了,调用自己的类里的同名方法"
        self.heappush(self.queue, item)

    # 下面的2个方法是heapq模块里的,抄过来小改一下就好了
    def heappush(self, heap, item):
        "这里不用改实现的逻辑,让它继续调用我们下面的_siftdown方法就好了"
        heap.append(item)
        self._siftdown(heap, 0, len(heap) - 1)

    def _siftdown(self, heap, startpos, pos):
        """主要就是修改这里
        源码是从heap里取出数据进行比较:if newitem < parent:
        这里改成比较数据里的第一个元素:if newitem[0] < parent[0]:
        这里写的比较简单。如果存入的不是元素应该就会报错了,不过这不是重点了,需要可以再改
        """
        newitem = heap[pos]
        while pos > startpos:
            parentpos = (pos - 1) >> 1
            parent = heap[parentpos]
            if newitem[0] < parent[0]:
                heap[pos] = parent
                pos = parentpos
                continue
            break
        heap[pos] = newitem

q1 = MyPriorityQueue()
q1.put((10, 'Jack'))  # 注意数据只占1个参数位置,所以这里得2层括号,存入一个元组
q1.put((-1, 'Zoey'))
q1.put((10, 'Perter'))
q1.put((-1, 'Alice'))
q1.put((-1, 'Bob'))
while not q1.empty():
    print(q1.get())

上面好长一段,其实我只需要改一行,该怎么做最好呢?

task_done 方法和 join 方法

task_done :每次从队列中取出数据并且处理好之后,调用一下这个方法,来提示 join 方法,是否停止阻塞。
join :阻塞,直到队列清空,再继续向往后执行。
具体例子看下面的生产者消费者模型,举例一

生产者消费者模型

回顾,之前讲迭代器的时候,通过yield生成器实现过一个单线程下的有并行效果的吃包子的函数。看之前 的笔记:Python自动化开发学习4-2
现在,学习了多线程和队列之后,我们可以用多线程来实现了,把数据存放到队列之中。

概念

在并发编程中使用生产者和消费者模式能够解决绝大多数并发问题。该模式通过平衡生产线程和消费线程的工作能力来提高程序的整体处理数据的速度。
在线程里,生产者就是生产数据的线程,消费者就是消费数据的线程。在多线程开发当中,如果生产者处理速度很快,而消费者处理速度很慢,那么生产者就必须等待消费者处理完,才能继续生产数据。同样的道理,如果消费者的处理能力大于生产者,那么消费者就必须等待生产者。为了解决这个问题于是引入了生产者和消费者模式。
生产者消费者模式是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者之间不直接通信,而是通过队列来传递数据。所以生产者生产完数据之后不用等待消费者处理,直接放入队列;消费者不找生产者要数据,而是直接从队列里取,队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力,这样就实现了解耦。
用之前学习的多线程和队列的知识,就可以实现这样的模型了。

举例一

举例说明。写一个生产者的函数,生产包子。再写一个消费者的函数,消费包子。用上面的 task_done 和 join 的方法。
生产者一次生产10个包子,放入队列,然后执行 join 阻塞,等到10个包子全部被取完了,才会继续。
消费者取包子,不是直接找生产者,而且从队列里去取。取了之后执行一下 task_done 通知以下生产者看看包子有没有被取完。

import queue, threading, time

def producer():
    while True:
        for i in range(10):
            q.put("包子 %s" % i)
        print("已经放入了10个包子")
        q.join()  # 阻塞,知道包子被取完
        print("包子已经被取完了...")
def consumer(name):
    while True:
        # tmp = q.get()
        print("%s 吃了一个包子 %s" % (name, q.get()))
        q.task_done()  # 通知join方法,取了一个数据了
        time.sleep(1)

if __name__ == '__main__':
    q = queue.Queue()
    p = threading.Thread(target=producer,)
    p.start()
    c1 = threading.Thread(target=consumer, args=('Eric',))
    c1.start()
    c2 = threading.Thread(target=consumer, args=('Lassie',))
    c2.start()
    c3 = threading.Thread(target=consumer, args=('Snoopy',))
    c3.start()

上面的例子中生产数据爽哦速度远远高于消费数据的速度,我们用阻塞来控制,防止生产了过多的数据来不及消费

举例二

队列本身就有自己的阻塞模式,由于使用了生产者消费者模型实现了解耦。我们不必关心生产和消费数据的速度。如果数据消费的快,在取空数据的时候就会进入阻塞,直到生产者把数据加入队列。数据生产过快的情况也是一样,队列满了自然进入阻塞,直到消费者消费了。

import queue, threading, time

# 随意调整一次生产的数量,以及每次sleep的时间间隔
def producer():
    while True:
        for i in range(5):
            q.put("包子 %s" % i)
        print("已经放入了5个包子")
        time.sleep(2)

def consumer(name):
    while True:
        print("%s 吃了一个包子 %s" % (name, q.get()))
        time.sleep(1)

# 这里也是可以多开几个生产者和消费者
if __name__ == '__main__':
    q = queue.Queue(10)  # 这次设定一下队列的大小,如果生产过快,也会阻塞等待消费
    p = threading.Thread(target=producer,)
    p.start()
    c1 = threading.Thread(target=consumer, args=('Eric',))
    c1.start()
    c2 = threading.Thread(target=consumer, args=('Lassie',))
    c2.start()
    c3 = threading.Thread(target=consumer, args=('Snoopy',))
    c3.start()

作业

类 Fabric 主机管理程序开发:

  1. 运行程序列出主机组或者主机列表
  2. 选择指定主机或主机组
  3. 选择让主机或者主机组执行命令或者向其传输文件(上传/下载)
  4. 充分使用多线程或多进程
  5. 不同主机的用户名密码、端口可以不同

补充-线程池-concurrent.futures模块

上面讲线程的内容里只有信号量,并没有线程池。信号量只是限制同事运行的线程,但是所有线程应该是全部都创建好的。线程占用的资源比较少,这也没太大问题。不过如果要限制同一时间创建的线程的数量,就需要线程池。

从Python3.2开始,标准库为我们提供了concurrent.futures模块,它提供了ThreadPoolExecutor和ProcessPoolExecutor两个类,实现了对threading和multiprocessing的更高级的抽象,对编写线程池/进程池提供了直接的支持。

例子:

from concurrent.futures import ThreadPoolExecutor

def ssh_cmd(obj):
    pass

# 执行者:创建线程池
executor = ThreadPoolExecutor(5)
for obj in objs:
    executor.submit(ssh_cmd, obj)  # 第一个参数是方法,之后的参数都是变量
executor.shutdown(wait=True)

项目里只用到这么多。