python 3.x 线程间通信

线程间通信

  • 线程间为什么需要通信???通过爬虫的示例代码进行理解线程间的通信.
  • 现在有两个线程,这两个线程必须要合作才能够完成爬取文章的详情页的,Thread1与Thread2两者之间如何协作呢???
    get_detail_url()函数爬取文章的列表页,然后并解析出其中的文章详情页的url,将url交给get_detail_html()函数.实际上这两个线程就需要线程间的通信,只有完成线程间的通信,才能爬取文章详情页的数据.
  • 通过使用Thread对象来创建线程,来完成线程间的通信.

线程间的通信方式:

  1. 共享变量,就是在代码中声明一个全局变量,然后将这个全局变量在各个线程中使用.
    共享变量存在很多问题,最直观的就是共享变量线程安全性是不安全的,因为GIL特性导致共享变量中的数据不是我们预期的数据,是因为这些操作不是线程安全的操作,
    所以说为了达到预期的效果,我们必须将这些操作上加一把锁.让它线程之间可以按照我们预定的顺序进行同步,这就是共享变量带来的最大的问题,
    所以说共享变量这种方式是实际上并不推荐大家去用作我们的通信,除非大家对这个锁足够的了解
  2. 推荐一种线程间的通信方法就能摆脱这种情况,就是通过queue的方法来完成线程间的通信.`

 

通过queue的方式进行线程间同步

  • Queue的内部实现使用了Condition.
  • 使用消息队列Queue来实现
  • Queue本身就是一个线程安全的,对Queue进行get()操作的时候,比如多个线程从Queue中取数据的时候,Queue不会使得我们多个线性去操作同一个Queue的时候,造成数据的错误.
  • Queue是如何做到线程安全???
    • 通过查看Queue的源码,get()和put()两个方法是线程安全的操作.  
    • 通过get()方法来看一下,get()内部实现是用到了线程同步锁的机制,通过锁做了很多的判断.但是在执行item = self._get()这行语句时,_get()方法并没有加锁.  
      • _get()方法的实现:    def _get(self):
                                                       return self.queue.popleft()
  • 直接使用self.queue,而self.queue = deque().deque()是python中的双端队列,关键是双端队列是线程安全的.
    deque()是在字节码的级别就已经达到线程安全.所以说内部的Queue,也就是线程里面的Queue,实际上内部还是使用了deque()
    .因为deque()本身是线程安全的,所以说Queue直接使用双端队列.不再需要在_get()方法上面加锁了.这样的话,Queue的性能也会比较高
  • Queue里面的方法全都是线程安全的:
    • put(self, item, block=True, timeout=None) ---> 往队列放数据  
      • block: 是否阻塞  
      • block为false时,如果消息队列有空间就直接把数据放进去,如果没有空间的话,直接抛出空异常    
      • timeout: 如果我们的队列已满,put()方法就会内阻塞,put()方法就会一直等待,但是在某些情况下,并不希望我们一直等待下去,这样会将我们整个程序block(阻塞)住,所以说我们可以通过设置timeout值,这个timeout结束之前对列还没有位置的话,抛出空异常
    • get(self, block=True, timeout=None) ---> 从队列取数据  
      • block: 是否阻塞    
      • get()方法是阻塞的方法,如果队列queue为空,会一直停在queue.get()这行代码上,符合我们的预期,然后取到之后,后面就可以执行后面的代码    
    • put_nowait(self,item)  
      • 在往队列里面放数据的时候,我们没有必要等待到他完成后在返回,put_nowait()是一个异步的方法,他的内部实现使用了put()方法    
    • get_nowait(self)  
    • qsize() 获取Queue队列的长度  
    • empty() 判断Queue队列是否为空  
    • full() 判断Queue队列是否已满,如果队列已满,put()方法会被阻塞,等到队列有空闲空间为止,才能够成功执行代码  
    • join(),task_done() 
      • 一直block住,直到我们的queue接受到一个task_done这么一个信号,join()方法从Queue的角度可以让我们主线程阻塞.    
      • join()函数想退出,我们必须要在某一个地方,给这个Queue发一个task_done,join()方法才会退出,不然的话,join()是一直不会退出的,只有join()函数退出,我们的主线程才能退出.    
      • 正是因为join()和task_done()是成对的,所以说在某些时候,我们就可以在爬虫里面加一些逻辑,    
      • 比如说我爬完1000条数据,我就把我整个爬虫给停掉,于是我就调用task_done()函数,我们主线程就能退出.    
  • Queue给我们提供了很多的方便,能够让我们更好的控制多线程,所以说在编程的时候,我们更加建议大家首选用Queue,去完成我们的线程间的一个通信
  • PriorityQueue优先级队列

    • 如果希望后插入的数据,能够尽快被获取到,就可以优先考虑使用优先级队列  

 

示例模拟伪代码(共享变量)

 1 import threading
 2 import time
 3 
 4 detail_url_list = []
 5 
 6 
 7 def get_detail_url():
 8     """
 9     爬取文章列表页
10     """
11     global detail_url_list
12     print("get detail url started!")
13     # 网络请求数据
14     time.sleep(4)
15     # 假如第一次爬取20条url
16     for i in range(20):
17         detail_url_list.append("http://projectsedu.com/{id}".format(id=i))
18     print("get detail url end!")
19 
20 
21 def get_detail_html():
22     """
23     爬取文章详情页
24     """
25     global detail_url_list
26     while True:
27         if len(detail_url_list):
28             # 将列表中的某个元素移除并返回,默认移除列表最后一个元素,如果列表为空抛出IndexError
29             url = detail_url_list.pop()
30             print("get detail html started!")
31             time.sleep(2)
32             print("get detail html end!")
33 
34 
35 if __name__ == "__main__":
36     thread_detail_url = threading.Thread(target=get_detail_url)
37 
38     start_time = time.time()
39 
40     thread_detail_url.start()
41 
42     for i in range(10):
43         thread_detail_html = threading.Thread(target=get_detail_html)
44         thread_detail_html.start()
45 
46     # thread_detail_url.join()
47     # thread_detail_html.join()
48 
49     print("last time: {}".format(time.time() - start_time))
50 
51 """
52 输出结果:
53     get detail url started!
54     Exception in thread Thread-2:
55     Traceback (most recent call last):
56       File "D:\python3\lib\threading.py", line 916, in _bootstrap_inner
57         self.run()
58       File "D:\python3\lib\threading.py", line 864, in run
59         self._target(*self._args, **self._kwargs)
60       File "F:/C/PythonProject/PythonHighLevelAndIo/test/chapter11/python_queue_01.py", line 48, in get_detail_html
61         url = detail_url_list.pop()
62     IndexError: pop from empty list
63     
64     get detail url end!
65     last time: 4.0012288093566895
66 """

 

 

示例模拟伪代码(Queue)

 1 from queue import Queue
 2 import time
 3 import threading
 4 
 5 
 6 def get_detail_url(queue):
 7     """
 8     爬取文章列表页
 9     """
10     print("get detail url started!")
11     time.sleep(4)
12     # 假如第一次爬取20条url
13     for i in range(20):
14         queue.put("http://projectsedu.com/{id}".format(id=i))
15     print("get detail url end!")
16 
17 
18 def get_detail_html(queue):
19     """
20     爬取文章详情页
21     """
22     while True:
23         # get()方法是阻塞的方法,如果队列queue为空,会一直停在queue.get()这行代码上.
24         url = queue.get()
25         print("get detail html started!")
26         time.sleep(2)
27         print("get detail html end!")
28 
29 
30 if __name__ == "__main__":
31     # 设置最大值为1000,允许消息队列最大有1000个消息.
32     # 因为如果Queue过大的话,实际上对内存会有很大的影响.
33     detail_url_queue = Queue(maxsize=1000)
34 
35     thread_detail_url = threading.Thread(target=get_detail_url, args=(detail_url_queue,))
36 
37     start_time = time.time()
38     thread_detail_url.start()
39 
40     for i in range(10):
41         thread_detail_html = threading.Thread(target=get_detail_html, args=(detail_url_queue,))
42         thread_detail_html.start()
43 
44     # 必须给Queue发送一个task_done,
45     detail_url_queue.task_done()
46     detail_url_queue.join()
47 
48     print("last time: {}".format(time.time() - start_time))

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

********

posted on 2019-04-24 10:49  jaydenjune  阅读(53)  评论(0)    收藏  举报

导航