LocatedFileStatusFetcher是MapReduce中一个针对给定输入路径数组,使用配置的线程数目来获取数据块位置的有用类。
它的主要作用就是利用多线程技术。每一个线程相应一个任务。每一个任务针对给定输入路径数组Path[],解析出文件状态列表队列BlockingQueue<List<FileStatus>>。当中。输入数据输入路径仅仅只是是一个Path。而输出数据则是文件状态列表队列BlockingQueue<List<FileStatus>>。文件状态FileStatus包括文件路径、长度、数据块大小、数据块副本数、文件所属用户、文件所属组、文件权限、文件近期改动时间、文件近期訪问时间、是否为文件夹等信息。
LocatedFileStatusFetcher採用了google并发编程包中的可监听Future模式ListenableFuture、可监听线程池ListeningExecutorService、回调函数FutureCallback。并使用了Java并发包中的可重入相互排斥锁ReentrantLock、多线程间协调通信工具Condition等实现了处理过程的多线程并发运行。并通过堵塞队列、回调函数等攻克了文件夹的递归解析问题。是一种很好的多线程环境下递归任务、可监听任务的实现。
那么。MapReduce中LocatedFileStatusFetcher是怎样实现的呢?本文将为你带来LocatedFileStatusFetcher的源代码分析。
首先,看下LocatedFileStatusFetcher的成员变量,代码例如以下:
// 输入路径数组 private final Path[] inputDirs; // 输入路径过滤器 private final PathFilter inputFilter; // 配置信息 private final Configuration conf; // 递归标志位 private final boolean recursive; // 使用MR新API标志位 private final boolean newApi; // 底层线程池rawExec private final ExecutorService rawExec; // 可监听线程池,基于底层线程池rawExec private final ListeningExecutorService exec; // 文件状态列表堵塞队列 private final BlockingQueue<List<FileStatus>> resultQueue; // 无效输入路径错误相关IO异常列表 private final List<IOException> invalidInputErrors = new LinkedList<IOException>(); // 处理原始输入路径回调函数 private final ProcessInitialInputPathCallback processInitialInputPathCallback = new ProcessInitialInputPathCallback(); // 处理输入路径回调函数 private final ProcessInputDirCallback processInputDirCallback = new ProcessInputDirCallback(); // 正在执行任务数原子计数器 private final AtomicInteger runningTasks = new AtomicInteger(0); // 可重入相互排斥锁 private final ReentrantLock lock = new ReentrantLock(); // 多线程间协调通信工具Condition private final Condition condition = lock.newCondition(); // 任务执行过程中未知错误 private volatile Throwable unknownError;LocatedFileStatusFetcher的成员变量比較多。可是大体能够分为下面几类:
一、实现基本功能的输入、输出成员变量
1、Path[] inputDirs:输入路径数组,其作为总体输入数据,每一个终于路径都会被LocatedFileStatusFetcher解析成文件状态FileStatus。
2、PathFilter inputFilter:输入路径过滤器,内置boolean accept(Path path)方法,对输入路径继续过滤,选取符合业务规则的路径;
3、Configuration conf:配置信息,能够从中获取运行任务的线程数。
4、boolean recursive:递归标志位。true表示对文件夹中的文件夹进行递归处理;
5、boolean newApi:使用MR新API标志位;
6、BlockingQueue<List<FileStatus>> resultQueue:文件状态列表堵塞队列,输出数据。即终于返回结果;
二、多线程须要使用的成员变量
1、ExecutorService rawExec:底层线程池;
2、ListeningExecutorService exec:基于底层线程池rawExec的可监听线程池,利用google的并发编程包实现;
3、ProcessInitialInputPathCallback processInitialInputPathCallback:处理原始输入路径回调函数;
4、ProcessInputDirCallback processInputDirCallback:处理输入路径回调函数;
5、AtomicInteger runningTasks:正在执行任务数原子计数器;
6、ReentrantLock lock:ReentrantLock lock。
7、Condition condition:多线程间协调通信工具。
三、存放中间结果或异常的成员变量
1、List<IOException> invalidInputErrors:无效输入路径错误相关IO异常列表。
2、Throwable unknownError:任务运行过程中未知错误;
再看下LocatedFileStatusFetcher的构造函数,代码例如以下:
/** * 构造函数 * * @param conf configuration for the job * @param dirs the initial list of paths * @param recursive whether to traverse the patchs recursively * @param inputFilter inputFilter to apply to the resulting paths * @param newApi whether using the mapred or mapreduce API * @throws InterruptedException * @throws IOException */ public LocatedFileStatusFetcher(Configuration conf, Path[] dirs, boolean recursive, PathFilter inputFilter, boolean newApi) throws InterruptedException, IOException { // 获取配置信息中的任务使用线程数numThreads,取參数mapreduce.input.fileinputformat.list-status.num-threads,參数未配置默觉得1, // 这里非常明显应该会大于1 int numThreads = conf.getInt(FileInputFormat.LIST_STATUS_NUM_THREADS, FileInputFormat.DEFAULT_LIST_STATUS_NUM_THREADS); // 使用Executors.newFixedThreadPool方式构造线程池rawExec。线程个数为numThreads。而且设置为后台线程,线程名格式为GetFileInfo #数字 rawExec = Executors.newFixedThreadPool( numThreads, new ThreadFactoryBuilder().setDaemon(true) .setNameFormat("GetFileInfo #%d").build()); // 使用MoreExecutors.listeningDecorator方式利用rawExec构造可监听线程池exec exec = MoreExecutors.listeningDecorator(rawExec); // 初始化终于返回结果数据结构,即文件状态列表的链式堵塞队列resultQueue resultQueue = new LinkedBlockingQueue<List<FileStatus>>(); // 依据构造函数入參初始化类成员变量。这些成员变量包含输入路径数组、配置信息、递归标志位等所有是外部输入数据 this.conf = conf; this.inputDirs = dirs; this.recursive = recursive; this.inputFilter = inputFilter; this.newApi = newApi; }LocatedFileStatusFetcher构造函数逻辑非常清晰,大体例如以下:
1、首先获取配置信息中的任务使用线程数numThreads:
取參数mapreduce.input.fileinputformat.list-status.num-threads,參数未配置默觉得1,这里非常明显应该会大于1。
2、使用Executors.newFixedThreadPool方式构造线程池rawExec,线程个数为numThreads,而且设置为后台线程,线程名格式为GetFileInfo #数字;
3、使用MoreExecutors.listeningDecorator方式利用rawExec构造可监听线程池exec;
4、初始化终于返回结果数据结构。即文件状态列表的链式堵塞队列resultQueue。
5、依据构造函数入參初始化类成员变量,这些成员变量包含输入路径数组、配置信息、递归标志位等所有是外部输入数据。
到了这里,您已经大概了解了LocatedFileStatusFetcher的结构。可是,您可能对Java并发编程或者google的可监听并发编程不是非常了解,为此,这里有必要做个简介,具体信息,读者可通过相关搜索引擎或书籍自行补脑。
首先说下Future,Future表示一个异步计算任务,当任务完毕时能够得到任务运行结果。您可能须要借助Future,通过启用另外的线程不断的查询任务状态,在任务完毕时,获取任务运行结果通知或者展示给用户。
而google的ListenableFuture顾名思义就是能够监听的Future。通过它在任务完毕后自己主动调用配置好的回调函数。您就能够非常方便的及时获取任务运行结果,採取下一步处理,这些回调函数统一都须要实现FutureCallback接口。
再来说下可重入相互排斥锁ReentrantLock,它是一个独占锁,即相互排斥的。意即当前线程获取该锁后。其它线程此时假设想要获取该锁,就必须等待当前线程释放锁。
何谓可重入呢?也非常easy。当前线程获取该锁后,未释放前,还能够再次获得或者说进入该锁。
第三个要说的是Condition,它是一个多线程间协调通信的工具类。通过其await()方法,当前线程会释放锁。进入睡眠,等待被唤醒;而其它线程借助Condition的signal()或signalAll()方法,则能够唤醒等待的线程,继续进行相关逻辑处理。
最后一个要说的是ListeningExecutorService,它是一个能够返回ListenableFuture的接口,其借助Java并发包中的ExecutorService。就能够实现一个可监听的线程池,而本例中的底层线程池是Executors.newFixedThreadPool。它是一个可重用固定线程数的线程池,以共享的无界队列方式来执行这些线程。
ListeningExecutorService中能够提交一些实现了Callable接口的线程任务,这些线程任务会被线程池调度。借助其call()方法完毕任务执行逻辑。
截至到眼下。相信您应该LocatedFileStatusFetcher使用的并发编程的一些基础知识有一个大致了解了吧!
好了。我们继续往下分析吧!看先LocatedFileStatusFetcher实现其核心功能的getFileStatuses()方法,代码例如以下:
/** * Start executing and return FileStatuses based on the parameters specified * 基于指定參数開始执行任务。并返回文件状态迭代器 * * @return fetched file statuses * @throws InterruptedException * @throws IOException */ public Iterable<FileStatus> getFileStatuses() throws InterruptedException, IOException { // Increment to make sure a race between the first thread completing and the // rest being scheduled does not lead to a termination. // 正在执行任务数原子计数器runningTasks加1 runningTasks.incrementAndGet(); // 遍历输入路径inputDirs for (Path p : inputDirs) { // 正在执行任务数原子计数器runningTasks加1 runningTasks.incrementAndGet(); // 将处理原始输入路径任务ProcessInitialInputPathCallable提交到线程池exec中去执行,并获取可监听Future,即ListenableFuture。 // 监听任务执行结果ProcessInitialInputPathCallable.Result ListenableFuture<ProcessInitialInputPathCallable.Result> future = exec .submit(new ProcessInitialInputPathCallable(p, conf, inputFilter)); // future中加入回调函数ProcessInitialInputPathCallback实例processInitialInputPathCallback Futures.addCallback(future, processInitialInputPathCallback); } // 正在执行任务数原子计数器runningTasks减1 runningTasks.decrementAndGet(); // 获取可重入相互排斥锁ReentrantLock实例lock lock.lock(); try { // 正在执行任务数原子计数器runningTasks不为0,且未知错误unknownError没有发生时 while (runningTasks.get() != 0 && unknownError == null) { // 等待全部任务执行完毕 condition.await(); } } finally { // 释放可重入相互排斥锁ReentrantLock lock.unlock(); } // 停止线程池exec this.exec.shutdownNow(); // 有未知错误unknownError的话处理未知错误 if (this.unknownError != null) { if (this.unknownError instanceof Error) { throw (Error) this.unknownError; } else if (this.unknownError instanceof RuntimeException) { throw (RuntimeException) this.unknownError; } else if (this.unknownError instanceof IOException) { throw (IOException) this.unknownError; } else if (this.unknownError instanceof InterruptedException) { throw (InterruptedException) this.unknownError; } else { throw new IOException(this.unknownError); } } // 有无效路径错误invalidInputErrors的话处理无效路径错误 if (this.invalidInputErrors.size() != 0) { if (this.newApi) { throw new org.apache.hadoop.mapreduce.lib.input.InvalidInputException( invalidInputErrors); } else { throw new InvalidInputException(invalidInputErrors); } } // 将结果队列resultQueue转换成迭代器并返回 return Iterables.concat(resultQueue); }getFileStatuses()方法的执行逻辑大体例如以下:
1、首先,正在执行任务数原子计数器runningTasks加1。这个是针对主线程任务的计数;
2、接着遍历输入路径inputDirs:
2.1、正在执行任务数原子计数器runningTasks加1,这个是针对每一个待处理输入路径的子线程任务的计数;
2.2、将处理原始输入路径任务提交到线程池exec中去运行。并获取可监听Future,即ListenableFuture,监听任务运行结果:
这里,原始输入路径任务为ProcessInitialInputPathCallable。它实现了Callable接口,并有一个内部静态类Result。作为任务处理结果,稍后我们对它做具体分析;
2.3、future中加入回调函数,待任务处理完毕后通过回调函数做进一步处理:
这里。回调函数为ProcessInitialInputPathCallback,即处理原始输入路径的回调函数。事实上现了FutureCallback接口。并对上述任务运行结果ProcessInitialInputPathCallable.Result进行回调处理;
3、正在执行任务数原子计数器runningTasks减1,这个是针对主线程任务的计数。含义是主线程任务在其他子线程任务所有执行完毕的情况下能够标记为处理完毕;
4、获取可重入相互排斥锁ReentrantLock实例lock;
5、当正在执行任务数原子计数器runningTasks不为0,且未知错误unknownError没有发生时,通过condition.await()方法,释放当前锁,进入睡眠。等待被唤醒。直到其它线程唤醒它,而且正在执行任务数原子计数器runningTasks为0,或者未知错误unknownError发生,才说明全部任务已执行完毕或不得不终止执行;
6、释放可重入相互排斥锁ReentrantLock;
7、停止线程池exec;
8、有未知错误unknownError的话处理未知错误;
9、有无效路径错误invalidInputErrors的话处理无效路径错误。
10、将结果队列resultQueue转换成迭代器并返回。
我们先说下这个原始输入路径任务为ProcessInitialInputPathCallable,它实现了Callable接口。并有一个内部静态类Result,作为任务处理结果,代码例如以下:
/** * Processes an initial Input Path pattern through the globber and PathFilter * to generate a list of files which need further processing. * 通过globber和路径过滤器PathFilter处理一个初始输入路径模式。产生一个须要进一步处理的文件列表。我们看到,它有三个成员变量,待处理路径path、配置信息conf、输入路径过滤器inputFilter。而且构造方法就是简单的根据入參初始化这三个成员变量。ProcessInitialInputPathCallable还提供了一个表示任务结果的内部静态类Result。它也有三个成员变量,处理过程中发生的IO异常列表errors、匹配的文件状态匹配的文件状态数组matchedFileStatuses数组matchedFileStatuses、文件系统实例fs。并提供了加入IO异常到errors列表的addError()方法。*/ private static class ProcessInitialInputPathCallable implements Callable<ProcessInitialInputPathCallable.Result> { // 待处理路径 private final Path path; // 配置信息 private final Configuration conf; // 输入路径过滤器 private final PathFilter inputFilter; public ProcessInitialInputPathCallable(Path path, Configuration conf, PathFilter pathFilter) { this.path = path; this.conf = conf; this.inputFilter = pathFilter; } @Override public Result call() throws Exception { // 构造任务结果Result实例result Result result = new Result(); // 从路径path中获取文件系统FileSystem实例fs FileSystem fs = path.getFileSystem(conf); // 设置任务结果Result实例result中的fs变量 result.fs = fs; // 通过文件系统FileSystem实例fs的globStatus()方法。将路径path根据输入路径过滤器inputFilter解析成文件状态FileStatus数组matches FileStatus[] matches = fs.globStatus(path, inputFilter); if (matches == null) { // 假设文件状态FileStatus数组matches为null,说明路径根本不存在,将IO异常通过addError()方法加入到result中 result.addError(new IOException("Input path does not exist: " + path)); } else if (matches.length == 0) { // 假设文件状态FileStatus数组matches不为null,但长度为0,说明路径存在可是没有通过过滤器过滤规则,将IO异常通过addError()方法加入到result中 result.addError(new IOException("Input Pattern " + path + " matches 0 files")); } else { // 将符合过滤规则的文件状态FileStatus数组matches赋值给任务结果result的matchedFileStatuses result.matchedFileStatuses = matches; } return result; } private static class Result { // 处理过程中发生的IO异常列表errors private List<IOException> errors; // 匹配的文件状态数组matchedFileStatuses private FileStatus[] matchedFileStatuses; // 文件系统实例 private FileSystem fs; // 加入IO异常到errors列表 void addError(IOException ioe) { if (errors == null) { errors = new LinkedList<IOException>(); } errors.add(ioe); } } }
重点看下 ProcessInitialInputPathCallable的call()方法。它是任务得以运行的入口方法,其大体逻辑例如以下:
1、构造任务结果Result实例result;
2、从路径path中获取文件系统FileSystem实例fs;
3、设置任务结果Result实例result中的fs变量。
4、通过文件系统FileSystem实例fs的globStatus()方法,将路径path根据输入路径过滤器inputFilter解析成文件状态FileStatus数组matches:
这里。限于篇幅及主题明白性,我们不做过多介绍,你仅仅要知道它的主要作用即可,我们将在单线程处理的博文中进行具体介绍;
5、依据matches分别处理任务运行结果:
5.1、假设文件状态FileStatus数组matches为null,说明路径根本不存在,将IO异常通过addError()方法加入到result中;
5.2、假设文件状态FileStatus数组matches不为null,但长度为0,说明路径存在可是没有通过过滤器过滤规则。将IO异常通过addError()方法加入到result中;
5.3、否则将符合过滤规则的文件状态FileStatus数组matches赋值给任务结果result的matchedFileStatuses;
6、返回任务结果result。
原始路径处理任务运行完毕的回调函数则是通过ProcessInitialInputPathCallback来定义的。代码例如以下:
/** * The callback handler to handle results generated by * {@link ProcessInitialInputPathCallable} * */ private class ProcessInitialInputPathCallback implements FutureCallback<ProcessInitialInputPathCallable.Result> { // 任务运行成功时:不是说结果对错,而是说任务能完整的运行下来 @Override public void onSuccess(ProcessInitialInputPathCallable.Result result) { try { // 假设任务结果有IO异常 if (result.errors != null) { // 通过registerInvalidInputError()方法,将IO异常列表errors所有加入到无效输入路径错误相关IO异常列表invalidInputErrors中 registerInvalidInputError(result.errors); } // 假设任务结果得到了匹配的文件状态数组 if (result.matchedFileStatuses != null) { // 遍历匹配的文件状态数组matchedFileStatuses,取出每一个文件状态FileStatus实例matched,做下面处理: for (FileStatus matched : result.matchedFileStatuses) { // 正在运行任务数原子计数器runningTasks加1,这里标识的是子任务数加1 runningTasks.incrementAndGet(); // 将处理输入路径任务ProcessInputDirCallable提交到线程池exec中去运行,并获取可监听Future,即ListenableFuture, // 监听任务运行结果ProcessInputDirCallable.Result ListenableFuture<ProcessInputDirCallable.Result> future = exec .submit(new ProcessInputDirCallable(result.fs, matched, recursive, inputFilter)); // future中加入回调函数ProcessInputDirCallback实例processInputDirCallback Futures.addCallback(future, processInputDirCallback); } } // 解析原始路径的任务完毕。调用decrementRunningAndCheckCompletion()做兴许处理工作: // 正在运行任务数原子计数器减1,并推断是否为0,为0,说明所有任务运行完毕,通过condition.signal()通知主线程进行处理 decrementRunningAndCheckCompletion(); } catch (Throwable t) { // Exception within the callback // 有异常的话,调用registerError()方法。重置任务运行过程中未知错误unknownError,并通过condition.signal()通知主线程, // 有未知发生错误。交由主线程处理(主线程在有位置错误unknownError的情况下会结束整个流程),潜台词就是第一次发生未知错误时,不会通知主线程结束整个流程。兴许再发生时才会通知 registerError(t); } } // 任务运行失败时:不是说结果对错,而是说任务不能完整的运行下来 @Override public void onFailure(Throwable t) { // Any generated exceptions. Leads to immediate termination. // 调用registerError()方法,重置任务运行过程中未知错误unknownError。并通过condition.signal()通知主线程, // 有未知发生错误,交由主线程处理(主线程在有位置错误unknownError的情况下会结束整个流程),潜台词就是第一次发生未知错误时,不会通知主线程结束整个流程。兴许再发生时才会通知 registerError(t); } }原始路径处理任务运行完毕的回调函数ProcessInitialInputPathCallback实现了FutureCallback接口。并对原始路径处理任务结果ProcessInitialInputPathCallable.Result进行检測处理,主要分为两种情况:
1、任务运行成功时:不是说结果对错,而是说任务能完整的运行下来
通过onSuccess()方法来处理,大体逻辑例如以下:
1.1、假设任务结果有IO异常。通过registerInvalidInputError()方法。将IO异常列表errors所有加入到无效输入路径错误相关IO异常列表invalidInputErrors中;
1.2、假设任务结果得到了匹配的文件状态数组,遍历匹配的文件状态数组matchedFileStatuses,取出每一个文件状态FileStatus实例matched,做下面处理:
1.2.1、正在执行任务数原子计数器runningTasks加1,这里标识的是子任务数加1;
1.2.2、将处理输入路径任务ProcessInputDirCallable提交到线程池exec中去运行,并获取可监听Future。即ListenableFuture。监听任务运行结果ProcessInputDirCallable.Result:
这里的ProcessInputDirCallable任务,主要是为给定文件状态FileStatus获取数据块位置,如有必要(即须要递归文件夹进行处理),加入额外的路径到处理队列,兴许递归处理。而给定文件状态FileStatus则是通过解析原始路径任务ProcessInitialInputPathCallable来获得的;
1.2.3、future中加入回调函数ProcessInputDirCallback实例processInputDirCallback;
1.3、解析原始路径的任务完毕。调用decrementRunningAndCheckCompletion()做兴许处理工作:正在执行任务数原子计数器减1。并推断是否为0,为0,说明所有任务执行完毕,通过condition.signal()通知主线程进行处理;
须要说明的是,上述逻辑运行期间。假设有Throwable发生。则会调用registerError()方法,至于怎样处理,參见2任务运行失败时的处理;
2、任务运行失败时:不是说结果对错,而是说任务不能完整的运行下来
通过onFailure()方法来处理。调用registerError()方法,重置任务运行过程中未知错误unknownError。并通过condition.signal()通知主线程。有未知发生错误。交由主线程处理(主线程在有位置错误unknownError的情况下会结束整个流程)。潜台词就是第一次发生未知错误时。不会通知主线程结束整个流程,兴许再发生时才会通知。
decrementRunningAndCheckCompletion()方法代码例如以下:
private void decrementRunningAndCheckCompletion() { // 获取可重入相互排斥锁lock lock.lock(); try { // 正在执行任务数原子计数器减1,并推断是否为0,为0。说明所有任务执行完毕。通过condition.signal()通知主线程进行处理 if (runningTasks.decrementAndGet() == 0) { condition.signal(); } } finally { // 释放可重入相互排斥锁lock lock.unlock(); } }而registerError()方法代码例如以下:
/** * Register fatal errors - example an IOException while accessing a file or a * full exection queue */ private void registerError(Throwable t) { // 获取可重入相互排斥锁lock lock.lock(); try { // 重置任务运行过程中未知错误unknownError,并通过condition.signal()通知主线程。 // 有未知发生错误。交由主线程处理(主线程在有位置错误unknownError的情况下会结束整个流程) if (unknownError != null) { unknownError = t; condition.signal(); } } finally { // 释放可重入相互排斥锁lock lock.unlock(); } }两个方法功能非常明白,凝视也非常具体,且上面已经提到过,这里不再赘述!
接下来,我们再看下ProcessInputDirCallable任务,它主要是为给定文件状态FileStatus获取数据块位置。如有必要(即须要递归文件夹进行处理),加入额外的路径到处理队列,兴许递归处理。事实上现例如以下:
/** * Retrieves block locations for the given @link {@link FileStatus}, and adds * additional paths to the process queue if required. * 为给定文件状态获取数据块位置,如有必要。加入额外的路径到处理队列。首先,ProcessInputDirCallable内部有四个成员变量。各自是文件系统实例fs、文件状态实例fileStatus、递归标志位recursive、输入路径过滤器inputFilter,意义都非常明白,而构造方法也是依据入參初始化这四个成员变量,不再详述。*/ private static class ProcessInputDirCallable implements Callable<ProcessInputDirCallable.Result> { // 文件系统实例 private final FileSystem fs; // 文件状态实例 private final FileStatus fileStatus; // 递归标志位 private final boolean recursive; // 输入路径过滤器 private final PathFilter inputFilter; // 构造函数 ProcessInputDirCallable(FileSystem fs, FileStatus fileStatus, boolean recursive, PathFilter inputFilter) { this.fs = fs; this.fileStatus = fileStatus; this.recursive = recursive; this.inputFilter = inputFilter; } // 任务运行主方法 @Override public Result call() throws Exception { // 构造结果Result Result result = new Result(); // 初始化结果中的文件系统实例fs result.fs = fs; // 假设文件状态fileStatus相应为文件夹 if (fileStatus.isDirectory()) { // 通过文件系统FileSystem实例fs的listLocatedStatus()方法获取fileStatus相应的带数据块位置信息文件状态迭代器iter RemoteIterator<LocatedFileStatus> iter = fs .listLocatedStatus(fileStatus.getPath()); // 通过迭代器iter遍历每一个带数据块位置信息文件状态stat while (iter.hasNext()) { LocatedFileStatus stat = iter.next(); // 通过输入路径过滤器的accept()方法进行过滤 if (inputFilter.accept(stat.getPath())) { // 假设须要递归,且stat为文件夹 if (recursive && stat.isDirectory()) { // 加入到结果result的dirsNeedingRecursiveCalls列表 result.dirsNeedingRecursiveCalls.add(stat); } else { // 否则加入到结果result的locatedFileStatuses列表 result.locatedFileStatuses.add(stat); } } } } else { // 假设文件状态fileStatus相应为文件。直接加入到结果result的locatedFileStatuses列表 result.locatedFileStatuses.add(fileStatus); } return result; } // 处理结果 private static class Result { // 已处理完的文件状态链表locatedFileStatuses private List<FileStatus> locatedFileStatuses = new LinkedList<FileStatus>(); // 须要递归的文件状态链表dirsNeedingRecursiveCalls private List<FileStatus> dirsNeedingRecursiveCalls = new LinkedList<FileStatus>(); // 文件系统实例 private FileSystem fs; } }
任务运行结果由其静态内部类Result来表示,它包括三个成员变量,已处理完的文件状态链表locatedFileStatuses、须要递归再处理的文件状态链表dirsNeedingRecursiveCalls、文件系统实例fs,意义都非常明白,不再详述。
接下来。我们再看下任务执行的入口方法call()的执行逻辑。归纳例如以下:
1、构造任务执行结果Result实例result;
2、初始化结果中的文件系统实例fs。
3、假设文件状态fileStatus相应为文件夹:
3.1、通过文件系统FileSystem实例fs的listLocatedStatus()方法获取fileStatus相应的带数据块位置信息文件状态迭代器iter:
文件系统FileSystem实例fs的listLocatedStatus()方法我们会在单线程任务重点描写叙述。这里你仅仅要记住它的主要功能就是依据文件状态获取数据块位置信息。并返回带数据块位置信息文件状态迭代器。而带数据块位置信息文件状态LocatedFileStatus是文件状态FileStatus的子类,其内部多了一个成员变量BlockLocation[] locations。表示文件所含数据块的位置信息;
3.2、通过迭代器iter遍历每一个带数据块位置信息文件状态stat:通过输入路径过滤器的accept()方法进行过滤,假设须要递归,且stat为文件夹,加入到结果result的dirsNeedingRecursiveCalls列表,否则加入到结果result的locatedFileStatuses列表;
4、假设文件状态fileStatus相应为文件,直接加入到结果result的locatedFileStatuses列表;
5、返回任务运行结果result。
如同上面提到的解析原始路径任务ProcessInitialInputPathCallable一样。ProcessInputDirCallable任务也须要在任务运行完毕后有回调函数做进一步处理,而这个回调函数是通过ProcessInputDirCallback来实现的,代码例如以下:
/** * The callback handler to handle results generated by * {@link ProcessInputDirCallable}. This populates the final result set. * */ private class ProcessInputDirCallback implements FutureCallback<ProcessInputDirCallable.Result> { // 任务运行完毕时:不是说结果对错,而是说任务能完整的运行下来 @Override public void onSuccess(ProcessInputDirCallable.Result result) { try { // 假设任务运行结果中已处理完的文件状态链表locatedFileStatuses有数据的话。将其加入到终于返回结果队列resultQueue中 if (result.locatedFileStatuses.size() != 0) { resultQueue.add(result.locatedFileStatuses); } // 假设任务运行结果中须要递归再处理的文件状态链表dirsNeedingRecursiveCalls,再次提交ProcessInputDirCallable任务到线程池ProcessInputDirCallable。 // runningTasks计数器加1,加入回调函数ProcessInputDirCallback。以实现迭代处理 if (result.dirsNeedingRecursiveCalls.size() != 0) { for (FileStatus fileStatus : result.dirsNeedingRecursiveCalls) { runningTasks.incrementAndGet(); ListenableFuture<ProcessInputDirCallable.Result> future = exec .submit(new ProcessInputDirCallable(result.fs, fileStatus, recursive, inputFilter)); Futures.addCallback(future, processInputDirCallback); } } // 解析路径的任务完毕,调用decrementRunningAndCheckCompletion()做兴许处理工作: // 正在运行任务数原子计数器减1,并推断是否为0,为0,说明所有任务运行完毕,通过condition.signal()通知主线程进行处理 decrementRunningAndCheckCompletion(); } catch (Throwable t) { // Error within the callback itself. // 有异常的话,调用registerError()方法,重置任务运行过程中未知错误unknownError,并通过condition.signal()通知主线程, // 有未知发生错误。交由主线程处理(主线程在有位置错误unknownError的情况下会结束整个流程),潜台词就是第一次发生未知错误时。不会通知主线程结束整个流程。兴许再发生时才会通知 registerError(t); } } // 任务运行失败时:不是说结果对错,而是说任务不能完整的运行下来 @Override public void onFailure(Throwable t) { // Any generated exceptions. Leads to immediate termination. // 调用registerError()方法。重置任务运行过程中未知错误unknownError,并通过condition.signal()通知主线程, // 有未知发生错误。交由主线程处理(主线程在有位置错误unknownError的情况下会结束整个流程),潜台词就是第一次发生未知错误时,不会通知主线程结束整个流程。兴许再发生时才会通知 registerError(t); } }
ProcessInputDirCallbacky如同上面介绍的ProcessInitialInputPathCallback一样。也分成功、失败两种情况分别进行处理:
1、任务运行成功时:不是说结果对错,而是说任务能完整的运行下来
通过onSuccess()方法来处理,大体逻辑例如以下:
1.1、假设任务运行结果中已处理完的文件状态链表locatedFileStatuses有数据的话。将其加入到终于返回结果队列resultQueue中。
1.2、假设任务运行结果中须要递归再处理的文件状态链表dirsNeedingRecursiveCalls,再次提交ProcessInputDirCallable任务到线程池ProcessInputDirCallable。runningTasks计数器加1,加入回调函数ProcessInputDirCallback,以实现迭代处理。
1.3、解析路径的任务完毕。调用decrementRunningAndCheckCompletion()做兴许处理工作:正在执行任务数原子计数器减1,并推断是否为0,为0,说明所有任务执行完毕。通过condition.signal()通知主线程进行处理;
须要说明的是。上述逻辑运行期间。假设有Throwable发生,则会调用registerError()方法,至于怎样处理,參见2任务运行失败时的处理;
2、任务运行失败时:不是说结果对错,而是说任务不能完整的运行下来
通过onFailure()方法来处理,调用registerError()方法。重置任务运行过程中未知错误unknownError,并通过condition.signal()通知主线程,有未知发生错误。交由主线程处理(主线程在有位置错误unknownError的情况下会结束整个流程),潜台词就是第一次发生未知错误时,不会通知主线程结束整个流程,兴许再发生时才会通知。
总结
LocatedFileStatusFetcher通过多线程的方式。实现了针对给定输入路径数组,使用配置的线程数目来获取数据块位置的核心功能。它通过google的可监听并发技术ListenableFuture、ListeningExecutorService。实现了两层级别的子任务的并发运行、结果监听与回调处理,第一层任务是ProcessInitialInputPathCallable,依据输入路径获取相应文件状态。第二层任务是ProcessInputDirCallable,依据文件状态获取带数据块位置信息的文件状态,每层任务都有一个静态内部类Result来非常好的抽象任务运行结果。每层任务都有一个回调函数,在获得任务运行结果后做进一步处理,而且第一层任务运行结束后,在回调函数里提交第二层任务,且第二层任务会依据是否递归的标志位和实际路径情况,在在回调函数里决定是否递归提交第二层任务。
另外,LocatedFileStatusFetcher还使用了可重入相互排斥锁ReentrantLock、多线程间协调通信工具Condition来解决多线程之间的并发同步问题。特别是主任务线程与子任务线程间的主从协调、通信等。不得不说,LocatedFileStatusFetcher是多线程处理递归任务一种非常好的实现,值得我们借鉴和学习。