#线程中生产者消费者模型

生产者消费者模型

为什么queue方法是线程安全的

Queue模块提供了一个适用于多线程编程的先进先出数据结构,可以用来安全的传递多线程信息。

它本身就是线程安全的,使用put和get来处理数据,不会产生对一个数据同时读写的问题,所以是安全的。

也就是说在put或者get的时候,就有锁在里面的,也就不必自己繁琐的进行lock.acquire()和 lock.release来释放锁,

或者Rlock.acquire()和Rlock.release进行上锁解锁来保护数据,而且加锁会消耗资源,因为加锁或者释放锁的过程中都会消耗资源 。关于锁的详情:https://blog.csdn.net/weixin_43097301/article/details/84400264

Queue中的部分源码展示

class Queue:
    '''Create a queue object with a given maximum size.

    If maxsize is <= 0, the queue size is infinite.
    '''

    def __init__(self, maxsize=0):
        self.maxsize = maxsize
        self._init(maxsize)

        # mutex must be held whenever the queue is mutating.  All methods
        # that acquire mutex must release it before returning.  mutex
        # is shared between the three conditions, so acquiring and
        # releasing the conditions also acquires and releases mutex.
        self.mutex = threading.Lock()
        
self._init(maxsize):使用了python自带的双端队列deque,来存储元素。

self.mutex互斥锁:任何获取队列的状态(empty(),qsize()),
或者修改队列的内容的操作(get,put等)都必须持有该互斥锁。共有两种操作require获取锁,release释放锁。
同时该互斥锁被三个共享变量同时享有,即操作conditiond时的require和release操作也就是操作了该互斥锁。

self.not_full条件变量:当队列中有元素添加后,会通知notify其他等待添加元素的线程,唤醒等待require互斥锁,
或者有线程从队列中取出一个元素后,通知其它线程唤醒以等待require互斥锁。

self.not empty条件变量:线程添加数据到队列中后,会调用self.not_empty.notify()通知其它线程,
唤醒等待require互斥锁后,读取队列。

self.all_tasks_done条件变量:消费者线程从队列中get到任务后,任务处理完成,
当所有的队列中的任务处理完成后,会使调用queue.join()的线程返回,表示队列中任务以处理完毕
基础queue用法展示
  1. 说明:

    最后一个例子演示了生产者-消费者模型这个场景。在这个场景下,商品或服务的生产者 生产商品,然后将其放到类似队列的数据结构中。生产商品的时间是不确定的,同样消费者 消费生产者生产的商品的时间也是不确定的。

  2. 代码示例:

    1. 一个生产者不断的放入数据
    2. 一个消费者不断的取消费数据
    3. 他们两个是独立运行的,生产者只管像生产线中去放入数据,消费者只管向生产线中去取出数据消费
    4. queue的用法
      1. 使用queue.Queue()去初始化一个普通的队列,其中可以去设置队列的最大值
      2. 使用put去向队列中加入数据,当加入的数据满了之后,put方法就会阻塞
      3. 使用get方法去获取队列中的值,如果队列中没有值,那么get方法就会阻塞
        (阻塞并不会占用系统资源)
	# 两个队列,一个生产者不断的去队列里面存放任务,一个消费者,不断的取队列里面消费任务
		import time							#时间库
		import random							#随机数库
		import queue							#线程安全的队列
		import threading						#多线程		
		
		
		def product_task_to_queue(queue):										
		    '''
		    生产者:
		    不断的像队列中去加入任务,每隔1,3秒加入一次
		    :return:
		    '''
		    task_num = 1
		    while True:
		        print('product task', str(task_num),'task_size',queue.qsize())
		        queue.put(str(task_num))             	#向队列添加数据
		        sleep_time = random.randint(1, 2)		#随机1-2秒的时间
		        time.sleep(sleep_time)					#随机休息
		        task_num += 1						  #自我加一,记录数据
		
		
		def consume_task_from_queue(queue):
		    """
		    消费者:
		    不断的向队列中获取任务,然后消费任务,每隔2-4秒消费一次
		    :param queue:
		    :return:
		    """
		    while True:								#循环进行拿取任务
		        task_num = queue.get()
		        sleep_time = random.randint(3, 4)					
		        time.sleep(sleep_time)				#随机进行睡眠3-4秒
		        consume_task(task_num,queue)		#将任务交付给处理函数进行处理
		
		
		def consume_task(task_num,queue):
		    """
		    消费者处理任务的函数
		    :param task_num:
		    :return:
		    """
		    print('consume task num:', task_num,'task_size',queue.qsize())#打印数据
		
		
		def loop():
			"""
		   启动函数
		    :param task_num:
		    :return:
		    """
		    que = queue.Queue(maxsize=10)			#添加queue队列,限制任务上限为10
		    t1 = threading.Thread(target=product_task_to_queue,args=(que,)) #设置线程1
		    t2 = threading.Thread(target=consume_task_from_queue, args=(que,))#设置线程2
		
		
		    t1.start()						#启动线程1
		    t2.start()						#启动线程2
		    t1.join()							#阻塞主线程
		    t2.join()							#阻塞主线程
		
		loop()																	
