使用Python实现一个简单的多线程
import timeimport threadingdef 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 threadingimport timeclass 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 threadingvalue = 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# 新建一个队列,队列的最大容量为4q = 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("队列已空")breakprint(value)
多线程简单实战示例
这里简单的爬取一下王者荣耀的高清壁纸,除了用到多线程,也综合使用一下安全队列
from urllib import parsefrom urllib import requestimport urllibimport requestsimport threadingimport queueimport time# url_name的队列url_name = queue.Queue(20)# 请求的urlurl = '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_startself.page_end = page_enddef 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__':# 两个线程获取图片的urlget_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在爬虫上怎么用的,但这个东西其实就是自动化的一个工具,暂时提不起兴趣去用它,所以先空着,后面有兴趣了再来补这一部分。
