Статьи

Google Guava — Фьючерсы

Этот пост является продолжением моей серии о Google Guava, на этот раз посвященной фьючерсам. Класс Futures представляет собой набор статических служебных методов для работы с интерфейсом Future / ListenableFuture. Future — это дескриптор асинхронной задачи, Runnable или Callable, которая была передана в ExecutorService. Интерфейс Future предоставляет методы для: получения результатов задачи, проверки выполнения задачи или отмены задачи. Интерфейс ListenableFuture расширяет интерфейс Future и добавляет возможность настроить прослушиватель завершения для запуска после завершения задачи. Чтобы создать ListenableFuture, вам сначала нужно украсить экземпляр ExecutorService следующим образом:

1
ExecutorService executorService = MoreExecutors.listeningDecorator(Executors.newCachedThreadPool());

Теперь все представленные Callables / Runnables будут возвращать ListenableFuture. Другие исполнители можно найти в пакете com.google.common.util.concurrent. ListenableFutures были освещены в предыдущем посте . В Futures слишком много методов, чтобы эффективно охватить их в одном посте, поэтому я расскажу только о них: chain, transform, allAsList и successAsList. В этом посте я буду ссылаться на Futures и ListenableFutures взаимозаменяемо.

цепь

Метод chain возвращает ListenableFuture, значение которого вычисляется путем взятия результатов из входного Future и применения его в качестве аргумента объекта Function , который, в свою очередь, возвращает другой ListenableFuture. Давайте посмотрим на пример кода и пройдемся по нему:

01
02
03
04
05
06
07
08
09
10
11
12
13
ListenableFuture<List<String>> indexSearch =
              luceneSearcher.searchAsync('firstName:martin');
 
Function<List<String>, ListenableFuture<List<Person>>> queryFunction =
 new Function<List<String>, ListenableFuture<List<Person>>>() {
            @Override
            public ListenableFuture<List<Person>> apply(final List<String> ids) {
                 return dataService.getPersonsByIdAsync(ids);
            }
        };
 
ListenableFuture<List<Person>> results =
              Futures.chain(indexSearch, queryFunction,executorService);
  1. Строка 1 выполняет асинхронный поиск с использованием Lucene и возвращает список идентификаторов, которые представляют первичный ключ записи о человеке, хранящейся в базе данных. (Я создал небольшой индекс, где единственными данными, хранящимися в Lucene, являются идентификаторы, а остальные данные были проиндексированы только).
  2. Строки 4 — 11 строят объект функции, где метод apply будет использовать результаты из будущего поиска в качестве входных данных. Будущее, возвращаемое из apply, является результатом вызова объекта dataService.
  3. Строка 12 — это будущее, которое возвращается из цепного вызова. ExecutorService используется для запуска функции после завершения ввода.

Для дополнительной ясности вот что делают методы searchAsync и getPersonsByIdAsync. Эти вызовы методов из строк 2 и 8 соответственно, в предыдущем примере кода:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
public ListenableFuture<List<String>> searchAsync(final String query)  {
            return executorService.submit(new Callable<List<String>>() {
                @Override
                public List<String> call() throws Exception {
                    return search(query);
                }
            });
  }
 
public ListenableFuture<List<Person>> getPersonsByIdAsync(final List<String> ids) {
        return executorService.submit(new Callable<List<Person>>() {
            @Override
            public List<Person> call() throws Exception {
                return getPersonsById(ids);
            }
        });
    }

Цепной метод имеет две подписи:

  1. цепочка (ListentableFuture, Function)
  2. цепочка (ListenableFuture, Function, ExecutorService)

При определении того, какой метод использовать, необходимо учитывать несколько моментов.
Если входное будущее завершено к вызванной временной цепочке, предоставленная функция будет немедленно выполнена в вызывающем потоке. Кроме того, если исполнитель не предоставлен, то используется MoreExecutors.sameThreadExecutor. MoreExecutors.sameThreadExecutor (как и следует из названия) следует за ThreadPoolExecutor.CallerRunsPolicy, что означает, что отправленная задача выполняется в том же потоке, который вызвал execute / submit.

