参考博客

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队列模块

多线程操作固然提高了工作效率,但是做不好线程之间的协调,容易出现两种极端情况:

  1. 多个线程串行严格按顺序同步处理,即变成了单线程操作,实际上工作效率没有提升
  2. 线程之间争夺资源造成 哲学家就餐问题 阻塞报错

因此,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四个线程都结束后,在主进程中计算总爬取时间。