python 3.x 多线程编程

多线程编程

  • 操作系统能够切换调度的最小单元是线程,在最开始的时候操作系统调度的最小单元是进程,但是因为进程对系统的资源消耗非常大,所以说后期就演变出线程,线程是依赖于进程的,每个进程下有多个线程.
  • 对于IO操作来说,多线程和多进程性能差别不大.
    • 对于以IO操作为主的程序来说,多线程与多进程两者进行编程时候,实际上性能差别并不是很大.甚至有可能多线程比多进程性能还高,对操作系统来说,线程的调度比进程的调度更加轻量级的  
  • 多线程编程的第一种方式:
  • 通过Thread类来实例化(代码量比较少的情况下适用,特别是需要动态的创建一些Thread的话,或者说我们要用线程池的话情况下).
    • threading.Thread(target=函数名称, args=(参数1,参数2,参数3...))  
    • args接收tuple类型  

 

  • 定义两个函数,通过一个非常非常简单的爬虫来做一个说明.
    • 需求:  
      • 爬取网站伯乐在线,爬取具体的文章信息,最简单的方式是,用一个线程去爬取列表页,将爬取到的url,把url传递给另外一个线程中,另外一个线程就负责爬取详情页的数据,然后将它解析入库,这就是一个非常典型的多线程应用.    
    • 分析:  
      • 先爬取列表页然后再去爬取详情页,这样的话,他就是一个顺序的过程,他的性能肯定是没有多线程的性能高,因为我们多线程一个是爬列表页一个是爬详情页,两者几乎是同时进行的.为什么认为他几乎是同时进行的呢???    
      • 是因为如果使用多线程编程的话,我们写两个线程,对于GIL来说他在遇到IO的时候,GIL会切换出去,将GIL锁释放,交给另外的一个线程去运行,如果爬取列表页的线程,url访问属于socket编程,socket编程属于IO编程,
        所以说当请求列表页的时候,请求返回的时候,就将GIL释放,马上让另外一个线程运行.第一我们的线程当前在等待一个网络的返回,等待网络返回的同时我们另外一个线程(因为GIL已经切换出去了,也就是爬取详情页的线程)在开始运作,
        所以爬取详情页的线程利用爬取列表页线程等待网络返回的同时执行爬取详情页的线程,所以说他们是一个并发的操作,    
      • 如果我们不用多线程编程,直接使用串行运行的话.第一等待时间大家想象一下爬取列表页等待网络返回,然后再去爬取详情页再等待网络返回,实际上等待时间没有被合理的运用就被被白白的消耗了.所以说用爬虫来理解多线程实际上就非常的简单    

使用简单的代码来演示爬虫的多线程编程

 1 import time
 2 import threading
 3 
 4 
 5 def get_detail_url(url):
 6     """
 7     获取列表页中所有的详情页url
 8     """
 9     print("get detail url started!")
10     # 睡2秒,模拟网络请求,请求所有详情页的url
11     time.sleep(2)
12     print("get detail url end!")
13 
14 
15 def get_detail_html(url):
16     """
17     获取详情页内容
18     """
19     print("get detail html started!")
20     # 睡2秒,模拟网络请求,请求详情页的内容
21     time.sleep(2)
22     print("get detail html end!")
23 
24 
25 if __name__ == "__main__":
26     thread1 = threading.Thread(target=get_detail_url, args=("",))
27     thread2 = threading.Thread(target=get_detail_html, args=("",))
28 
29     start_time = time.time()
30 
31     thread1.start()
32     thread2.start()
33 
34     print("last time: {}".format(time.time() - start_time))
35 
36 
37 输出结果:
38     get detail url started!
39     get detail html started!
40     last time: 0.0009999275207519531
41     get detail html end!
42     get detail url end!

通过输出结果分析:

  • get detail url started!和get detail html started!说明两个线程都开始执行,但是时间差为什么几乎为0.0???
  • 正常按照我们的理解,他应该是如果两个线程并行,应该是2秒,为什么几乎是0.0呢???是因为我们在运行在文件的时候,在里面创建两个线程thread1和thread2,但实际上他有三个线程,还有一个线程就是主线程,就是这两个线程你运行起来了,
    在这个脚本(程序)里面运行其他的代码print("last time: {}".format(time.time() - start_time))是在主线程里面运行的.所以说一共有三个线程,通过debug中的左侧的frames栏中可以看到有三个线程MainThread,Thread-6,Thread-7.
  • 线程之间既然可以并行,就意味着Thread1,Thread2开始之后,主线程仍然可以继续往下执行,因为我们是并行的,既然是并行的话,就造成了在Thread1和Thread2开始sleep的时候print("last time: {}".format(time.time() - start_time))这行代码已经开始运行了.
    所以说我不要等待线程Thread1和Thread2执行完成,就可以直接执行print("last time: {}".format(time.time() - start_time))这行代码.

