【问题标题】:Downloading files from urls using multithreading使用多线程从 url 下载文件
【发布时间】:2014-09-19 06:30:48
【问题描述】:

我需要从 url 下载一个 excel 文件列表并将它们保存在一个文件夹中。 (最多可以有 200 个文件左右。)我从以下代码开始循环和下载每个文件:

<cfloop query="idsToDownload">
    <cfset fileURL = "https://myLink/#downloadID#" /><!--- link to an xlsx file --->
    <cfexecute name="curl" arguments = "#fileURL# -k" timeout="10" outputFile="#downloadID#.xlsx" />
</cfloop>

这会下载每个文件并将它们保存在 Coldfusion 临时目录中。 (现在,只是为了测试——最终我们将决定我们希望它们存储在哪里并使用路径更新outputFile。)这很好,除了最终达到 cfloop 时间限制(大约 30 个文件下载之后)。但无论如何,我们真的很想为每次下载启动一个线程,以最大限度地提高效率。所以我在循环中添加了一个 cfthread 标记(免责声明:我是 cfthreading 的新手):

<cfloop query="idsToDownload">
    <cfthread name="download_#downloadID#" action="run">
        <cfset fileURL = "https://myLink/#downloadID#" />
        <cfexecute name="curl" arguments = "#fileURL# -k" timeout="10" outputFile="#downloadID#.xlsx" />
    </cfthread>
</cfloop>

我假设这将像以前一样执行,除了每次下载都将在异步线程中运行。但是,当我运行它时,什么也没有发生。我在页面上没有收到任何错误,但 ColdFusion 临时文件中没有显示任何文件(就像使用简单的无线程 cfloop 一样)。这段代码有什么问题?

编辑:

我也尝试过一次下载的单线程,它工作正常:

<cfthread name="downloadFile" action="run">
    <cfset fileURL = "https://myLink/123" />
    <cfexecute name="curl" arguments = "#fileURL# -k" timeout="10" outputFile="123.xlsx" />
</cfthread>

所以 cfloop/cfthread 组合似乎有问题...

【问题讨论】:

  • 你没有在页面上看到错误并不意味着没有发生。您只会看到从主线程生成的错误。要检查其他线程上的错误,请查看您的异常日志。
  • @DanBracuk - 我知道可能存在错误,我只是不确定它们是什么以及如何找到它们。

标签: multithreading curl coldfusion


【解决方案1】:

我认为您可能需要一种不同的方法来下载数百个文件。每个文件的线程只会在目标服务器可能停止响应或阻止您之前扩展如此之高(如果它不受您的控制)。此外,如果您使用 cURL,那么您会为每个线程生成一个子进程,因此会占用大量资源。

相反,我会创建一个线程池并在它们之间分配工作。创建 N 个线程并给每个线程一个要下载的文件列表。每个线程都将在列表中运行,您可以轻松调整 N 以获得最佳性能/资源使用权衡。

上述方法的潜在缺点是,如果一个文件列表的下载速度比其他文件列表快得多,那么它将提前结束,剩余的工作将由更少的线程执行。您可以实现单个工作跟踪器,每个线程调用它来获取下一个要下载的文件。只要它的 getNextFile() 方法被适当地同步,这将保持所有 N 个线程工作,直到有更多工作要做。

此外,如果您的下载与示例看起来一样简单,请考虑不使用 cURL。考虑 CFHTTP 或其中一个 java HTTP 客户端库,因为您不必每次下载都生成一个进程。

编辑 关于让现有代码运行,我能够构建一个相应的示例,它似乎执行正常(CF10/OSX):

Thread test...<br/>
<cfloop from="1" to="3" index="i">  
    <cfoutput>Starting #i# <br/></cfoutput><cfflush>
    <cfthread action="run" name="dl-thread-#i#" urlNumber="#i#">
        <cflog log="Application" text="#urlNumber#">
    <cfexecute name="/opt/local/bin/curl" arguments="https://www.google.co.uk/?q=#urlNumber#" outputfile="#GetTemplatePath()##urlNumber#.html" errorFile="#GetTemplatePath()##urlNumber#.html.err">
    <!--- alternatively....
    <cfhttp url="https://www.google.co.uk/?q=#urlNumber#" file="#urlNumber#.html" path="#GetDirectoryFromPath(GetTemplatePath())#" method="get" />
    --->
    </cfexecute>
</cfthread> 
</cfloop>
Done....