爬虫用法
  1. 一个程序不断的向队列中去写入数据
  2. 另一个程序不断的向队列中去读取数据
import requests  # 页面请求
from bs4 import BeautifulSoup  # 网页解析
from queue import Queue  # 安全队列
from threading import Thread  # 多线程


def get_html_doc(url):  # 根据指定的url获取html文档
    res = requests.get(url)  # 向url链接发送get请求
    print(res.content)  # 打印拿到的网页内容
    return res.content.decode("utf8")  # 返回内容并用utf8解码


def get_detail(que):
    while True:
        url = que.get()
        Thread(target=get_html_doc, args=(url,))


def parse_index(url, que, index_urls_list):
    # 解析列表页面
    html_doc = get_html_doc(url)  # 拿到返回的网页内容
    data = BeautifulSoup(html_doc)  # 解析网页内容

    detail_urls = data.select('[class=post-thumb] a')  # 把index里面的url取出来再取下面的url
    # data.select调用css选择器 选择出来是dict
    for i in detail_urls:
        que.put(i)  # 获取细节的url,把细节的url添加到que队列中

    index_urls = data.select('a[class=page-numbers]')  # 取出所有其他index页面的翻页url 去解析其他的url
    for i in index_urls:
        if i not in index_urls_list:  # 保证url不重复,
            index_urls_list.append(i)  # 添加到list里面,表示我已经处理了这个数据
            url = i['href']  # 获取详细url
            Thread(target=parse_index, args=(url, que))  # 多线程处理,启动函数parse_index,并传参


def start(url, que, index_urls_list):  # 开启
    parse_index(url, que, index_urls_list)


if __name__ == "__main__":
    url = "http://blog.jobbole.com/category/it-tech/"  # 爬取的网页地址
    que = Queue()  # 建立安全队列que
    index_urls_list = []

    start(url, que, index_urls_list)  # 调用开启函数

  1. queue的方式进行同步:

    1. threading自带的queue结构可以在线程中去共享变量,并且queue的数据结构中有锁机制,不会引发多线程的gil字节码的问题
    2. 我们去优化之前的爬虫代码,使他有多线程的机制,我们先做一个简单的队列,只取一个列表页,然后向queue中写入url,然后另一个专门获取详情页的线程去获取详情页面
import requests
	from bs4 import BeautifulSoup
	from queue import Queue
	from threading import Thread
	import time
	
	
	def get_html_doc(url):					 # 根据指定的url获取html文档
	    res = requests.get(url)				#请求网页
	    print(res.content)					#打印信息bytes(b“内容”)
	    return res.content.decode("utf8")		#返回页面信息并utf8解码
	
	
	def get_detail(detail_urls_queue):		#循环处理拿到任务,并建个任务url传给函数获取页面
	    while True:
	        url = detail_urls_queue.get(1,timeout=2)    #到安全队列中取值,每次取值间隔2s
	        print('consumer--->',url)
	        get_html_doc(url)			    #传参url给函数进行获取页面
	
	
	def parse_index(url, detail_urls_queue):      # 解析列表页面
	    html_doc = get_html_doc(url)				#获取解码后的页面信息
	    data = BeautifulSoup(html_doc)				#解析所得页面信息

    # 把index里面的url取出来再取下面的url
    # data.select调用css选择器 选择出来是dict
   		detail_urls = data.select('[class=post-thumb] a')
    # 获取细节的url,把细节的url交给其他线程处理
	    for i in detail_urls:
	        url = i['href']						#字典取值,key value键值对取值 url
	        print('productor------>',url)
	        detail_urls_queue.put(url)			#添加到安全队列,即添加任务
       
		if __name__ == "__main__":
		    url = "http://blog.jobbole.com/category/it-tech/"
		    # 详细页面的url
		    detail_urls = Queue(100)
		   	 	# 列表url 防止重复
		   	 	# index_urls_list = []   #省略建一个列表用来去重复url
		    t1 = Thread(target=parse_index,args=(url,detail_urls))	#新建线程1,启动parse_index 
		    t2 = Thread(target=get_detail, args=(detail_urls,))	#新建线程2,启动get_detail
		    t1.start()
		    t2.start()
		    # parse_index(url, detail_urls, index_urls_list)
		    t1.join()
		    t2.join()
		    print('down')
  1. 完善的多线程获取页面方式

    1. 说明:
      1. 把列表页的url加入
      2. 列表页也不断的去循环列表页的url
