本文的文字及图片来源于网络,仅供学习、交流使用,不具有任何商业用途,版权归原作者所有,如有问题请及时联系我们以作处理
以下文章来源于Python知识学堂 ,作者: 東不归
私信回复“资料 ” ,即可免费领取Python实战案例讲解视频
Python多线程爬虫讲解视频观看地址
https://www.bilibili.com/video/BV1L54y1r73F/
前言
本次推文介绍一下多线程。不过值得注意的是 ,不能滥用多线程,多线程爬虫请求内容速度过快,可能会导致服务器过载 ,或者是IP被封禁 。为了避免这一问题,我们在使用多线程爬虫的时候需要设置一个delay时间,用于请求同一域名时的最小时间间隔 。
线程和进程如何工作
当程序在运行时 ,就会创建包含代码和状态的进程。这些进程通过一个或者多个CPU来执行。不过同一时刻每个CPU只会执行一个进程,然后在不同进程之间快速切换,这样就感觉多个程序同时运行 。同理 ,在一个进程中,程序的执行也是在不同线程间进行切换的,每个线程执行程序的不同部分。这就意味着一个线程在等待执行时 ,进程会切换到其他的线程执行,这样可以避免浪费CPU时间。
Threading线程模块
在Python标准库中,使用threading模块来支持多线程 。Threading模块对thread进行了封装 ,绝大数情况 ,只需要使用threading这个模块。使用起来也非常简单:
t1=threading.Thread(target=run,args=("t1",)) 创建一个线程实例 # target是要执行的函数名(不是函数),args是函数对应的参数,以元组的形式存在 t1.start() 启动这个线程实例。
普通创建方式
线程的创建很简单 ,如下:
import threading import time def printStr(name): print(name+"-python青灯") s=0.5 time.sleep(s) print(name+"-python青灯") t1=threading.Thread(target=printStr,args=("你好!",)) t2=threading.Thread(target=printStr,args=("欢迎你!",)) t1.start() t2.start()
自定义线程
本质是继承threading.Thread,重构Thread类中的run方法
import threading import time class testThread(threading.Thread): def __init__(self,s): super(testThread,self).__init__() self.s=s def run(self): print(self.s+"——python") time.sleep(0.5) print(self.s+"——青灯") if __name__=='__main__': t1=testThread("测试1") t2=testThread("测试2") t1.start() t2.start()
守护线程
使用setDaemon(True)把子线程都变成主线程的守护线程,因此当主线程结束后 ,子线程也会随之结束 。也就是说,主线程不等待其守护线程执行完成再去关闭。
import threading import time def run(s): print(s,"python") time.sleep(0.5) print(s,"青灯") if __name__ == "__main__": t=threading.Thread(target=run,args=("你好!",)) t.setDaemon(True) t.start() print("end")
结果:
你好! python
end
当主线程结束后,守护线程不管有没有结束 ,都自动结束。
主线程等待子线程结束
使用join方法,让主线程等待子线程执行 。如下:
import threading import time def run(s): print(s,"python") time.sleep(0.5) print(s,"青灯") if __name__ == "__main__": t=threading.Thread(target=run,args=("你好!",)) t.setDaemon(True) t.start() t.join() print("end")
结果:
你好! python
你好! 青灯
end
以上是多线程的几种简单的用法,那么threading模块还有做什么呢?请往下看。
Lock 锁
其实在介绍diskcache缓存的时候也介绍过锁的相关内容 ,其实不难理解为啥多线程中也会出现锁的概念,当没有保护共享资源时,多个线程在处理同一资源时 ,可能会出现脏数据 ,造成不可以预期的结果,即线程不安全。
如下示例出现不可预期的结果:
import threading price=0 def changePrice(n): global price price=price+n price=price-n def runChange(n): for i in range(2000000): changePrice(n) if __name__ == "__main__": t1=threading.Thread(target=runChange,args=(5,)) t2=threading.Thread(target=runChange,args=(8,)) t1.start() t2.start() t1.join() t2.join() print(price)
理论上的结果为0,但是每次运行的结果可能都是不一样的 。
所以这个时候就需要锁去处理了 ,如下:
import threading import time from threading import Lock price=0 def changePrice(n): global price lock.acquire() #获取锁 price=price+n print("price:"+str(price)) price=price-n lock.release() #释放锁 def runChange(n): for i in range(2000000): changePrice(n) if __name__ == "__main__": lock=Lock() t1=threading.Thread(target=runChange,args=(5,)) t2=threading.Thread(target=runChange,args=(8,)) t1.start() t2.start() t1.join() t2.join() print(price)
结果值与理论值是一致的 。锁的意义在于每次只允许一个线程去修改同一数据,以保证线程安全。
信号量
BoundedSemaphore类,同时允许一定数量的线程更改数据 ,如下:
import threading import time def work(n): semaphore.acquire() print("序号:"+str(n)) time.sleep(1) semaphore.release() if __name__ == "__main__": semaphore=threading.BoundedSemaphore(5) for i in range(100): t=threading.Thread(target=work,args=(i+1,)) t.start() #active_count获取当前正在运行的线程数 while threading.active_count()!=1: pass else: print("end")
结果为:每5次打印停顿一下,直到结束。
GIL全局解释器锁
说到多线程,不得不提一下GIL 。GIL的全称是Global Interpreter Lock(全局解释器锁) ,这是python设计之初,为了数据安全所做的决定。某个线程想要执行,必须先拿到GIL ,并且在一个进程中,GIL只有一个。只有拿到GIL的线程,才能进入CPU执行 。GIL只在cpython中才有 ,因为cpython调用的是c语言的原生线程 ,所以他不能直接操作cpu,只能利用GIL保证同一时间只能有一个线程拿到数据。而在pypy和jpython中是没有GIL的。
总结
本篇文章介绍了多线程的用法,要根据实际的情况去使用 。多线程编程 ,容易发生冲突,必须用锁加以隔离,又得小心发生死锁。由于Python的设计时有GIL全局锁 ,导致多线程无法利用多核,使得在多线程并发的情况下并不理想。