参考博客
Python3多线程threading介绍
Python多线程在爬虫中的应用
1.threading 模块
Python3的多线程操作推荐使用import threading
,封装了Thread
类 较方便,性能较好
具体API和使用方法参考博客,里面有详细的介绍,就不复读了
2.对于join()方法的理解
博客里也有详细介绍,总结一下就是,让主线程能够等待子线程执行完毕后,再进行操作。举例代码如下:
# 打印结果如下,主线程和子线程的先后是没有关系的,各自执行
import threading
import time
def thread_print(index,content):
print('Thread %s start!'%(index))
time.sleep(1)
print(content)
time.sleep(1)
print('Thread %s end!'%(index))
print('Main thread start!')
for i in range(2):
t=threading.Thread(target=thread_print,args=(i,'name %s'%(i)))
t.start()
print('Main thread end!')
打印结果如下,主线程和子线程的先后是没有关系的,各自执行
Main thread start!
Thread 0 start!
Thread 1 start!
Main thread end!
name 0
name 1
Thread 1 end!
Thread 0 end!
那当我们需要保证子线程执行完了之后 才能继续执行主线程的操作,应该怎么控制呢?代码如下:
# 打印结果如下,主线程和子线程的先后是没有关系的,各自执行
import threading
import time
def thread_print(index,content):
print('Thread %s start!'%(index))
time.sleep(1)
print(content)
time.sleep(1)
print('Thread %s end!'%(index))
print('Main thread start!')
thread_list=[]
for i in range(2):
t=threading.Thread(target=thread_print,args=(i,'name %s'%(i)))
t.start()
thread_list.append(t)
for t in thread_list:
t.join()
print('Main thread end!')
打印结果如下:重点在于使用了join()
方法,该方法会阻塞主线程直到被调用的子线程执行完毕
Main thread start!
Thread 0 start!
Thread 1 start!
name 0
name 1
Thread 0 end!
Thread 1 end!
Main thread end!
可是join()
方法不能滥用,滥用就变成了强制顺序执行,即单线程,就属于因噎废食了。详情见参考博客,很详细
3.线程锁
这部分代码详情参考博客,下面只做简单介绍:
1.互斥锁是一种独占锁,同一时刻只有一个线程可以访问共享的数据,使用很简单,初始化锁对象,然后将锁当做参数传递给任务函数,在任务中加锁,使用后释放锁。
2.信号Semaphore 这种锁允许一定数量的线程同时更改数据,它不是互斥锁。比如地铁安检,排队人很多,工作人员只允许一定数量的人进入安检区,其它的人继续排队
3.事件 Event 类名Event, 事件线程锁的运行机制:全局定义了一个Flag,如果Flag的值为False,那么当程序执行wait()方法时就会阻塞,如果Flag值为True,线程不再阻塞。这种锁,类似交通红绿灯(默认是红灯),它属于在红灯的时候一次性阻挡所有线程,在绿灯的时候,一次性放行所有排队中的线程。事件主要提供了四个方法set()、wait()、clear()和is_set()
- clear()方法会将事件的Flag设置为False
- set()方法会将Flag设置为True
- wait()方法将等待“红绿灯”信号
- is_set():判断当前是否"绿灯放行"状态
使用Quene队列模块
多线程操作固然提高了工作效率,但是做不好线程之间的协调,容易出现两种极端情况:
- 多个线程串行严格按顺序同步处理,即变成了单线程操作,实际上工作效率没有提升
- 线程之间争夺资源造成 哲学家就餐问题 阻塞报错
因此,Python中引入Quene队列模块来方便的解决该问题,比如下列代码使用Queue构造一个大小为1000的线程安全的先进先出队列,通过往队列里put
需要爬取的url
,同时一个线程的爬虫只能取一个队列的url。保证了线程之间不会阻塞,同时也不会保证线程出现空载
import threading # 导入threading模块
from queue import Queue #导入queue模块
import time #导入time模块
# 爬取文章详情页
def get_detail_html(detail_url_list, id):
while True:
url = detail_url_list.get() #Queue队列的get方法用于从队列中提取元素
time.sleep(2) # 延时2s,模拟网络请求和爬取文章详情的过程
print("thread {id}: get {url} detail finished".format(id=id,url=url)) #打印线程id和被爬取了文章内容的url
# 爬取文章列表页
def get_detail_url(queue):
for i in range(10):
time.sleep(1) # 延时1s,模拟比爬取文章详情要快
queue.put("http://testedu.com/{id}".format(id=i))#Queue队列的put方法用于向Queue队列中放置元素,由于Queue是先进先出队列,所以先被Put的URL也就会被先get出来。
print("get detail url {id} end".format(id=i))#打印出得到了哪些文章的url
#主函数
if __name__ == "__main__":
detail_url_queue = Queue(maxsize=1000) #用Queue构造一个大小为1000的线程安全的先进先出队列
# 先创造四个线程
thread = threading.Thread(target=get_detail_url, args=(detail_url_queue,)) #A线程负责抓取列表url
html_thread= []
for i in range(3):
thread2 = threading.Thread(target=get_detail_html, args=(detail_url_queue,i))
html_thread.append(thread2)#B C D 线程抓取文章详情
start_time = time.time()
# 启动四个线程
thread.start()
for i in range(3):
html_thread[i].start()
# 等待所有线程结束,thread.join()函数代表子线程完成之前,其父进程一直处于阻塞状态。
thread.join()
for i in range(3):
html_thread[i].join()
print("last time: {} s".format(time.time()-start_time))#等ABCD四个线程都结束后,在主进程中计算总爬取时间。