python线程通信方式_python线程通信与生产者消费者模式

本文主要讲解生产者消费者模式,它基于线程之间的通信。

生产者消费者模式是指一部分程序用于生产数据,一部分程序用于处理数据,两部分分别放在两个线程中来运行。

举几个例子一个程序专门往列表中添加数字,另一个程序专门提取数字进行处理,二者共同维护这样一个列表

一个程序去抓取待爬取的url,另一个程序专门解析url将数据存储到文件中,这相当于维护一个url队列

维护ip池,一个程序在消耗ip进行爬虫,另一个程序看ip不够用了就启动开始抓取

我们可以想象到,这种情况不使用并发机制(如多线程)是难以实现的。

如果程序线性运行,只能做到先把所有url抓取到列表中,再遍历列表解析数据;或者解析的过程中将新抓到的url加入列表,但是列表的增添和删减并不是同时发生的。对于更复杂的机制,线程程序更是难以做到,比如维护url列表,当列表长度大于100时停止填入,小于50时再启动开始填入。

本文结构

本文思路如下首先,两个线程维护同一个列表,需要使用锁保证对资源修改时不会出错

threading模块提供了Condition对象专门处理生产者消费者问题

但是为了呈现由浅入深的过程,我们先用普通锁来实现这个过程,通过考虑程序的不足,再使用Condition来解决,让读者更清楚Condition的用处

下一步,python中的queue模块封装了Condition的特性,为我们提供了一个方便易用的队列结构。用queue可以让我们不需要了解锁是如何设置的细节

线程安全的概念解释

这个过程其实就是线程之间的通信,除了Condition,再补充一种通信方式Event

本文分为下面几个部分Lock与Condition的对比

生产者与消费者的相互等待

Queue

线程安全

Event

Lock与Condition的对比

下面我们实现这样一个过程维护一个整数列表integer_list,共有两个线程

Producer类对应一个线程,功能:随机产生一个整数,加入整数列表之中

Consumer类对应一个线程,功能:从整数列表中pop掉一个整数

通过time.sleep来表示两个线程运行速度,设置成Producer产生的速度没有Consumer消耗的快

代码如下

import time

import threading

import random

class Producer(threading.Thread):

# 产生随机数,将其加入整数列表

def __init__(self, lock, integer_list):

threading.Thread.__init__(self)

self.lock = lock

self.integer_list = integer_list

def run(self):

while True: # 一直尝试获得锁来添加整数

random_integer = random.randint(0, 100)

with self.lock:

self.integer_list.append(random_integer)

print('integer list add integer{}'.format(random_integer))

time.sleep(1.2 * random.random()) # sleep随机时间,通过乘1.2来减慢生产的速度

class Consumer(threading.Thread):

def __init__(self, lock, integer_list):

threading.Thread.__init__(self)

self.lock = lock

self.integer_list = integer_list

def run(self):

while True: # 一直尝试去消耗整数

with self.lock:

if self.integer_list: # 只有列表中有元素才pop

integer = self.integer_list.pop()

print('integer list lose integer{}'.format(integer))

time.sleep(random.random())

else:

print('there is no integer in the list')

def main():

integer_list = []

lock = threading.Lock()

th1 = Producer(lock, integer_list)

th2 = Consumer(lock, integer_list)

th1.start()

th2.start()

if __name__ == '__main__':

main()

程序会无休止地运行下去,一个产生,另一个消耗,截取前面一部分运行结果如下

integer list add integer 100

integer list lose integer 100

there is no integer in the list

there is no integer in the list

... 几百行一样的 ...

there is no integer in the list

integer list add integer 81

integer list lose integer 81

there is no integer in the list

there is no integer in the list

there is no integer in the list

......

我们可以看到,整数每次产生都会被迅速消耗掉,消费者没有东西可以处理,但是依然不停地询问是否有东西可以处理(while True),这样不断地询问会比较浪费CPU等资源(特别是询问之后不只是print而是加入计算等)。

如果可以在第一次查询到列表为空的时候就开始等待,直到列表不为空(收到通知而不是一遍一遍地查询),资源开销就可以节省很多。Condition对象就可以解决这个问题,它与一般锁的区别在于,除了可以acquire release,还多了两个方法wait notify,下面我们来看一下上面过程如何用Condition来实现

import time

import threading

import random

class Producer(threading.Thread):

def __init__(self, condition, integer_list):

threading.Thread.__init__(self)

self.condition = condition

self.integer_list = integer_list

def run(self):

while True:

random_integer = random.randint(0, 100)

with self.condition:

self.integer_list.append(random_integer)

print('integer list add integer{}'.format(random_integer))

self.condition.notify()

time.sleep(1.2 * random.random())

class Consumer(threading.Thread):

def __init__(self, condition, integer_list):

threading.Thread.__init__(self)