import requests
	from bs4 import BeautifulSoup
	from queue import Queue
	from threading import Thread,RLock
	
	
	def get_html_doc(url):
	    # 根据指定的url获取html文档
	    res = requests.get(url)
	    # print(res.content)
	    return res.content.decode("utf8")
	
	
	def get_detail(detail_urls_queue):
	    while True:
	        url = detail_urls_queue.get(1, timeout=2)
	        print('consumer--->', url)
	        get_html_doc(url)
	
	
	def parse_index(detail_urls_queue, index_urls_queue,index_urls_queue_record):
	    while True:
	        url = index_urls_queue.get(1)				#请求页面信息
	        print('get_index_url--->', url)
	        # 解析列表页面
	        html_doc = get_html_doc(url)
	        data = BeautifulSoup(html_doc)					#解析页面
	
	        # 把index里面的url取出来再取下面的url
	        # data.select调用css选择器 选择出来是dict
	        detail_urls = data.select('[class=post-thumb] a')
	        # 获取细节的url,把细节的url交给其他线程处理
	        for i in detail_urls:
	            url = i['href']
	            print('productor------>', url)
	            detail_urls_queue.put(url)
	
	        # 取出所有其他index页面的翻页url 去解析其他的url
	        index_urls = data.select('a[class=page-numbers]')
	        for i in index_urls:
	            if i in index_urls_queue_record:			#去重复操作
	                continue
	            url = i['href']
	            index_urls_queue.put(url)					#添加入安全队列中
	            #在添加的时候,不要让gil去释放锁
	            locks.acquire()							#上锁
	            index_urls_queue_record.append(url)
	            locks.release()							#释放锁
	            print('put_index_url--->', url)
	            # 去重 使用redis数据库
	
	
	if __name__ == "__main__":
	    url = "http://blog.jobbole.com/category/it-tech/"
	    # 详细页面的url
       detail_urls = Queue(40)
       index_urls_queue = Queue(5)
	    index_urls_queue.put(url)
	    #记录重复url
	    index_urls_queue_record = [url]
	    locks = RLock()			#设置Rlock锁,防止多线程数据存储读取错误
	    		# 列表url 防止重复
	   			 # index_urls_list = []  #也可以添加一个list用来去重,这里没有进行演示
	    t1 = Thread(target=parse_index, args=(detail_urls, index_urls_queue,index_urls_queue_record,locks))
	    t2 = Thread(target=get_detail, args=(detail_urls,))
	    t1.start()
	    t2.start()
	    t1.join()
	    t2.join()
	    print('down')

线程池

简单的示例

给出一个任务,然后交给线程池完成,线程池可以设置最大线程的数量,所以他会一次执行三个


from concurrent.futures import ThreadPoolExecutor
import time

#简单的线程池使用
def consume(num):
    time.sleep(2)
    print('consuming',num)

pools = ThreadPoolExecutor(3)			#设置线程数3

