17-多线程

GIL

  • global interpreter lock 全局解释器锁
  • 作用
    • 保证只有一个线程执行
    • 因为有GIL的存在,多个线程也只会占用一个CPU
    • 进程会使用多个CPU

创建线程

  • 线程:主线程/分线程

  • 方法1

    • # 线程是CPU分配资源的基本单位。一个程序开始运行,就变成了一个进程,而一个进程相当于一个或者多个线程。当没有多线程编程时,一个进程也是一个主线程,当有多线程编程时,一个进程包含多个线程(包括主线程)。使用线程可以实现程序的并发。
      
      import _thread
      import time
      import threading
      
      # 第一种方式
      # 子线程/分线程
      def thread1(*args):
          print('线程1:', args)
          print('子线程', threading.current_thread().name)  # 子线程 Dummy-1
      
      def creat_thread1():
          # 当前线程名  # MainThread
          print('主线程', threading.current_thread().name)
          # 创建子线程
          # 守护线程:子线程会随着主线程的结束而结束
          # 主公就是主线程,忠臣就是子线程
          _thread.start_new_thread(thread1, ('韩信', '刘邦'))
      
          time.sleep(20)
      
      
      # 第二种方式
      def thread2(*args):
          print('子线程:', args)
          print('子线程:', threading.current_thread().name)  # 子线程: 线程1
      
      def create_thread2():
          t = threading.Thread(target=thread2, name='线程1', args=('李白', '杜甫'))
          t.start()  # 启动线程
          print('hello')
      
      
      # 第三种方式
      # 自定义线程
      # 继承
      class MyThread(threading.Thread):
          def __init__(self, url):
              super().__init__()
              self.url = url
      
              # 重写run方法:会自动调用,这个函数就是子线程的函数,类似target
          def run(self):
              print('子线程:', threading.current_thread().name)  # 子线程: Thread-1
      
      def create_thread3():
          t = MyThread('http://www.baidu.com')
          t.start()
      
      if __name__ == '__main__':
          # creat_thread1()
          # create_thread2()
          create_thread3()
      

多线程

  • 同步:在一个线程上执行

  • 异步:在不同的线程上执行,在异步的基础上加上t.join()(阻塞),即变成同步

    • # 等待所有线程全部执行完
      for t in t_list:
      t.join()  # 阻塞,等到最后一个线程结束
      
  • ​ Python的多线程是假的多线程,但是仍然会提高效率

  • 线程的其他属性和方法

    • print(t.name, t.getName()) # 当前线程的名字
    • print(t.isDaemon()) # 是否为守护线程
    • print(t.ident) # 线程号
    • print(t.is_alive()) # 线程是否在运行
    • print(threading.active_count()) # 正在运行的线程数量
    • print(threading.enumerate()) # 列举所有正在运行的线程

cpu密集型(计算密集型)&I/O密集型

  • 计算密集型任务由于主要消耗CPU资源,代码运行效率至关重要,C语言编写;

    • 计算密集型:建议使用多进程(多进程可以使用到多核)
  • IO密集型,涉及到网络、磁盘IO的任务都是IO密集型任务,这类任务的特点是CPU消耗很少,任务的大部时间都在等待IO操作完成,99%的时间花费在IO上,脚本语言是首选,C语言最差。

    • IO密集型:Input Output(一般是比较耗时的操作),建议使用多线程。
  • 多线程爬虫

    • # 使用多线程爬取深圳所有区的房源,并分别保存到单独的以区为文件名的html中,如: 南山区.html
      # (爬取每个区的第一页即可)
      # https://sz.lianjia.com/ershoufang/pg1/
      import re
      import threading
      import requests
      
      # 模拟浏览器
      headers = {
          "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36"
      }
      
      # 获取所有区
      def get_area():
          url = 'https://sz.lianjia.com/ershoufang/pg1/'
          # 获取网页数据
          response = requests.get(url, headers=headers)
          content = response.text
          # print(content)
          # 获取所有区
          pattern = r'data-role="ershoufang"(.*?)</div>'
          area_str = re.findall(pattern, content, re.S)[0]
          print(area_str)
      
          pattern2 = r'href="(.*?)"'
          area_url_list = re.findall(pattern2, area_str, re.S)
          # print(area_url_list)
          pattern3 = r'">(.*?)</a>'
          area_name_list = re.findall(pattern3, area_str, re.S)
          # print(area_name_list)
          return area_url_list, area_name_list
      
      # 子线程
      def get_data(url, name):
          # 每个区的url网址
          url = 'https://sz.lianjia.com' + url
          response = requests.get(url, headers=headers)
      
          with open(f'lianjia/{name}.html', 'wb') as fp:
              fp.write(response.content)  # 写入二进制
      
      if __name__ == '__main__':
          # 获取所有区
          area_url_list, area_name_list = get_area()
          for i in range(len(area_url_list)):
              name = area_name_list[i]
              url = area_url_list[i]
      
              t = threading.Thread(target=get_data, args=(url, name))  # 注意传入变量的顺序
              t.start()
      

线程冲突即线程锁

  • 线程冲突

    • n = 0
      def f():
          global n
          for _ in range(1000000):
              n += 1
          print(n)
          
      def create_thread():
          # 同时开启5个线程,同时访问同一个变量n
          for _ in range(5):
              t = threading.Thread(target=f)
              t.start()
      
      create_thread()
      
    • 多个线程同时操作一个资源时,可能造成资源混乱

  • 线程锁/互斥锁

    • 解决线程冲突

      • 自动加锁,用完后自动解锁,加锁过程种其他线程无法访问

      • 对资源加锁

        • import threading
          # 线程冲突:多个线程同时操作一个资源时,可能造成资源混乱
          # 线程锁/互斥锁:解决线程冲突
          
          # 死锁现象:多个线程同时操作多个资源时
          # 比如:
          # 线程1   线程2
          #   A       B
          # 递归锁/重用锁:解决死锁
          
          n = 0
          def f():
              global n
              for _ in range(1000000):
                  n += 1
              print(n)
          
          # 线程冲突
          def create_thread():
              # 同时开启5个线程,同时访问同一个变量n
              for _ in range(5):
                  t = threading.Thread(target=f)
                  t.start()
                  # t.join()  # 同步
          
          # 线程锁:解决线程冲突
          lock = threading.Lock()
          # 递归锁:解决死锁
          # rlock = threading.RLock()
          
          
          def f2():
              # 自动加锁,用完后会自动解锁
              # 加锁过程种其他线程无法访问
              # with lock:
              # with rlock:  # 递归锁的使用
              #     global n
              #     for _ in range(1000000):
              #         n += 1
              #     print(n)
          
              lock.acquire()  # 加锁
              global n
              for _ in range(1000000):
                  n += 1
              print(n)
          
              lock.release()  # 解锁
          
          # 线程冲突
          def create_thread2():
              # 同时开启5个线程,同时访问同一个变量n
              for _ in range(5):
                  t = threading.Thread(target=f2)
                  t.start()
          
          if __name__ == '__main__':
              # 线程冲突
              # create_thread()
              # 线程锁
              create_thread2()
          

信号量

  • # 信号量
    import random
    import threading
    
    # 信号量:控制线程的最大并发数,每次最多5个线程同时执行
    import time
    
    sem = threading.Semaphore(5)
    
    def fn(*args):
        with sem:
            print('子线程:', args)
            time.sleep(3+random.random())
    
    if __name__ == '__main__':
        for i in range(1, 21):
            threading.Thread(target=fn, args=(f'Thread-{i}',)).start()
    
原文地址:https://www.cnblogs.com/lotuslaw/p/14022916.html