преобразование

Метод transform аналогичен методу цепочки в том, что он принимает объекты Future и Function в качестве аргументов. Разница в том, что ListenableFuture не возвращается, только результаты применения данной функции к результатам входного будущего. Учтите следующее:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
List<String> ids = ....
ListenableFuture<List<Map<String, String>>> dbRecords =  
                                           dataService.getPersonDataByIdAsync(ids);
 
 Function<List<Map<String, String>>,List<Person>> transformDbResults =
  new Function<List<String>, List<Person>>() {
              @Override
              public List<Person> apply(List<Map<String, String>> personMapList) {
                      List<Person> personObjList = new ArrayList<Person>();
                      for(Map<String,String> personDataMap : personMapList){
                             personObjList.add(new Person(personDataMap);
                      }
                      return personObjList;
                    }
     };
 
ListenableFuture<List<Person>> transformedResults =
                      Futures.transform(dbRecords, transformDbResults, executorService);
  1. В строке 2 выполняется асинхронный поиск в базе данных.
  2. В строке 4 создается объект функции, но в строке 8 обратите внимание, что тип возвращаемого значения — List <Person>

Метод transform имеет те же перегруженные вызовы метода, что и цепочка, с теми же оговорками.

AllAsList

Метод allAsList будет принимать произвольное количество ListenableFutures либо в виде переменных, либо в форме итератора <ListenableFuture>. Возвращается ListenableFuture, значением которого является список всех результатов входных данных. Значения, возвращаемые в списке, находятся в том же порядке, что и исходный список. Если какое-либо из входных значений было отменено или не выполнено, то возвращаемое ListenableFuture также будет отменено или не выполнится. Отмена возвращенного будущего из вызова allAsList не будет распространяться ни на одну из исходных задач, представленных в списке.

1
2
3
4
5
ListenableFuture<List<Person>> lf1 = getPersonsByFirstNameFuture('martin');
ListenableFuture<List<Person>> lf2 = getPersonsByFirstNameFuture('bob');
ListenableFuture<List<List<Person>>> lfResults = Futures.allAsList(lf1, lf2);
//assume lf1 failed
List<List<Person>> personLists = lfResults.get() //call results in exception

SuccessfulAsList

Метод successAsList очень похож на allAsList, но более щадящий. Так же, как и allAsList, successAsList возвращает список результатов в том же порядке, что и список ввода, но если какой-либо из входов был неудачным или был отменен, соответствующее значение в списке равно нулю. Отмена возвращенного будущего также не отменяет никаких исходных данных.

1
2
3
4
5
6
7
ListenableFuture<List<Person>> lf1 = getPersonsByFirstNameFuture('martin');
ListenableFuture<List<Person>> lf2 = getPersonsByFirstNameFuture('bob');
ListenableFuture<List<List<Person>>> lfResults = Futures.successfulAsList(lf1, lf2);
//assume lf1 failed
List<List<Person>> personLists = lfResults.get();
List<Person> listOne = personLists.get(0) //listOne is null
List<Person> listTwo = personLists.get(1) //listTwo, not null

Вывод

Надеюсь, это помогло обнаружить полезность, содержащуюся в классе Futures, из Google Guava. Я создал модульный тест, который показывает пример использования методов, описанных в этом посте. Поскольку существует достаточное количество вспомогательного кода, я создал проект на gihub, guava -blog . Этот проект также будет содержать исходный код из моих предыдущих постов ( Monitor , ListenableFuture ) на Guava. Как всегда комментарии и предложения приветствуются.

Ресурсы

Ссылка: Google Guava — Фьючерс от нашего партнера по JCG Билла Бекака в блоге « Случайные мысли о кодировании» .