我能看到的唯一真正区别是我将参数显式传递给线程并允许线程代码使用这些参数来组装 URL(请参阅 urlNumber 属性)。在我这样做之前,我看到了非常奇怪的结果:我得到了为值 2-4 而不是 1-3 写入的文件。

我会确保线程需要的任何数据都明确传入。此外,cfexecute 上的文档指出 name 属性需要是一个绝对路径,包括扩展名,但是如果没有你的代码似乎可以正常工作?

我已经包含了一个使用&lt;cfhttp&gt; 来实现与 curl 相同的功能的示例。启动任何外部流程数百次几乎肯定不会扩展。调整上面的示例以拆分列表,并且该列表上的每项工作都应该很简单。

编辑 2 下面的 sn-p 实现了在可配置数量的线程之间划分工作负载:

Thread test...<br/>
<cfscript>
    urlCount=100;
    threads=5;
    urls=[];

    //utility function to split an array into a set of equal arrays
    function ArrayDivide(arr,divisor){
        divided=[];
        for(i=1;i<=divisor;i++){
            divided[i]=[];
        }
        for(i=1;i<=ArrayLen(arr);i++){
            ArrayAppend(divided[(i%divisor)+1],arr[i]);
            WriteOutput((i%divisor)+1 & "<br>");
        }
        return divided;
    }

    //Create a set of dummy URLs to test against
    //sleep.cfm waits for as long as it's asked to in order to simulate downloads taking a bit of time
    for(i=1;i<=urlCount;i++){
        urls[i]="http://localhost:8500/sleep.cfm?duration="&(i*50);
    }

    urlLists=ArrayDivide(urls,threads);

</cfscript>

<cfloop from="1" to="#threads#" index="i">  
    <cfoutput>Starting #i# <br/></cfoutput><cfflush>
    <cflog log="Application" text="#i# spawn">
    <cfthread action="run" name="dl-thread-#i#" urlList="#urlLists[i]#" threadNumber="#i#">
        <cflog log="Application" text="#threadNumber# start">
        <cfloop from="1" to="#ArrayLen(urlList)#" index="j">
            <cfhttp url="#urlList[j]#" file="#threadNumber#_#j#.html" path="#GetDirectoryFromPath(GetTemplatePath())#" method="get" />
        </cfloop>
        <cflog log="Application" text="#urlNumber# end">
</cfthread> 

</cfloop>
Done....

【讨论】:

  • CFHTTP 不会为每个调用生成新线程吗?此外,尽管这个答案提供了有用的替代方案,但它实际上并没有修复我的代码:) 有什么想法吗?
  • 另外,Wget 的开销会比 cURL 少吗?
  • 感谢更新!我将处理所有选项。我没有意识到您应该将数据传递给线程。如果我使用您的提示使其正常工作,我会接受答案
  • 这成功了!该错误是由于数据未明确传递到线程。谢谢!
  • 顺便说一句,我最终在 Edit 2 中的 sn-p 上对我的代码进行了建模,以实现“线程池”,所以谢谢! (我知道这是不久前的事了,但我才意识到我从未更新过)
【解决方案2】:

我认为您需要让线程休眠一小段时间,以确保嵌套在 cfthread 标记中的代码在 idsToDownload 循环实际执行完成之前不会执行。

<cfloop query="idsToDownload">
<cfthread name="download_#downloadID#" action="run" downloadId="#idsToDownload.downloadId#" fileUrl="#idsToDownload.fileUrl#">
    <cfthread action="sleep" duration="500"/>
    <cfset fileURL = "https://myLink/#attributes.downloadID#" />
    <cfexecute name="curl" arguments = "#attributes.fileURL# -k" timeout="10" outputFile="#attributes.downloadID#.xlsx" />
</cfthread>

我不确定这是否可行,因为我从未使用过 curl。但是让我知道会发生什么。

【讨论】:

  • 这不是重点吗?我只想让每个线程异步运行
  • 线程具有异步特性,因此它们将独立运行。 500 毫秒只是为了确保 CFThread 的逻辑可能比 CFLoop 的单个迭代花费更长的时间。只要它需要更长的时间——这可能是由于处理能力或仅仅由于线程排队——那么你就会遇到一些问题。我还更新了代码以使其更正确。请检查。
  • @TusharBhaware 您还可以将线程优先级设置为高,以便在执行时获得优先级。默认情况下,它具有正常的优先级。
  • 我还没有真正理解这个逻辑 - 但我确实尝试添加代码但没有帮助。
  • @froadie,对不起,伙计,它没有用。我不太会说话。要了解代码背后的逻辑,请阅读ben nadel's blog post
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多