使用Python实现一个简单的多线程

  1. import time
  2. import threading
  3. def coding():
  4. for i in range(3):
  5. print(i, "写代码...")
  6. time.sleep(1)
  7. def drawing():
  8. for i in range(3):
  9. print(i, "画画...")
  10. time.sleep(1)
  11. if __name__ == '__main__':
  12. # 指定target参数来指定线程运行时执行的函数
  13. th1 = threading.Thread(target=coding)
  14. th2 = threading.Thread(target=drawing)
  15. th1.start()
  16. th2.start()

继承threading.Thread类的方式实现线程

  • threading.current_thread():获取当前线程对象,在不同线程中执行所返回的线程对象自然也不同
  • threading.enumerate():获取执行时整个程序所有的线程
  1. import threading
  2. import time
  3. class coding(threading.Thread):
  4. # 重写run()方法,线程开始执行,从这个方法开始
  5. def run(self) -> None:
  6. # threading.current_thread()获取当前线程对象
  7. thd = threading.current_thread()
  8. print("当前线程的名字:", thd.name)
  9. for i in range(3):
  10. print("写代码...")
  11. time.sleep(1)
  12. if __name__ == '__main__':
  13. # 获取整个程序的所有线程
  14. thd1 = threading.enumerate()
  15. print(thd1)
  16. th1 = coding()
  17. th1.start()
  18. thd2 = threading.enumerate()
  19. print(thd2)

全局变量共享问题

在多线程中,资源共享的问题是一个常见的问题,解决资源互斥访问,一般采用上锁的机制进行解决。
Python使用锁时,应把尽量少何不耗时的代码放到锁中执行,代码执行完也要记得释放锁。例子如下:

  1. import threading
  2. value = 0
  3. # 创建一个锁
  4. lock = threading.Lock()
  5. def add_value():
  6. # 在函数中使用全局变量是,用global关键字进行说明
  7. global value
  8. # 上锁
  9. lock.acquire()
  10. for i in range(1000000):
  11. value += 1
  12. # 释放锁
  13. lock.release()
  14. print(value)
  15. if __name__ == '__main__':
  16. # 创建两个线程来执行
  17. for j in range(2):
  18. th = threading.Thread(target=add_value)
  19. th.start()

以上代码是使用Lock来解决了共享问题,但使用Lock也存在一定的不足,如生产者和消费者问题中,如果消费者准备消费了,进行上锁,但上锁完成后却发现资源不够,无法消费,导什么也没干就将锁释放掉,这样的过程其实空占了资源,造成了资源浪费,上锁也是一个很耗费CPU资源的行为。如果能在以上问题,在判断出无法消费时线程进入等待,主动释放资源,等待资源充足时被唤醒,再继续执行,这样就能提高程序的性能,而使用threading.Condition()就可以完成,其功能与threading.Lock()类似,这里介绍一下其常用函数:

  • acquire():上锁
  • release():释放锁
  • wait():将当前进程处于等待状态,并且会释放锁,被其它进程唤醒后会继续等待上锁,再继续执行
  • notify():通知某个正在等待的线程,默认是第一个等待的线程
  • notifyall():通知所有正在等待的线程(notify()和notifyall()不会释放锁,需要在release()之前调用)

    Queue线程安全队列

    在线程中,访问一些全局变量,加锁是一个经常的过程。如果是想把一些数据存储到某个队列中,就可以使用线程安全队列Queue,其满足了队列的“先进先出”原则,也有“后进先出”的LifoQueue特殊队列。这些队列都实现了锁原语,能够在多线程中直接使用,可以实现多线程之间的同步。

一下例子中展示几个基础的方法的使用说明:

  1. import queue
  2. # 新建一个队列,队列的最大容量为4
  3. q = queue.Queue(4)
  4. # qsize()反回队列的大小
  5. print(q.qsize())
  6. # empty()判断队列是否为空
  7. print(q.empty())
  8. # put()将一个数据放到队列中
  9. # 如果队列满了发生阻塞,直到队列中的数据被取走,再加入数据
  10. # 设置block=False关闭阻塞,队列满时再加抛出异常
  11. for i in range(4):
  12. q.put(i)
  13. try:
  14. q.put('已经满了再加', block=False)
  15. except queue.Full:
  16. print("队列满了,已接收异常")
  17. # full()判断队列是否满
  18. print(q.full())
  19. # get()从队列头取出一个数据
  20. # 如果队列已空,发生阻塞,直到队列中有数据后再取出数据
  21. # 设置block=False可以关闭阻塞,队列已空时再取抛出异常
  22. for i in range(5):
  23. try:
  24. value = q.get(block=False)
  25. except queue.Empty:
  26. print("队列已空")
  27. break
  28. print(value)

