Основы Grand Central Dispatch, часть третья: источники посылок
Автор: Mike Ash; оригинал статьи: Intro to Grand Central Dispatch, Part III: Dispatch Sources
Аннотация
Сегодня мы продолжим обсуждение Grand Central Dispatch. В прошлые разы я в основном концентрировался на очередях событий. Теперь же я собираюсь изучить источники посылок (dispatch sources), как они работают и как их использовать.
Хочется отметить. Я предполагаю, что вы прочитали обе предыдущие статьи серии. Первая особенно важна, вторая чуть менее. Если вы этого не сделали, прочитайте прямо сейчас.
Перед тем, как идти дальше, поделюсь отличной новостью. GCD перешел в разряд OpenSource-продуктов. Это очень хорошо со стороны Apple. Исходный код достаточно чистый и очень интересен для изучения.
Что такое источники посылок (dispatch sources)
Коротко, источник посылок — это объект, который следит за каким-то типом событий. Когда событие наступает, он автоматически посылает блок (задачу) на исполнение в очередь посылок.
Немного неясное описание. О каких именно событиях мы говорим? Вот список событий, которые поддерживаются GCD в 10.6.0:
- Порт (mach port) отослал изменение состояния прав;
- Порт (mach port) получил изменение состояния прав;
- Изменение состояния внешнего процесса;
- Дескриптор файла готов для чтения;
- Дескриптор файла готов для записи;
- Событие элемента файловой системы;
- Сигнал POSIX;
- Пользовательский таймер;
- Пользовательское событие.
Среди этого списка очень много чего полезного. Это практически все, что поддерживает kqueue, плюс порты (mach ports), плюс встроенная поддержка таймеров (вместо того, чтобы заставлять вас придумывать свои, используя параметр timeout), плюс пользовательские события.
Пользовательские события
Большая часть этих событий понятна без дополнительных объяснений, но, скорее всего, вам интересно, что такое пользовательское событие. Вкратце, это событие, которое создает сам разработчик, вызывая функцию dispatch_source_merge_data.
Немного странное имя для функции, которая создает событие. Причина этой странности в том, что GCD автоматически объединит несколько событий, которые произойдут перед тем, как обработчик получит управление. Вы можете «вливать» («merge») данные в источник посылок сколько угодно раз, и если очередь посылок была занята все это время, GCD вызывет обработчик событий только один раз.
Доступны два типа пользовательских событий: DISPATCH_SOURCE_TYPE_DATA_ADD и DISPATCH_SOURCE_TYPE_DATA_OR. У пользовательского источника событий есть атрибут unsigned long data, и можно также передать unsigned long в функцию dispatch_source_merge_data. В случае использования варианта с _ADD, при объединении события эти числа складываются. Используя тип _OR, числа объединяются бинарным «или». Когда же выполняется обработчик событий, он может получить текущее значение при помощи функции dispatch_source_get_data, после чего значение data сбрасывается в 0.
Давайте посмотрим на сценарий, когда это может быть полезным. Представим некий асинхронный код, который выполняет какую-то работу, в процессе которого нужно обновлять прогресс-бар. Так как главный поток — это просто другая очередь посылок в GCD, мы можем отправить GUI-задачу в главную очередь. С другой стороны, таких задач может быть очень много (тысячи, десятки тысяч в секунду — запросто, прим. пер.), а мы не хотим обновлять UI слишком часто. Лучше объединять все изменения, пока главный поток занят другой работой.
Источники посылок отлично подходят для этой задачи, если использовать тип DISPATCH_SOURCE_TYPE_DATA_ADD. Мы суммируем величину проделанной работы, после чего главный поток сможет выяснить, сколько работы было проделано со времени последнего события, обновив соответствующим образом индикатор.
Хватит разговоров, вот немного кода:
dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0, dispatch_get_main_queue());
dispatch_source_set_event_handler(source, ^{
[progressIndicator incrementBy:dispatch_source_get_data(source)];
});
dispatch_resume(source);
dispatch_apply([array count], globalQueue, ^(size_t index) {
// do some work on data at index
dispatch_source_merge_data(source, 1);
});
(Хочу сделать одно замечания по поводу этого кода, это ввело меня в ступор, когда я начал работать с источниками посылок. Это настолько меня задело, что я даже выделю полужирным. Источники посылок всегда стартуют в приостановленном состоянии (suspended)! Вы должны запустить их после создания, если вы хотите, чтобы события доставлялись!)
Если вы настроили индикатор прогресса на правильные минимальное/максимальное значение, это отлично сработает. Данные будут обрабатываться параллельно, как только задача будет выполнена, она просигнализирует источнику событий и добавит 1 к полю data источника посылок, что мы интерпретируем, как количество выполненных задач. Обработчик событий увеличит значение индикатора на количество выполненных задач с момента его последнего вызова. Если главный поток бездельничает, а задачи завершаются медленно, обработчик будет вызван каждый раз после завершения задачи, показывая результат в реальном времени. Если же главный поток занят или задачи завершаются слишком быстро, события будут объединяться и индикатор прогресса будет обновляться один раз, когда главный поток освободится для его обработки.
Вы сейчас, наверное, подумали, что это все круто, но я ведь могу намеренно захотеть, чтобы мои события не объединялись. Иногда нужно, чтобы каждый сигнал вызывал действие, безо всяких умностей, происходящих «за сценой». Это достаточно просто, вы просто должны подумать чуть чуть «за рамками». Если вы хотите, чтобы каждое ваше событие вызывало какое-либо действие, просто пользуйтесь dispatch_async вместо источника посылок. В конце-концов, это именно то, для чего функция сделана: планировать выполнение блока в указанной очереди. На самом деле, единственная причина существования источников посылок вместо dispatch_async — это возможность пользоваться плюсами объединения.
Встроенные события
Мы рассмотрели, как работать с пользовательскими событиями, но что с встроенными? Давайте посмотрим пример чтения из стандартного потока ввода при помощи GCD:
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_source_t stdinSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ,
STDIN_FILENO,
0,
globalQueue);
dispatch_source_set_event_handler(stdinSource, ^{
char buf[1024];
int len = read(STDIN_FILENO, buf, sizeof(buf));
if(len > 0) {
NSLog(@"Got data from stdin: %.*s", len, buf);
}
});
dispatch_resume(stdinSource);
В общем, все достаточно просто. Поскольку мы использовали глобальную очередь, обработчик автоматически будет исполняться в фоне, параллельно с остальным приложением, что означает автоматический параллельный прирост скорости в приложении, если оно делает что-либо еще в этот же момент.
Это также более приятно, нежели стандартный UNIX-вариант написания кода, так как не приходится писать циклы. Обычно, используя вызовы read, вам необходимо быть осторожным, поскольку он может вернуть меньше данных, чем вы запросили, и также страдает от случайных «ошибок» вроде EINTR (interrupted system call). Используя GCD, вы просто пропускаете все эти вещи, ничего не делая. Если у вас останутся непрочитанные данные в файловом дескрипторе, GCD просто вызовет обработчик еще раз.
Для стандартного ввода это не является проблемой, но для других файловых дескрипторов вам нужно понимать, как корректно завершить работу после чтения данных из (или записи в) дескриптора. Вы не должны закрывать дескриптор, пока источник посылок еще активен. Если создан другой файловый дескриптор (возможно, из другого потока) и так получилось, что ему присвоили тот же номер, ваш источник посылок будет (внезапно!) заниматься чтением из того, откуда не должен. Будет забавно посмотреть на процесс отладки такой ситуации.
Метод корректного завершения работы с дескриптором заключается в использовани функции dispatch_source_set_cancel_handler и передачи ей блока, который закрывает файловый дескриптор. После вы можете использовать функцию dispatch_source_cancel, чтобы завершить работу источника посылок, вызвав переданный ранее блок и, таким образом, закрыв файловый дескриптор.
Использование других источников посылок очень похоже. Обычно вы даете идентификатор источнику (порт, файловый дескриптор, ID процесса, и т. д.) в качестве названия (handle) источника посылок. Аргумент mask обычно не используется, но, например, в случае работы с DISPATCH_SOURCE_TYPE_PROC, показывает, какие типы событий процессов вам интересно получать. После чего назначьте обработчик, запустите источник (resume) и поехали. Эти источники посылок также предоставляют данные, которые зависят от источника, и могут быть получены при помощи функции dispatch_source_get_data. Например, файловые дескрипторы выдадут количество байт, доступных в дескрипторе. Источники событий процессов предоставят маску событий, произошедших с момента последнего вызова. Для полного списка того, что обозначает data в разных ситуациях, смотрите соответствующие страницы man.
Таймеры
Таймеры несколько отличаются от остального. Они не используют аргументы handle и mask, вместо этого работая с отдельной функцией, dispatch_source_set_timer, чтобы настроить таймер. Эта функция принимает три параметра для контроля частоты вызова таймера:
Параметр start контролирует, когда таймер будет вызван в первый раз. Тип параметра — dispatch_time_t, который является закрытым и не может модифицироваться напрямую. Функции dispatch_time и dispatch_walltime могут быть использованы для создания его экземпляров, а константы DISPATCH_TIME_NOW и DISPATCH_TIME_FOREVER помогут в случае, если вам нужны соответствующие значения.
Аргумент interval — целое число, которое понятно, что делает. А вот аргумент leeway более интересен. Он говорит системе, с какой точностью должен работать таймер. Таймер не может работать со 100% точностью, но можно хотя бы попытаться сказать системе, насколько сильно нужно стараться попасть точно во время. Если вы хотите, чтобы таймер вызывался раз в 5 секунд и чтобы точность была максимальная, передайте 0. С другой стороны, представим себе такую задачу, как регулярная проверка почты. Можно проверять, например, каждые 10 минут, но это совершенно не обязательно должно быть точное время. Можно передать, например, погрешность в 60 секунд, говоря системе, что вполне допустимо запустить таймер даже через минуту после того, как наступило его время.
Зачем это сделано? Если коротко — для экономии потребляемой электроэнергии. Более энерговыгодно, если CPU будет спать столько, сколько это возможно, а потом проснется и выполнит несколько задач сразу, чем если он будет постоянно что-то делать. Давая большую погрешность вашему таймеру, вы позволяете системе группировать свои действия вместе с другими похожими.
Заключение
Теперь вы знаете, как использовать источники посылок GCD для отслеживания файловых дескрипторов, запуска таймеров, объединения пользовательских событий и других похожих действий. Поскольку источники посылок полностью интегрированы с очередями посылок, вы можете использовать любую доступную вам очередь. Также вы можете попросить запускать обработчик в главном потоке, параллельно в одном из глобальных потоков или последовательно, учитывая используемую в каком-либо модуле вашей программы, пользовательскую очередь.
Ресурсы
Об авторе оригинальной статьи. Его зовут Mike Ash (Майк Эш), он — разработчик под Mac, как он говорит сам на своей домашней страничке, работает на Rogue Amoeba ночью и летает на глайдере днем.
- Оригинал статьи: Intro to Grand Central Dispatch, Part III: Dispatch Sources