使用多线程从URL下载文件

时间:2014-09-17 09:02:35

标签: multithreading curl coldfusion

我需要从网址下载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组合似乎有问题......

2 个答案:

答案 0 :(得分:2)

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

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

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

另外,如果下载的内容与示例一样简单,请考虑不使用cURL。考虑使用CFHTTP或其中一个java HTTP Client库,因为每次下载都不需要生成进程。

修改 关于使现有代码运行,我能够构建一个相应的示例,看起来执行正常(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属性需要是一个绝对路径,包括扩展名,但是你的代码似乎没有工作吗? / p>

我添加了一个注释掉的示例,使用<cfhttp>来实现与curl相同的功能。启动任何外部过程数百次几乎肯定不会扩展。调整上面的示例以分割列表和该列表上的每个工作应该很简单。

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

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....

答案 1 :(得分:1)

我认为您需要在线程中休眠一小段时间,以确保嵌套在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。但让我知道会发生什么。