Python通过两个标准库thread和threading提供对线程的支持。thread提供了低级别的、原始的线程以及一个简单的锁。threading是对thread进行封装的高级模块,一般情况下只需要学习这个就可以了。
threading 模块提供的其他方法:
threading.currentThread() 返回当前的线程变量。
threading.enumerate() 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。
threading.activeCount() 返回正在运行的线程数量,与len(threading.enumerate())有相同的结果。
除了使用方法外,线程模块同样提供了Thread类来处理线程,Thread类提供了以下方法:
run() 用以表示线程活动的方法。
start() 启动线程活动。
join([time]) 等待至线程中止。这阻塞调用线程直至线程的join() 方法被调用中止-正常退出或者抛出未处理的异常-或者是可选的超时发生。
isAlive() 返回线程是否活动的。
getName() 返回线程名。
setName() 设置线程名。
线程同步
如果多个线程共同对某个数据修改,则可能出现不可预料的结果,为了保证数据的正确性,需要对多个线程进行同步。
使用Thread对象的Lock和Rlock可以实现简单的线程同步,这两个对象都有acquire方法和release方法,对于那些需要每次只允许一个线程操作的数据,可以将其操作放到acquire和release方法之间。如下:
多线程的优势在于可以同时运行多个任务(至少感觉起来是这样)。但是当线程需要共享数据时,可能存在数据不同步的问题。
考虑这样一种情况:一个列表里所有元素都是0,线程"set"从后向前把所有元素改成1,而线程"print"负责从前往后读取列表并打印。
那么,可能线程"set"开始改的时候,线程"print"便来打印列表了,输出就成了一半0一半1,这就是数据的不同步。为了避免这种情况,引入了锁的概念。
锁有两种状态——锁定和未锁定。每当一个线程比如"set"要访问共享数据时,必须先获得锁定;如果已经有别的线程比如"print"获得锁定了,那么就让线程"set"暂停,也就是同步阻塞;等到线程"print"访问完毕,释放锁以后,再让线程"set"继续。
经过这样的处理,打印列表时要么全部输出0,要么全部输出1,不会再出现一半0一半1的尴尬场面。
线程优先级队列( Queue)
Python的Queue模块中提供了同步的、线程安全的队列类,包括FIFO(先入先出)队列Queue,LIFO(后入先出)队列LifoQueue,和优先级队列PriorityQueue。这些队列都实现了锁原语,能够在多线程中直接使用。可以使用队列来实现线程间的同步。
Queue模块中的常用方法:
Queue.qsize() 返回队列的大小
Queue.empty() 如果队列为空,返回True,反之False
Queue.full() 如果队列满了,返回True,反之False
Queue.full 与 maxsize 大小对应
Queue.get([block[, timeout]])获取队列,timeout等待时间
Queue.get_nowait() 相当Queue.get(False)
Queue.put(item) 写入队列,timeout等待时间
Queue.put_nowait(item) 相当Queue.put(item, False)
Queue.task_done() 在完成一项工作之后,Queue.task_done()函数向任务已经完成的队列发送一个信号
Queue.join() 实际上意味着等到队列为空,再执行别的操作
多线程爬虫使用方法如下:
首先导入多线程以及队列模块
import threading
import queue as Queue
示例代码如下:
import pymongo
import requests
import threading
import queue as Queue
import time
client = pymongo.MongoClient('localhost', 27017)
mydb = client['mydb']
library = mydb['library']#连接数据库及集合
headers = {
'User-Agent':'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 \
(KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36',
}
class myThread(threading.Thread): #继承父类threading.Thread
#初始化参数
def __init__(self, name, q):
threading.Thread.__init__(self)
self.name = name
self.q = q
def run(self): #把要执行的代码写到run函数里面 线程在创建后会直接运行run函数
print('开始' + self.name)
while True:
try:
crawler(self.name, self.q) #调用crawler()爬虫函数
except:
break
print('退出' + self.name)
def crawler(threadName, q):
#自定义爬虫函数,传入线程名和队列
url = q.get(timeout=2) #获取队列中的url,timeout是等待时间
try:
r = requests.get(url,timeout=20)
print(q.qsize(), threadName, r.status_code, url) #打印队列大小,进程名等...
except Exception as e:
print(q.qsize(), threadName, url, 'Error:', e)
if __name__ == '__main__':
urls = [items['real_url'] for items in library.find()] #待爬取url列表
start = time.time() #计时开始
threadList = ['Thread-1','Thread-2','Thread-3','Thread-4','Thread-5'] #线程名列表
workQueue = Queue.Queue(len(urls)) #指明队列存放的上限,一旦达到上限,插入会导致阻塞,直到队列中的数据被消费掉。如果maxsize小于或者等于0,队列大小没有限制。
threads = [] #定义空进程列表
#遍历线程名列表,创建新线程
for tName in threadList:
thread = myThread(tName, workQueue) #实例化新线程,传入线程名和队列上限
thread.start() #开启线程
threads.append(thread) #添加线程到线程列表
#将url填充到workQueue队列
for url in list(urls):
workQueue.put(url)
#等待所有线程完成
for t in threads:
t.join()
end = time.time()
print('Queue多线程爬虫的总时间为:', end-start)
print('退出主线程')
网友评论