线程池执行器 vs 线程模块中的 Thread

12
我有一个关于`ThreadPoolExecutor`和单独的`Thread`类性能的问题,这似乎说明我缺乏一些基本的理解。
我有一个网络爬虫,包括两个函数。第一个函数用于解析网站主页每个图像的链接,第二个函数用于加载解析出的链接上的图像。
import threading
import urllib.request
from bs4 import BeautifulSoup as bs
import os
from concurrent.futures import ThreadPoolExecutor

path = r'C:\Users\MyDocuments\Pythom\Networking\bbc_images_scraper_test'
url = 'https://www.bbc.co.uk'

# Function to parse link anchors for images
def img_links_parser(url, links_list):
    res = urllib.request.urlopen(url)
    soup = bs(res,'lxml')
    content = soup.findAll('div',{'class':'top-story__image'})

    for i in content:
        try:
            link = i.attrs['style']
            # Pulling the anchor from parentheses
            link = link[link.find('(')+1 : link.find(')')]
            # Putting the anchor in the list of links
            links_list.append(link)
        except:
            # links might be under 'data-lazy' attribute w/o paranthesis
            links_list.append(i.attrs['data-lazy'])

# Function to load images from links
def img_loader(base_url, links_list, path_location):
    for link in links_list:
        try:
            # Pulling last element off the link which is name.jpg
            file_name = link.split('/')[-1]
            # Following the link and saving content in a given direcotory
            urllib.request.urlretrieve(urllib.parse.urljoin(base_url, link), 
            os.path.join(path_location, file_name))
        except:
            print('Error on {}'.format(urllib.parse.urljoin(base_url, link)))

以下代码分为两种情况:
情况1:我正在使用多个线程:
threads = []
t1 = threading.Thread(target = img_loader, args = (url, links[:10], path))
t2 = threading.Thread(target = img_loader, args = (url, links[10:20], path))
t3 = threading.Thread(target = img_loader, args = (url, links[20:30], path))
t4 = threading.Thread(target = img_loader, args = (url, links[30:40], path))
t5 = threading.Thread(target = img_loader, args = (url, links[40:50], path))
t6 = threading.Thread(target = img_loader, args = (url, links[50:], path))

threads.extend([t1,t2,t3,t4,t5,t6])
for t in threads:
    t.start()
for t in threads:
    t.join()

上述代码在我的机器上工作了10秒。

情况2:我正在使用ThreadPoolExecutor

with ThreadPoolExecutor(50) as exec:
    results = exec.submit(img_loader, url, links, path)

上面的代码结果为18秒。

我的理解是ThreadPoolExecutor为每个工作者创建一个线程。因此,如果我将max_workers设置为50,将会产生50个线程,因此应该更快地完成任务。

请问有人能解释一下我错在哪里吗?我承认我在这里犯了一个愚蠢的错误,但我就是不明白。

非常感谢!


正如 @hansaplast 所指出的,我只使用了一个工作进程。因此,我修改了我的 img_loader 函数以接受单个链接,然后在上下文管理器下面添加了一个 for 循环来处理列表中的每个链接。这将时间缩短到了 3.8 秒。 - Vlad
2个回答

8
在方案2中,您将所有链接都发送给了一个工作器。与其这样做,不如
exec.submit(img_loader, url, links, path)

您需要:

for link in links:
    exec.submit(img_loader, url, [link], path)

我没有亲自尝试过,这只是从ThreadPoolExecutor文档中阅读得到的。


1
是的,你说得完全正确。虽然我也在考虑过这个问题,但我不知道为什么没有自己尝试一下。非常感谢你回复我!结果是3.8秒,很棒! :) - Vlad
1
@Vlad,你能为你的问题添加一个合适的答案吗?concurrent.futures.ThreadPoolExecutorthreading之间有什么区别? - Keto

1

根据链接中的解释,您也可以使用executor.map函数,而不是像hansaplast建议的那样运行for循环。

with ThreadPoolExecutor() as executor:

    # Create a new partially applied function that stores the directory
    # argument.
    # 
    # This allows the download_link function that normally takes two
    # arguments to work with the map function that expects a function of a
    # single argument.
    fn = partial(download_link, download_dir)

    # Executes fn concurrently using threads on the links iterable. The
    # timeout is for the entire process, not a single call, so downloading
    # all images must complete within 30 seconds.
    executor.map(fn, links, timeout=30)

我认为它可以很容易地适应您的需求。

回答关于ThreadPoolExecutor的问题,虽然我不是专家,但根据我迄今阅读的文档,ThreadPoolExecutor比自己使用Threading.Thread创建动态工作池更简单。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接