Python 爬虫进阶篇——多线程

本文旨在简要介绍多线程的相关知识。需要注意的是,在实际应用中应当避免过度使用多线程以防止其带来的潜在风险。具体而言,在进行网络爬虫操作时若频繁请求同一网站内容其速度较快可能会导致服务器超负荷运行或IP地址被封锁。为了避免这一情况发生建议在进行多线程爬虫操作时设置适当的等待时间以避免潜在的问题。
线程和进程如何工作
当程序运行时就会生成包含代码与状态的进程 这些进程通常由一个或多个中央处理器负责 执行过程中同一时刻每一个中央处理器只能处理一个任务 并迅速切换至其他处理器以实现多任务并行 这种机制使得从外部观察似乎有多余数量的程序同时运行 在单个进程中 程序也会通过相互间的协作来进行模块化处理 每一模块负责完成特定的任务 当某个模块处于空闲状态时 系统会立即转而处理其他模块 从而最大限度地提高资源利用率
Threading线程模块
在Python标准库中,默认情况下可以通过调用threading模块来实现多线程功能。该模块对thread进行了封装处理,并且通常情况下只需调用throiding模块即可完成操作。操作起来非常容易。
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)将所有子 line thread 配置为主 line thread 的守护 process. 由此一旦主线程序终止时 子 thread 自动退出. I.e., 主 line 程序无需等到其守护 process 执行完毕即可关闭.
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模块还有什么其他的用途吗?接下来的内容会更深入地介绍 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全局解释器锁
说到多 thread时
总结
本文阐述了多线程的应用方法,并建议在实际开发中灵活运用。在进行多线程编程时容易遇到冲突问题,在这种情况下必须借助锁机制来隔离潜在的问题。为了防止出现死锁现象,在编写代码时必须谨慎处理相关逻辑。值得注意的是,在Python语言设计中存在全局锁机制(GIL),这使得在进行多线程并发操作时效果并不理想。