但是我们可以看一下另外一个问题就是虽然print("last time: {}".format(time.time() - start_time))没有等待Thread1和Thread2执行完就执行完成,但是我们的程序并没有退出,如果我们程序主线程退出的话,就会有一个问题就是不会打印get detail url end!和get detail html end!这两句话.

  • 主线程退出,进程就会退出,进程一但关闭了,后面的语句就不会执行.
  • 所以在这我们引出了两个需求:
    • 1.当主线程退出的时候,子线程kill掉,为了解决这个问题,需要用的Thread给我们提供另外一个函数setDaemon(Boolean类型)  
    •  
       1 import time
       2 import threading
       3 
       4 
       5 def get_detail_url(url):
       6     """
       7     获取列表页中所有的详情页url
       8     """
       9     print("get detail url started!")
      10     # 睡2秒,模拟网络请求,请求所有详情页的url
      11     time.sleep(2)
      12     print("get detail url end!")
      13 
      14 
      15 def get_detail_html(url):
      16     """
      17     获取详情页内容
      18     """
      19     print("get detail html started!")
      20     # 睡2秒,模拟网络请求,请求详情页的内容
      21     time.sleep(2)
      22     print("get detail html end!")
      23 
      24 
      25 if __name__ == "__main__":
      26     thread1 = threading.Thread(target=get_detail_url, args=("",))
      27     thread2 = threading.Thread(target=get_detail_html, args=("",))
      28 
      29     thread1.setDaemon(True)
      30     thread2.setDaemon(True)
      31 
      32     start_time = time.time()
      33 
      34     thread1.start()
      35     thread2.start()
      36 
      37     print("last time: {}".format(time.time() - start_time))
      38 
      39 
      40 输出结果:
      41     get detail url started!
      42     get detail html started!
      43     last time: 0.002000093460083008

      通过输出结果分析:
                     对Thread1和Thread2线程设置函数setDaemon(True),当主线程退出的时候,子线程kill掉. 执行完print("last time: {}".format(time.time() - start_time))语句后根本没有管Thread1和Thread2这两个线程是否完成.
             setDaemon(True)是什么意思呢???
                     将Thread1和Thread2两个线程设置成守护线程,守护线程意思就是说,当主线程退出之后,义无反顾的直接将守护线程也给关闭掉

如果将其中的一个子线程设置setDaemon(Boolean类型)函数,其他子线程不设置,会是一个什么情况???

 1 import time
 2 import threading
 3 
 4 
 5 def get_detail_url(url):
 6     """
 7     获取列表页中所有的详情页url
 8     """
 9     print("get detail url started!")
10     # 睡2秒,模拟网络请求,请求所有详情页的url
11     time.sleep(2)
12     print("get detail url end!")
13 
14 
15 def get_detail_html(url):
16     """
17     获取详情页内容
18     """
19     print("get detail html started!")
20     # 睡2秒,模拟网络请求,请求详情页的内容
21     time.sleep(2)
22     print("get detail html end!")
23 
24 
25 if __name__ == "__main__":
26     thread1 = threading.Thread(target=get_detail_url, args=("",))
27     thread2 = threading.Thread(target=get_detail_html, args=("",))
28 
29     # thread1.setDaemon(True)
30     thread2.setDaemon(True)
31 
32     start_time = time.time()
33 
34     thread1.start()
35     thread2.start()
36 
37     print("last time: {}".format(time.time() - start_time))
38 
39 
40 输出结果:
41     get detail url started!
42     get detail html started!
43     last time: 0.002000093460083008
44     get detail url end!
通过输出结果分析:
  • Thread1不是守护性线程,如果主线程想退出,就必须等待Thread1执行完成.Thread2是守护线程,那我把Thread1执行完成后,主线程肯定不管Thread2线程是否执行完毕就直接退出,所以才会有这样输出结果.

 

