使用线程的Python文件下载
我正在写一个Python脚本,这个脚本可以接受一个远程文件的路径和一个线程数量。文件的大小会根据线程的数量进行分割,当每个线程完成任务后,我希望它们把获取到的数据追加到一个本地文件中。
我该怎么做才能确保这些线程按照生成的顺序把数据追加到本地文件里,这样字节就不会乱了呢?
另外,如果我想同时下载多个文件,该怎么办呢?
4 个回答
你可以使用一个线程安全的“信号量”,像这样:
class Counter:
counter = 0
@classmethod
def inc(cls):
n = cls.counter = cls.counter + 1 # atomic increment and assignment
return n
使用 Counter.inc() 可以在多个线程之间返回一个递增的数字,这个数字可以用来跟踪当前的字节块。
不过,没必要把文件下载分成多个线程,因为下载的速度远远慢于写入磁盘的速度,所以一个线程总是会在下一个线程开始下载之前完成。
最简单且不占资源的方式就是直接把下载的文件描述符链接到磁盘上的文件对象。
你需要在每个线程中获取文件的完全不同部分。根据线程的数量来计算每个部分的开始和结束位置。每个部分之间必须没有重叠。
举个例子,如果目标文件的大小是3000字节,而你想用三个线程来获取数据:
- 线程1:获取第1到1000字节
- 线程2:获取第1001到2000字节
- 线程3:获取第2001到3000字节
你需要提前分配一个和原文件大小一样的空文件,然后把数据写回到文件的相应位置。
你可以用锁等方式来协调工作,但我建议你使用队列(Queue),这通常是协调Python中多线程(和多进程)最好的方法。
我建议主线程启动你认为合适数量的工作线程(你可能需要通过实验来平衡性能和远程服务器的负载);每个工作线程都在同一个全局的Queue.Queue
实例上等待,我们可以叫它workQ
,用于接收“工作请求”(wr = workQ.get()
可以正确获取工作请求——每个工作请求由一个工作线程获取,简单明了)。
在这个情况下,一个“工作请求”可以简单地是一个三元组(包含三个项目的元组):远程文件的标识(URL或其他),请求获取数据的起始位置,以及要获取的字节数(注意,这种方式同样适用于获取一个或多个文件的数据)。
主线程将所有工作请求推送到workQ
(对于每个请求,只需workQ.put((url, from, numbytes))
),并等待结果返回到另一个Queue
实例,我们叫它resultQ
(每个结果也将是一个三元组:文件标识、起始位置、从该文件在该位置获取的字节字符串)。
每个工作线程在完成请求后,会将结果放入resultQ
,然后回去获取另一个工作请求(或者等待新的请求)。与此同时,主线程(或者如果需要的话,可以有一个单独的“写入线程”——也就是说,如果主线程还有其他工作要做,比如处理GUI)从resultQ
中获取结果,并执行必要的open
、seek
和write
操作,将数据放到正确的位置。
结束操作有几种方法:例如,可以发送一个特殊的工作请求,要求接收它的线程终止——主线程在所有实际工作请求之后,将与工作线程数量相同的这种请求放入workQ
,然后在所有数据接收并写入后,等待所有工作线程结束(还有很多其他选择,比如直接加入队列,让工作线程成为守护线程,这样当主线程终止时它们就会自动结束,等等)。