使用Python实现一个简单的多线程
import time
import threading
def coding():
for i in range(3):
print(i, "写代码...")
time.sleep(1)
def drawing():
for i in range(3):
print(i, "画画...")
time.sleep(1)
if __name__ == '__main__':
# 指定target参数来指定线程运行时执行的函数
th1 = threading.Thread(target=coding)
th2 = threading.Thread(target=drawing)
th1.start()
th2.start()
继承threading.Thread类的方式实现线程
- threading.current_thread():获取当前线程对象,在不同线程中执行所返回的线程对象自然也不同
- threading.enumerate():获取执行时整个程序所有的线程
import threading
import time
class coding(threading.Thread):
# 重写run()方法,线程开始执行,从这个方法开始
def run(self) -> None:
# threading.current_thread()获取当前线程对象
thd = threading.current_thread()
print("当前线程的名字:", thd.name)
for i in range(3):
print("写代码...")
time.sleep(1)
if __name__ == '__main__':
# 获取整个程序的所有线程
thd1 = threading.enumerate()
print(thd1)
th1 = coding()
th1.start()
thd2 = threading.enumerate()
print(thd2)
全局变量共享问题
在多线程中,资源共享的问题是一个常见的问题,解决资源互斥访问,一般采用上锁的机制进行解决。
Python使用锁时,应把尽量少何不耗时的代码放到锁中执行,代码执行完也要记得释放锁。例子如下:
import threading
value = 0
# 创建一个锁
lock = threading.Lock()
def add_value():
# 在函数中使用全局变量是,用global关键字进行说明
global value
# 上锁
lock.acquire()
for i in range(1000000):
value += 1
# 释放锁
lock.release()
print(value)
if __name__ == '__main__':
# 创建两个线程来执行
for j in range(2):
th = threading.Thread(target=add_value)
th.start()
以上代码是使用Lock来解决了共享问题,但使用Lock也存在一定的不足,如生产者和消费者问题中,如果消费者准备消费了,进行上锁,但上锁完成后却发现资源不够,无法消费,导什么也没干就将锁释放掉,这样的过程其实空占了资源,造成了资源浪费,上锁也是一个很耗费CPU资源的行为。如果能在以上问题,在判断出无法消费时线程进入等待,主动释放资源,等待资源充足时被唤醒,再继续执行,这样就能提高程序的性能,而使用threading.Condition()就可以完成,其功能与threading.Lock()类似,这里介绍一下其常用函数:
- acquire():上锁
- release():释放锁
- wait():将当前进程处于等待状态,并且会释放锁,被其它进程唤醒后会继续等待上锁,再继续执行
- notify():通知某个正在等待的线程,默认是第一个等待的线程
- notifyall():通知所有正在等待的线程(notify()和notifyall()不会释放锁,需要在release()之前调用)
Queue线程安全队列
在线程中,访问一些全局变量,加锁是一个经常的过程。如果是想把一些数据存储到某个队列中,就可以使用线程安全队列Queue,其满足了队列的“先进先出”原则,也有“后进先出”的LifoQueue特殊队列。这些队列都实现了锁原语,能够在多线程中直接使用,可以实现多线程之间的同步。
一下例子中展示几个基础的方法的使用说明:
import queue
# 新建一个队列,队列的最大容量为4
q = queue.Queue(4)
# qsize()反回队列的大小
print(q.qsize())
# empty()判断队列是否为空
print(q.empty())
# put()将一个数据放到队列中
# 如果队列满了发生阻塞,直到队列中的数据被取走,再加入数据
# 设置block=False关闭阻塞,队列满时再加抛出异常
for i in range(4):
q.put(i)
try:
q.put('已经满了再加', block=False)
except queue.Full:
print("队列满了,已接收异常")
# full()判断队列是否满
print(q.full())
# get()从队列头取出一个数据
# 如果队列已空,发生阻塞,直到队列中有数据后再取出数据
# 设置block=False可以关闭阻塞,队列已空时再取抛出异常
for i in range(5):
try:
value = q.get(block=False)
except queue.Empty:
print("队列已空")
break
print(value)
多线程简单实战示例
这里简单的爬取一下王者荣耀的高清壁纸,除了用到多线程,也综合使用一下安全队列
from urllib import parse
from urllib import request
import urllib
import requests
import threading
import queue
import time
# url_name的队列
url_name = queue.Queue(20)
# 请求的url
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'
# 请求头
header = {
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:88.0) Gecko/20100101 Firefox/88.0'
}
# 解析出url和name线程
class GET_URL_NAME(threading.Thread):
def __init__(self, page_start, page_end):
super(GET_URL_NAME, self).__init__()
self.page_start = page_start
self.page_end = page_end
def run(self) -> None:
for page in range(self.page_start, self.page_end):
response = requests.get(url.format(page), headers=header)
# 将请求的对象直接变为字典
result = response.json()
info_list = result["List"]
for info in info_list:
img_url = parse.unquote(info["sProdImgNo_8"]).replace('200', '0')
img_name = parse.unquote(info["sProdName"]) + '.jpg'
# print(threading.current_thread().name, img_name)
# 用列表的形式将图片的url和名字放入队列
url_name.put([img_name, img_url])
# 下载图片线程
class DOWN(threading.Thread):
def run(self) -> None:
# 暂停一秒,防止解析进程还未解析出数据导致下载进程直接结束
time.sleep(1)
while not url_name.empty():
info = url_name.get()
try:
request.urlretrieve(info[1], 'images/' + info[0])
print(threading.current_thread().name, info[0], ' 完成下载')
except urllib.error.ContentTooShortError:
print(info, ' 下载异常', threading.current_thread().name)
if __name__ == '__main__':
# 两个线程获取图片的url
get_url1 = GET_URL_NAME(0, 10)
get_url2 = GET_URL_NAME(10, 20)
get_url1.start()
get_url2.start()
# 五个线程去下载图片
for i in range(8):
download = DOWN(name='下载{}号'.format(i))
download.start()
多线程GIL锁
Python自带的解释器是CPython。CPython解释器的多线程是一个假的多线程(在多核CPU中,只能利用一核,不能利用多核)。同一时间只有一个线程在执行,为了保证同一时刻只有一个线程在执行,在CPython解释器中有一个东西叫GIL(Global Interpreter Lock),叫全局解释器锁。
- Jython:用Java实现的Python解释器。不存在GIL锁
- IronPython:用.net实现的Python解释器。不存在GIL锁
- PyPy:用Python实现的Python解释器,存在GIL锁
GIL虽然是一个假的多线程,但在处理IO操作上还是可以很大程度提高效率的。在IO操作上建议使用多线程提高效率,在一些CPU计算操作上不建议使用多线程,而使用多进程。
后记
本篇内容偏短,其实后面我是要写selenium在爬虫上怎么用的,但这个东西其实就是自动化的一个工具,暂时提不起兴趣去用它,所以先空着,后面有兴趣了再来补这一部分。