self.condition = condition

self.integer_list = integer_list

def run(self):

while True:

with self.condition:

if self.integer_list:

integer = self.integer_list.pop()

print('integer list lose integer{}'.format(integer))

time.sleep(random.random())

else:

print('there is no integer in the list')

self.condition.wait()

def main():

integer_list = []

condition = threading.Condition()

th1 = Producer(condition, integer_list)

th2 = Consumer(condition, integer_list)

th1.start()

th2.start()

if __name__ == '__main__':

main()

相比于Lock,Condition只有两个变化在生产出整数时notify通知wait的线程可以继续了

消费者查询到列表为空时调用wait等待通知(notify)

这样结果就井然有序

integer list add integer 7

integer list lose integer 7

there is no integer in the list

integer list add integer 98

integer list lose integer 98

there is no integer in the list

integer list add integer 84

integer list lose integer 84

.....

生产者与消费者的相互等待

上面是最基本的使用,下面我们多实现一个功能:生产者一次产生三个数,在列表数量大于5的时候停止生产,小于4的时候再开始

import time

import threading

import random

class Producer(threading.Thread):

def __init__(self, condition, integer_list):

threading.Thread.__init__(self)

self.condition = condition

self.integer_list = integer_list

def run(self):

while True:

with self.condition:

if len(self.integer_list) > 5:

print('Producer start waiting')

self.condition.wait()

else:

for _ in range(3):

self.integer_list.append(random.randint(0, 100))

print('now{}after add '.format(self.integer_list))

self.condition.notify()

time.sleep(random.random())

class Consumer(threading.Thread):

def __init__(self, condition, integer_list):

threading.Thread.__init__(self)

self.condition = condition

self.integer_list = integer_list

def run(self):

while True:

with self.condition:

if self.integer_list:

integer = self.integer_list.pop()

print('all{}lose{}'.format(self.integer_list, integer))

time.sleep(random.random())

if len(self.integer_list) < 4:

self.condition.notify()

print("Producer don't need to wait")

else:

print('there is no integer in the list')

self.condition.wait()

def main():

integer_list = []

condition = threading.Condition()

th1 = Producer(condition, integer_list)

th2 = Consumer(condition, integer_list)

th1.start()

th2.start()

if __name__ == '__main__':

main()

可以看下面的结果体会消长过程

now [33, 94, 68] after add

all [33, 94] lose 68

Producer don't need to wait

now [33, 94, 53, 4, 95] after add

all [33, 94, 53, 4] lose 95

all [33, 94, 53] lose 4

Producer don't need to wait

now [33, 94, 53, 27, 36, 42] after add

all [33, 94, 53, 27, 36] lose 42

all [33, 94, 53, 27] lose 36

all [33, 94, 53] lose 27

Producer don't need to wait

now [33, 94, 53, 79, 30, 22] after add

all [33, 94, 53, 79, 30] lose 22

all [33, 94, 53, 79] lose 30

now [33, 94, 53, 79, 60, 17, 34] after add

all [33, 94, 53, 79, 60, 17] lose 34

all [33, 94, 53, 79, 60] lose 17

now [33, 94, 53, 79, 60, 70, 76, 21] after add

all [33, 94, 53, 79, 60, 70, 76] lose 21

Producer start waiting

all [33, 94, 53, 79, 60, 70] lose 76

all [33, 94, 53, 79, 60] lose 70

all [33, 94, 53, 79] lose 60

all [33, 94, 53] lose 79

Producer don't need to wait

all [33, 94] lose 53

Producer don't need to wait

all [33] lose 94

Producer don't need to wait

all [] lose 33

Producer don't need to wait

there is no integer in the list

now [16, 67, 23] after add

all [16, 67] lose 23

Producer don't need to wait

now [16, 67, 49, 62, 50] after add

Queue

queue模块内部实现了Condition,我们可以非常方便地使用生产者消费者模式

import time

import threading

import random

from queue import Queue

class Producer(threading.Thread):

def __init__(self, queue):

threading.Thread.__init__(self)

self.queue = queue

def run(self):

while True:

random_integer = random.randint(0, 100)

self.queue.put(random_integer)

print('add{}'.format(random_integer))

time.sleep(random.random())

class Consumer(threading.Thread):

def __init__(self, queue):

threading.Thread.__init__(self)

self.queue = queue

def run(self):

while True:

get_integer = self.queue.get()

print('lose{}'.format(get_integer))

time.sleep(random.random())

def main():

queue = Queue()

th1 = Producer(queue)

th2 = Consumer(queue)

th1.start()

th2.start()

if __name__ == '__main__':

main()

Queue中get方法会移除并赋值(相当于list中的pop),但是它在队列为空的时候会被阻塞(wait)

put方法是往里面添加值