既然有守护线程,那么我们还有另外的一个需求

  • 就是我们不希望在thread1.start()和thread2.start()线程启动之后,就直接运行print("last time: {}".format(time.time() - start_time))这行代码.
    希望在thread1.start()和thread2.start()线程运行完成之后,再来执行print("last time: {}".format(time.time() - start_time))这行代码.
    而不是Thread1,Thread2,MainThread这三个线性并发运行.
  • 如何来等待这两个线程执行完成???
    • 使用Thread给我们提供的函数join(),这样的话,会阻塞在thread1.join()和thread2.join()这两个地方.
      等待Thread1和Thread2这两个线程执行完成,才会执行print("last time: {}".format(time.time() - start_time))  
  •  1 import time
     2 import threading
     3 
     4 
     5 def get_detail_url(url):
     6     """
     7     获取列表页中所有的详情页url
     8     """
     9     print("get detail url started!")
    10     # 睡2秒,模拟网络请求,请求所有详情页的url
    11     time.sleep(2)
    12     print("get detail url end!")
    13 
    14 
    15 def get_detail_html(url):
    16     """
    17     获取详情页内容
    18     """
    19     print("get detail html started!")
    20     # 睡4秒,模拟网络请求,请求详情页的内容
    21     time.sleep(4)
    22     print("get detail html end!")
    23 
    24 
    25 if __name__ == "__main__":
    26     thread1 = threading.Thread(target=get_detail_url, args=("",))
    27     thread2 = threading.Thread(target=get_detail_html, args=("",))
    28 
    29     # thread1.setDaemon(True)
    30     # thread2.setDaemon(True)
    31 
    32     start_time = time.time()
    33 
    34     thread1.start()
    35     thread2.start()
    36 
    37     thread1.join()
    38     thread2.join()
    39 
    40     print("last time: {}".format(time.time() - start_time))
    41 
    42 
    43 输出结果:
    44     get detail url started!
    45     get detail html started!
    46     get detail url end!
    47     get detail html end!
    48     last time: 4.0062291622161865

    通过输出结果分析:
                  根据输出结果我们可以看到,第一点last time总共是4秒,第二点print("last time: {}".format(time.time() - start_time))会等到前面的所有线程执行完成之后,才会执行.
                  所以说last time这里面有两个点:
                               第一点是last time会等到前面的所有线程执行完成后才会执行print("last time: {}".format(time.time() - start_time)).
                               第二点是线程整个运行时间是程序中的最大时间4秒( time.sleep(4) ),并不是两个线程执行时间的相加(2+4=6秒).
                  以上结果可以看到,实际上是一个并发的过程

综上:
       牢记setDaemon(Boolean类型)函数和join()函数的用法,实际上在我们编程的时候,很多情况下这两个函数对我们有很大的用处.

 

  • 多线程编程第二种方式:
    • 通过继承Thread类来完成多线程编程(代码量过大,或者逻辑复杂的情况下使用).  
    • 重写run()函数,在run()函数中写我们自己的逻辑.  
    • 在线程类中可以加入很多复杂的逻辑,实际上继承Thread类的编写方法实际上更加适用的.  
    • 当然在比较简单的情况下直接使用Thread对象来完成多线程编程,特别是需要动态的创建一些Thread的话,或者说我们要用线程池的情况下  
    •  1 import time
       2 import threading
       3 
       4 
       5 class GetDetailUrl(threading.Thread):
       6     def __init__(self, name):
       7         super().__init__(name=name)
       8 
       9     def run(self):
      10         print("get detail url started!")
      11         # 睡2秒,模拟网络请求,请求所有详情页的url
      12         time.sleep(2)
      13         print("get detail url end!")
      14 
      15 
      16 class GetDetailHtml(threading.Thread):
      17     def __init__(self, name):
      18         super().__init__(name=name)
      19 
      20     def run(self):
      21         print("get detail html started!")
      22         # 睡4秒,模拟网络请求,请求详情页的内容
      23         time.sleep(4)
      24         print("get detail html end!")
      25 
      26 
      27 if __name__ == "__main__":
      28     thread1 = GetDetailUrl("get detail url")
      29     thread2 = GetDetailHtml("get detail html")
      30 
      31     start_time = time.time()
      32 
      33     thread1.start()
      34     thread2.start()
      35 
      36     thread1.join()
      37     thread2.join()
      38 
      39     print("last time: {}".format(time.time() - start_time))
      40 
      41 
      42 输出结果:
      43     get detail url started!
      44     get detail html started!
      45     get detail url end!
      46     get detail html end!
      47     last time: 4.0012288093566895

       

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

********

posted on 2019-04-22 08:41  jaydenjune  阅读(50)  评论(0)    收藏  举报

导航