并发编程

程序、进程、线程的基本概念

  • 程序:由源代码组成的可执行文件
  • 进程:程序运行资源(内存资源)分配的最小单位;一个进程下可以有多个线程,但是至少有一个线程(主线程);进程之间相互独立,互不影响,资源也不共享
  • 线程:CPU调度的最小单位,必须依赖

    创建多线程的两种方式

    1. # 直接创建不使用多线程
    2. import time
    3. def download():
    4. print('开始下载')
    5. time.sleep(2)
    6. print('下载结束')
    7. if __name__ == '__main__':
    8. download()

    方法一

    target:指定的是需要执行多线程任务的函数名 args:可以传递元组形式的参数给执行的函数

  1. # 直接创建多线程
  2. # 步骤:
  3. # 1.创建线程
  4. # 2.启动线程
  5. import time, threading
  6. def download(filename):
  7. print(filename'开始下载')
  8. time.sleep(2)
  9. print(filename'下载结束')
  10. if __name__ == '__main__':
  11. # 使用循环创建3个线程
  12. for i in rang(3):
  13. # 创建线程
  14. t = threading.Tread(target=download, args=(i,))
  15. # 启动线程
  16. t.start()

方法二

  1. # 使用类创建多线程(推荐)
  2. # 步骤:
  3. # 1.自定义一个类并继承Tread类
  4. # 2.覆写run()方法
  5. import time, threading
  6. class MyThread(threading.Tread):
  7. # 如果需要传参则需要定义初始化方法
  8. def __init__(self, filename):
  9. # 处理父类的init
  10. # 方法一:
  11. # threading.Tread.__init__(self)
  12. # 方法二:
  13. super().__init__()# super()就相当于threading.Tread
  14. self.filename = filename
  15. # 覆写run方法
  16. def run(self):
  17. print(self.filename, '开始下载')
  18. time.sleep(2)
  19. print(self.filename, '下载结束')
  20. if __name__ == '__main__':
  21. # 使用循环创建3个线程
  22. for i in rang(3):
  23. # 实例化对象(创建线程)
  24. t = MyTread(i)# 传参需要定义初始化方法并处理父类的init
  25. # 启动线程,会自动执行run()方法
  26. t.start()

线程名称

主要应用于调错

  1. import threading
  2. class MyTread(threading.Tread):
  3. def run(self):
  4. # treading.current_thread:查看当前运行的线程
  5. # 输出结果为:<MyTread(Tread-1, started 4408)
  6. print('{}正在运行'.format(threading.current_thread()))
  7. # name属性查看线程的名称,默认为Tread-n
  8. print('{}正在运行'.format(self.name))
  9. if __name__ == '__main__':
  10. # 可以在实例化对象是传入name属性,修改线程名称
  11. t = MyTread(name = 'file{}'.format('新名称'))
  12. t.start()

互斥锁

线程之间共享全局变量的安全性问题

  1. import threading
  2. # 创建锁
  3. lock = threading.Lock()
  4. # 定义初始值,0为数值型,不可变类型
  5. value = 0
  6. # 定义加1函数
  7. def add_value():
  8. # 在函数中对全局变量中的不可变类型进行修改需要先global
  9. global value
  10. # 加锁(哪里有问题在哪里加锁)
  11. lock.acquire()
  12. for i in range(100000):
  13. value += 1
  14. # 释放锁
  15. lock.release()
  16. print(value)# 在不加锁的情况下此时得出的结果会出现问题
  17. if __name__ == '__main__':
  18. for i in range(2):
  19. t = threading.Thread(target=add_value)
  20. t.start()

注意:加锁之后必须释放锁,不然会形成死锁

生产者和消费者模式

生产者:获取数据
消费者:保存数据

队列

队列在爬虫中的使用:

  1. 将爬取的URL存放到队列中,每次生产者进行爬取时,会从队列中取出URL进行请求
  2. 将生产者爬取到的数据存放到队列中,消费者从队列中取出数据进行保存
  1. # 导入队列(先进先出)
  2. from queue import Queue
  3. # 创建Queue对象
  4. q = Queue(maxsize=4)# maxsize代表队列中的最大容量
  5. # put():向队列中添加元素
  6. q.put(1)
  7. q.put(2)
  8. q.put(3)
  9. q.put(4)
  10. # q.put(5)
  11. # 注意:如果队列满了,再添加数据,程序不报错,而是进入阻塞状态(等待从队列中取出元素)
  12. # get():取出队列中的元素
  13. print(q.get())
  14. print(q.get())
  15. print(q.get())
  16. print(q.get())
  17. # print(q.get())
  18. # 注意:如果队列空了,在获取数据,程序不报错,而是进入阻塞状态(等待箱队列中添加元素)
  19. # full():判断队列是否为满,返回布尔值
  20. # empty():判断队列是否为空,返回布尔值

线程池

Pool.map创建线程池

爬虫属于I/O(Input/Output,输入/输出)密集型程序所以使用多线程;涉及计算密集型程序需要使用多进程

multiprocessing下的dummy模块下的Pool类,用来实现线程池。这个线程池有一个map()方法,可以让线程池里面的所有线程都“同时”执行一个函数。

  1. from multiprocessing.dummy import Pool
  2. def calc(num):
  3. return num * num
  4. # 初始化含有3个线程的线程池
  5. pool = Pool(3)
  6. # 可迭代序列(列表、元组、集合、字典等)
  7. origin_num = [x for x in range(10)]
  8. # map(函数名, 可迭代序列)
  9. result = pool.map(calc, origin_num)
  10. print(f'计算0-9的平方分别为:{result}')
  11. pool.close()
  12. pool.join() # 这里要先关闭再JOIN。进程池中进程执行完后再关闭,如果注释,那么程序直接关闭