多线程简单实战示例

这里简单的爬取一下王者荣耀的高清壁纸,除了用到多线程,也综合使用一下安全队列

  1. from urllib import parse
  2. from urllib import request
  3. import urllib
  4. import requests
  5. import threading
  6. import queue
  7. import time
  8. # url_name的队列
  9. url_name = queue.Queue(20)
  10. # 请求的url
  11. url = 'https://apps.game.qq.com/cgi-bin/ams/module/ishow/V1.0/query/workList_inc.cgi?activityId=2735&sVerifyCode=ABCD&sDataType=JSON&iListNum=20&totalpage=0&page={}&iOrder=0&iSortNumClose=1&iAMSActivityId=51991&_everyRead=true&iTypeId=2&iFlowId=267733&iActId=2735&iModuleId=2735&_=1621844420680'
  12. # 请求头
  13. header = {
  14. 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:88.0) Gecko/20100101 Firefox/88.0'
  15. }
  16. # 解析出url和name线程
  17. class GET_URL_NAME(threading.Thread):
  18. def __init__(self, page_start, page_end):
  19. super(GET_URL_NAME, self).__init__()
  20. self.page_start = page_start
  21. self.page_end = page_end
  22. def run(self) -> None:
  23. for page in range(self.page_start, self.page_end):
  24. response = requests.get(url.format(page), headers=header)
  25. # 将请求的对象直接变为字典
  26. result = response.json()
  27. info_list = result["List"]
  28. for info in info_list:
  29. img_url = parse.unquote(info["sProdImgNo_8"]).replace('200', '0')
  30. img_name = parse.unquote(info["sProdName"]) + '.jpg'
  31. # print(threading.current_thread().name, img_name)
  32. # 用列表的形式将图片的url和名字放入队列
  33. url_name.put([img_name, img_url])
  34. # 下载图片线程
  35. class DOWN(threading.Thread):
  36. def run(self) -> None:
  37. # 暂停一秒,防止解析进程还未解析出数据导致下载进程直接结束
  38. time.sleep(1)
  39. while not url_name.empty():
  40. info = url_name.get()
  41. try:
  42. request.urlretrieve(info[1], 'images/' + info[0])
  43. print(threading.current_thread().name, info[0], ' 完成下载')
  44. except urllib.error.ContentTooShortError:
  45. print(info, ' 下载异常', threading.current_thread().name)
  46. if __name__ == '__main__':
  47. # 两个线程获取图片的url
  48. get_url1 = GET_URL_NAME(0, 10)
  49. get_url2 = GET_URL_NAME(10, 20)
  50. get_url1.start()
  51. get_url2.start()
  52. # 五个线程去下载图片
  53. for i in range(8):
  54. download = DOWN(name='下载{}号'.format(i))
  55. download.start()

多线程GIL锁

  1. Python自带的解释器是CPythonCPython解释器的多线程是一个假的多线程(在多核CPU中,只能利用一核,不能利用多核)。同一时间只有一个线程在执行,为了保证同一时刻只有一个线程在执行,在CPython解释器中有一个东西叫GILGlobal Interpreter Lock),叫全局解释器锁。
  • Jython:用Java实现的Python解释器。不存在GIL锁
  • IronPython:用.net实现的Python解释器。不存在GIL锁
  • PyPy:用Python实现的Python解释器,存在GIL锁

GIL虽然是一个假的多线程,但在处理IO操作上还是可以很大程度提高效率的。在IO操作上建议使用多线程提高效率,在一些CPU计算操作上不建议使用多线程,而使用多进程。


后记

本篇内容偏短,其实后面我是要写selenium在爬虫上怎么用的,但这个东西其实就是自动化的一个工具,暂时提不起兴趣去用它,所以先空着,后面有兴趣了再来补这一部分。