num = 1
while True:

    time.sleep(0.5)
    pools.submit(consume,(num))		#启动线程池,运行函数consume
    num += 1


  1. 说明:

    1. 什么是线程池,顾名思义就是首先把多个线程放入一个池子中(内存),当有剩余的线程的时候,我们就把线程取出来使用,如果没有剩余的线程,程序就会等待线程
    2. 使用线程池我们可以获取到任务的返回结果
  2. 基本使用方式:

    1. 继承 concurrent.futures库来实现多线程库
    2. max_workers表示线程池中最大有多少个线程
    3. submit表示把任务提交给线程池
    4. done方法可以查看任务是否完成(bool)
    5. result 方式会阻塞程序,等待线程完成,并且获取函数的返回结果
    6. cancel方式能够取消线程,但是只能取消没有提交上去的线程
    
    from concurrent.futures import ThreadPoolExecutor
    import time
    					# 1.并发
    					# 2.获取线程的返回值 当一个线程完成的时候,主线程能够知道
    					# 3.让多线程和多进程编程接口一致
    def get_html(sleep_time):
        time.sleep(sleep_time)
        print("get page {} success".format(sleep_time))
        return sleep_time
    
    
    executor = ThreadPoolExecutor(max_workers=2)
    
    task1 = executor.submit(get_html,(3))  	#通过sumbit提交到线程池中,间隔三秒
    task2 = executor.submit(get_html,(2))		#间隔2秒
    task3 = executor.submit(get_html,(2))		#间隔2秒
    
    print(task3.cancel())
    
    #print(task1.done())   # done 用于判断线程是否完成,完成返回True,否则返回False
    
    print(task1.result())# result会阻塞 等待任务完成,获取返回结果
    print(task2.result())
    
    
    
  3. as_completed 方法

    使用 as_completed方法来获取所有完成的线程,并获取返回值

    
    from concurrent.futures import ThreadPoolExecutor, as_completed
    import time
    import random
    from functools import partial
    
    def get_html(sleep_time,num):
        time.sleep(sleep_time)
        # print("get page {} success".format(sleep_time))
        return num
    
    
    executor = ThreadPoolExecutor(max_workers=2)			#线程池上限任务两个
    			
    tasks = list()
    for i in range(10):
        sleep_time = random.randint(2, 5)				#睡眠时间随机2-5秒
        #把右边函数看成一个整体
        tasks.append(executor.submit(partial(get_html,sleep_time), (i)))
       				 # 通过sumbit提交到线程池中,进行一个添加get_html,
    
    for i in as_completed(tasks):
        data = i.result()     	#阻塞 等待完成的函数
    
        print('num {} success'.format(data))
    	
    
  4. wait 阻塞主线程

    wait可以等待tasks的某个任务或者所有任务完成之后再执行其他的(阻塞),
    他有三种可选方式,默认是等待所有tasks完成

    1. 等待所有完成
    2. 等待第一个完成
    3. 等待第一个错误
    
    FIRST_COMPLETED = 'FIRST_COMPLETED'					#一个完成
    FIRST_EXCEPTION = 'FIRST_EXCEPTION'					#一个异常
    ALL_COMPLETED = 'ALL_COMPLETED'						#所有完成
    
    
    from concurrent.futures import ThreadPoolExecutor, as_completed, wait
    import time
    import random
    
    
    def get_html(sleep_time):
        time.sleep(sleep_time)
        # print("get page {} success".format(sleep_time))
        return sleep_time
    
    
    executor = ThreadPoolExecutor(max_workers=2)
    # 通过sumbit提交到线程池中
    tasks = list()
    for i in range(10):
        sleep_time = random.randint(2, 5)
        tasks.append(executor.submit(get_html, (sleep_time)))
        			#添加get_html(sleep_time)函数的返回值sleep_time
    
    
    wait(tasks, return_when='FIRST_COMPLETED') #阻塞等待任务完成
    
    for i in as_completed(tasks):
        data = i.result()
    
        print('num {} success'.format(data))
    
    print('12312312')	
    
    
  5. 示例:

    通过上面的线程池,我们就可以把获取详细页面的任务交给线程池去获取

    
    import requests
    from bs4 import BeautifulSoup
    from queue import Queue
    from threading import Thread
    from concurrent.futures import ThreadPoolExecutor
    
    
    def get_html_doc(url):    					# 根据指定的url获取html文档				
        res = requests.get(url)
        print('ex--->url',url)
        return res.content.decode("utf8")
    
    
    def get_detail(detail_urls_queue,pools):	#从队列中拿取url任务,并进行传参进行页面请求
        while True:
            url = detail_urls_queue.get(1,timeout=2)
            # print('consumer--->',url)
            pools.submit(get_html_doc,(url))
    
    
    def parse_index(detail_urls_queue,index_urls_queue):
    										#解析页面,并找出标签,取出标签中的url,最后添加到安全队列任务中
        while True:
            url = index_urls_queue.get(1)
            # print('get_index_url--->',url)
            # 解析列表页面
            html_doc = get_html_doc(url)
            data = BeautifulSoup(html_doc)
    
            							# 把index里面的url取出来再取下面的url
            							# data.select调用css选择器 选择出来是dict
            detail_urls = data.select('[class=post-thumb] a')
            							# 获取细节的url,把细节的url交给其他线程处理
            for i in detail_urls:
                url = i['href']			#字典取值,或得详细url
                # print('productor------>',url)
                detail_urls_queue.put(url)		#添加到安全队列任务中
    
    
    
           								 # 取出所有其他index页面的翻页url 去解析其他的url
            index_urls = data.select('a[class=page-numbers]')
            for i in index_urls:
                url = i['href']				#字典取值,获取详细url
                index_urls_queue.put(url)		#添加到安全队列,即添加任务
                									#去重 使用redis数据库
                # print('put_index_url--->', url)		#测试
    
    
    if __name__ == "__main__":
        url = "http://blog.jobbole.com/category/it-tech/"
        # 详细页面的url
        detail_urls = Queue()
        index_urls_queue = Queue()		#建立安全队列
        index_urls_queue.put(url)		#添加安全队列
        # 列表url 防止重复
        # index_urls_list = []
        executor = ThreadPoolExecutor(max_workers=10)
        t1 = Thread(target=parse_index,args=(detail_urls,index_urls_queue)) #设置线程
        t2 = Thread(target=get_detail, args=(detail_urls,executor))		#设置线程
        t1.start()			#启动线程
        t2.start()			#启动线程
        t1.join()				#阻塞主线程
        t2.join()				#阻塞主线程
        print('down')
    
    
    

你可能感兴趣的:(Python)