如果想设置队列最大长度,初始化时这样做queue = Queue(10)指定最大长度,超过这个长度就会被阻塞(wait)

使用Queue,全程不需要显式地调用锁,非常简单易用。不过内置的queue有一个缺点在于不是可迭代对象,不能对它循环也不能查看其中的值,可以通过构造一个新的类来实现,详见这里。

下面消防之前Condition方法,用Queue实现生产者一次加3个,消费者一次消耗1个,每次都返回当前队列内容,改写代码如下

import time

import threading

import random

from queue import Queue

# 为了能查看队列数据,继承Queue定义一个类

class ListQueue(Queue):

def _init(self, maxsize):

self.maxsize = maxsize

self.queue = [] # 将数据存储方式改为list

def _put(self, item):

self.queue.append(item)

def _get(self):

return self.queue.pop()

class Producer(threading.Thread):

def __init__(self, myqueue):

threading.Thread.__init__(self)

self.myqueue = myqueue

def run(self):

while True:

for _ in range(3): # 一个线程加入3个,注意:条件锁时上在了put上而不是整个循环上

self.myqueue.put(random.randint(0, 100))

print('now{}after add '.format(self.myqueue.queue))

time.sleep(random.random())

class Consumer(threading.Thread):

def __init__(self, myqueue):

threading.Thread.__init__(self)

self.myqueue = myqueue

def run(self):

while True:

get_integer = self.myqueue.get()

print('lose{}'.format(get_integer), 'now total', self.myqueue.queue)

time.sleep(random.random())

def main():

queue = ListQueue(5)

th1 = Producer(queue)

th2 = Consumer(queue)

th1.start()

th2.start()

if __name__ == '__main__':

main()

得到结果如下

now [79, 39, 64] after add

lose 64 now total [79, 39]

now [79, 39, 9, 42, 14] after add

lose 14 now total [79, 39, 9, 42]

lose 42 now total [79, 39, 9]

lose 27 now total [79, 39, 9, 78]

now [79, 39, 9, 78, 30] after add

lose 30 now total [79, 39, 9, 78]

lose 21 now total [79, 39, 9, 78]

lose 100 now total [79, 39, 9, 78]

now [79, 39, 9, 78, 90] after add

lose 90 now total [79, 39, 9, 78]

lose 72 now total [79, 39, 9, 78]

lose 5 now total [79, 39, 9, 78]

上面限制队列最大为5,有以下细节需要注意首先ListQueue类的构造:因为Queue类的源代码中,put是调用了_put,get调用_get,_init也是一样,所以我们重写这三个方法就将数据存储的类型和存取方式改变了。而其他部分锁的设计都没有变,也可以正常使用。改变之后我们就可以通过调用self.myqueue.queue来访问这个列表数据

输出结果很怪异,并不是我们想要的。这是因为Queue类的源代码中,如果队列数量达到了maxsize,则put的操作wait,而put一次插入一个元素,所以经常插入一个等一次,循环无法一次运行完,而print是在插入三个之后才有的,所以很多时候其实加进去值了却没有在运行结果中显示,所以结果看起来比较怪异。

其实这里也可以直接使用Queue,只是输出的就不是list了,而是deque对象,不过目的也是可以达到的。像这里这样改写Queue多是将数据类型改为Set,以此来实现去重操作。

另外,queue模块中有其他类,分别实现先进先出、先进后出、优先级等队列,还有一些异常等,可以参考这篇文章和官网。

线程安全

讲到了Queue就提一提线程安全。线程安全其实就可以理解成线程同步。

官方定义是:指某个函数、函数库在多线程环境中被调用时,能够正确地处理多个线程之间的共享变量,使程序功能正确完成。

我们常常提到的说法是,某某某是线程安全的,比如queue.Queue是线程安全的,而list不是。

根本原因在于前者实现了锁原语,而后者没有。

原语指由若干个机器指令构成的完成某种特定功能的一段程序,具有不可分割性;即原语的执行必须是连续的,在执行过程中不允许被中断。

queue.Queue是线程安全的,即指对他进行写入和提取的操作不会被中断而导致错误,这也是在实现生产者消费者模式时,使用List就要特意去加锁,而用这个队列就不用的原因。

Event

Event与Condition的区别在于:Condition = Event + Lock,所以Event非常简单,只是一个没有带锁的Condition,也是满足一定条件等待或者执行,这里不想说很多,只举一个简单的例子来看一下

import threading

import time

class MyThread(threading.Thread):

def __init__(self, event):

threading.Thread.__init__(self)

self.event = event

def run(self):

print('first')

self.event.wait()

print('after wait')

event = threading.Event()

MyThread(event).start()

print('before set')

time.sleep(1)

event.set()

可以看到结果

first

before set

先出现,1s之后才出现

after wait

专栏信息

专栏目录:目录

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值