Контакты

Загрузки файлов из интернета. Состояния потоков и планирование их выполнения


Перед тем как начать изучение файловой системы языка С, необходимо уяснить, в чем разница между потоками и файлами . В системе ввода/вывода С для программ поддерживается единый интерфейс, не зависящий от того, к какому конкретному устройству осуществляется доступ. То есть в этой системе между программой и устройством находится нечто более общее, чем само устройство. Такое обобщенное устройство ввода или вывода (устройство более высокого уровня абстракции) называется потоком , в то время как конкретное устройство называется файлом . (Впрочем, файл - тоже понятие абстрактное.) Очень важно понимать, каким образом происходит взаимодействие потоков и файлов.

Потоки

Файловая система языка С предназначена для работы с самыми разными устройствами, в том числе терминалами, дисководами и накопителями на магнитной ленте. Даже если какое-то устройство сильно отличается от других, буферизованная файловая система все равно представит его в виде логического устройства, которое называется потоком. Все потоки ведут себя похожим образом. И так как они в основном не зависят от физических устройств, то та же функция, которая выполняет запись в дисковый файл, может ту же операцию выполнять и на другом устройстве, например, на консоли. Потоки бывают двух видов: текстовые и двоичные.

Текстовые потоки

Текстовый поток - это последовательность символов. В стандарте С считается, что текстовый поток организован в виде строк, каждая из которых заканчивается символом новой строки. Однако в конце последней строки этот символ не является обязательным. В текстовом потоке по требованию базовой среды могут происходить определенные преобразования символов. Например, символ новой строки может быть заменен парой символов - возврата каретки и перевода строки. Поэтому может и не быть однозначного соответствия между символами, которые пишутся (читаются), и теми, которые хранятся во внешнем устройстве. Кроме того, количество тех символов, которые пишутся (читаются), и тех, которые хранятся во внешнем устройстве, может также не совпадать из-за возможных преобразований.

Двоичные потоки

Двоичный поток - это последовательность байтов, которая взаимно однозначно соответствует байтам на внешнем устройстве, причем никакого преобразования символов не происходит. Кроме того, количество тех байтов, которые пишутся (читаются), и тех, которые хранятся на внешнем устройстве, одинаково. Однако в конце двоичного потока может добавляться определяемое приложением количество нулевых байтов. Такие нулевые байты, например, могут использоваться для заполнения свободного места в блоке памяти незначащей информацией, чтобы она в точности заполнила сектор на диске.

Файлы

В языке С файлом может быть все что угодно, начиная с дискового файла и заканчивая терминалом или принтером. Поток связывают с определенным файлом, выполняя операцию открытия . Как только файл открыт, можно проводить обмен информацией между ним и программой.

Но не у всех файлов одинаковые возможности. Например, к дисковому файлу прямой доступ возможен, в то время как к некоторым принтерам - нет. Таким образом, мы пришли к одному важному принципу, относящемуся к системе ввода/вывода языка С: все потоки одинаковы, а файлы - нет.

Если файл может поддерживать запросы на местоположение (указатель текущей позиции) , то при открытии такого файла указатель текущей позиции в файле устанавливается в начало. При чтении из файла (или записи в него) каждого символа указатель текущей позиции увеличивается, обеспечивая тем самым продвижение по файлу.

Файл отсоединяется от определенного потока (т.е. разрывается связь между файлом и потоком) с помощью операции закрытия . При закрытии файла, открытого с целью вывода, содержимое (если оно есть) связанного с ним потока записывается на внешнее устройство. Этот процесс, который обычно называют дозаписью потока, гарантирует, что никакая информация случайно не останется в буфере диска. Если программа завершает работу нормально, т.е. либо main() возвращает управление операционной системе, либо вызывается exit() , то все файлы закрываются автоматически. В случае аварийного завершения работы программы, например, в случае краха или завершения путем вызова abort() , файлы не закрываются.

У каждого потока, связанного с файлом, имеется управляющая структура, содержащая информацию о файле; она имеет тип FILE . В этом блоке управления файлом никогда ничего не меняйте .

Если вы новичок в программировании, то разграничение потоков и файлов может показаться излишним или даже «заумным». Однако надо помнить, что основная цель такого разграничения - это обеспечить единый интерфейс. Для выполнения всех операций ввода/вывода следует использовать только понятия потоков и применять всего лишь одну файловую систему. Ввод или вывод от каждого устройства автоматически преобразуется системой ввода/вывода в легко управляемый поток.

Или принудительным освобождением (содержимого) буфера .
Блок управления файлом - небольшой блок памяти, временно выделенный операционной системой для хранения информации о файле, который был открыт для использования. Блок управления файлом обычно содержит информацию об идентификаторе файла, его расположении на диске и указателе текущей позиции в файле.
Если, конечно, вы не разрабатываете систему ввода-вывода.

Состояния потоков и планирование их выполнения


Для каждого созданного потока в системе предусматриваются три возможных его состояния:

  • состояние выполнения, когда код потока выполняется процессором; на однопроцессорных платформах в этом состоянии в каждый момент времени может находиться только один поток;
  • состояние готовности к выполнению, когда поток готов продолжать свою работу и ждет освобождения ЦП;
  • состояние ожидания наступления некоторого события; в этом случае поток не претендует на время ЦП, пока не наступит определенное событие (завершение операции ввода/вывода, освобождение необходимого потоку занятого ресурса, сигнала от другого потока); часто такие потоки называют блокированными.
Изменение состояния потока происходит в результате соответствующих действий. Удобно для этих целей использовать следующую диаграмму состояний и переходов.

Переходы между состояниями можно описать следующим образом:

  • «готовность» → «выполнение»: система в соответствии с алгоритмом планирования выбирает для выполнения текущий поток, предоставляя ему ЦП
  • «выполнение» → «готовность»: поток готов продолжать свою работу, но система принимает решение прервать его выполнение; чаще всего это происходит по следующим двум причинам:
    • завершается выделенное потоку время владения процессором;
    • в числе готовых к выполнению появляется более приоритетный поток по сравнению с текущим;
  • «выполнение» → «ожидание»: дальнейшее исполнение кода текущего активного потока невозможно без наступления некоторого события, и поэтому активный поток прерывает свое выполнение и переводится системой в состояние ожидания (блокируется);
  • «ожиданием» → «готовность»: в системе происходит некоторое событие, наступление которого ожидает один из блокированных потоков, и поэтому система переводит этот поток в состояние готовности (разблокирует), после чего он будет учитываться системой при планировании порядка предоставления ЦП;
  • наконец, поток может нормально или аварийно завершить свое выполнение, после чего система удаляет его дескриптор из своей внутренней структуры, и тем самым поток перестает существовать.
В состояниях готовности и ожидания может находиться несколько потоков, поэтому система создает для хранения их дескрипторов отдельные списковые структуры. Организация этих списков зависит от тех принципов, которые положены в основу планирования потоков для данной ОС.

Цель планирования потоков вполне очевидна - определение порядка выполнения потоков в условиях внешней или внутренней многозадачности. Однако способы достижения этой цели существенно зависят от типа ОС. Рассмотрим сначала принципы планирования для универсальных ОС. Для таких ОС нельзя заранее предсказать, сколько и какие потоки будут запущены в каждый момент времени и в каких состояниях они будут находиться. Поэтому планирование должно выполняться динамически на основе сбора и анализа информации о текущем состоянии вычислительной системы.

Для этого в состав ОС включается модуль-планировщик, реализующий выбранные алгоритмы планирования. Поскольку этот модуль представляет собой программный код, то для решения своих задач планировщик должен на некоторое время забирать ЦП. Отсюда следует, что алгоритмы планирования должны быть максимально простыми, иначе возникает опасность, что система будет тратить недопустимо большое время на решение своих внутренних задач, а на выполнение прикладных программ времени не останется.

Кроме вычислительной простоты, алгоритмы планирования должны обладать следующими общими свойствами:

  • обеспечение максимально возможной загрузки ЦП;
  • обеспечение равномерной загрузки ресурсов вычислительной системы;
  • обеспечение справедливого обслуживания всех процессов и потоков;
  • минимизация времени отклика для интерактивных процессов.
За время существования ОС было предложено и реализовано несколько принципов управления потоками. В настоящее время большинство универсальных ОС используют метод вытесняющей многозадачности (preemptive multitasking), который тоже имеет несколько разновидностей. В основе метода лежат два важнейших и достаточно понятных принципа: квантование времени ЦП и приоритеты потоков.

Квантование означает, что каждому потоку система выделяет определенный интервал времени (квант), в течение которого процессор потенциально может выполнять код этого потока. По завершении выделенного кванта планировщик принудительно переключает процессор на выполнение другого готового потока (если, конечно, такой есть), переводя старый активный поток в состояние готовности. Это гарантирует, что ни один поток не захватит ЦП на непозволительно большое время (как было в более ранних системах с так называемой невытесняющей или кооперативной многозадачностью). Конечно, выделенный квант поток может и не использовать до конца, если в процессе своего выполнения он нормально или аварийно завершится, или потребует наступления некоторого события, или будет прерван системой.

Для эффективной работы ОС большое значение имеет выбор величины кванта. Очень маленькие значения кванта приводят к частым переключениям ЦП, что повышает непроизводительные расходы из-за необходимости постоянного сохранения контекста прерываемого потока и загрузки контекста активизируемого потока. Наоборот, большие значения кванта уменьшают иллюзию одновременного выполнения нескольких приложений. Некоторые планировщики умеют изменять кванты в определенных пределах, увеличивая их для тех потоков, которые не используют до конца выделенное время, например, из-за частых обращений к операциям ввода/вывода. Типичный диапазон изменения кванта – от 10 до 50 миллисекунд. При этом необходимо учитывать все возрастающие скорости работы современных процессоров: за 10 миллисекунд (т.е. за 1/100 секунды) процессор успеет выполнить около 10 млн. элементарных команд.

Можно связать величину кванта с приоритетом потока. Приоритет определяет важность потока и влияет на частоту запуска потока и, возможно, на величину выделяемого кванта. Интуитивно понятно, что потоки могут иметь разную степень важности: системные – более высокую (иначе ОС не сможет решать свои задачи), прикладные – менее высокую. Многие ОС позволяют группировать потоки по их важности, выделяя три группы, или класса:

  • потоки реального времени с максимально высоким уровнем приоритета;
  • системные потоки с меньшим уровнем приоритета;
  • прикладные потоки с самым низким приоритетом.
Внутри каждой группы выделяется свой диапазон возможных значений приоритетов, причем эти диапазоны между собой не пересекаются, т.е. максимально возможный приоритет прикладного потока всегда будет строго меньше минимально возможного приоритета для системных потоков. Внутри каждой группы могут использоваться разные алгоритмы управления приоритетами.

Если приоритет потока может меняться системой, то такие приоритеты называют динамическими, иначе – фиксированными. Конечно, реализация фиксированных приоритетов гораздо проще, тогда как динамические приоритеты позволяют реализовать более справедливое распределение процессорного времени. Например, потоки, интенсивно использующие внешние устройства, очень часто блокируются до завершения выделенного кванта времени, т.е. не используют эти кванты полностью. Справедливо при разблокировании таких потоков дать им более высокий приоритет для быстрой активации, что обеспечивает большую загрузку относительно медленных внешних устройств. С другой стороны, если поток полностью расходует выделенный квант, система может после его приостановки уменьшить приоритет. Тем самым, более высокие приоритеты получают более короткие потоки, быстро освобождающие процессор, и следовательно, достигается более равномерная загрузка вычислительной системы в целом.

Довольно интересной и часто используемой разновидностью приоритетов являются так называемые абсолютные приоритеты: как только среди готовых потоков появляется поток, приоритет которого выше, чем приоритет текущего активного потока, этот активный поток досрочно прерывается с передачей процессора более приоритетному потоку.

Для реализации приоритетного обслуживания ОС должна создавать и поддерживать набор приоритетных очередей. Для каждого возможного значения приоритета создается своя очередь, в которую потоки (в виде своих дескрипторов) помещаются строго в соответствии с очередностью. Планировщик просматривает эти очереди по порядку следования приоритетов и выбирает для выполнения первый поток в самой приоритетной непустой очереди. Отсюда следует, что потоки с меньшими приоритетами будут выполняться, только если пусты все более приоритетные очереди. Если допускается изменение приоритета, то планировщик должен уметь перемещать поток в другую очередь в соответствии с новым значением приоритета.

Схематично массив приоритетных очередей представлен на следующем рисунке, где для удобства более приоритетные потоки собраны в левой части массива, менее приоритетные – в правой, а сами приоритеты изменяются от 1 (максимум) до n (минимум). Условное обозначение «поток i.2» показывает, что данный поток имеет приоритет i и стоит вторым по порядку в своей очереди.

Для изменения приоритета и, возможно, кванта времени планировщику необходима следующая информация: базовая величина приоритета и кванта, время ожидания в очереди, накопленное время выполнения, интенсивность обращения к операциям ввода/вывода. Вся эта информация должна сохраняться в соответствующих структурах данных.

В итоге, планировщик включается в работу при возникновении одного из следующих событий:

  • завершение кванта времени для текущего активного потока (сигнал от системного таймера);
  • нормальное завершение кода текущего активного потока;
  • аварийное завершение кода текущего активного потока;
  • запрос активным потоком занятого системного ресурса;
  • появление среди готовых потоков более приоритетного потока.
При этом запускается код планировщика, который просматривает приоритетные очереди и выбирает наиболее приоритетный поток. После этого происходит собственно само переключение потоков:
  • формируется контекст прерываемого потока;
  • с помощью контекста вновь активизируемого потока восстанавливается необходимое состояние вычислительной системы, в частности, загружаются необходимые значения во все регистры процессора;
  • поскольку в регистр-счетчик команд из контекста заносится адрес очередной подлежащей выполнению команды активизируемого потока, то процессор переходит к выполнению кода нового потока точно с того места, где оно было прервано.
Планирование потоков в системах реального времени строится на других принципах. Поскольку для подобных систем наиболее важным показателем является скорость работы, то планирование выполняется статически. Для этого заранее строится так называемая таблица переключений, с помощью которой в зависимости от текущего состояния вычислительного процесса быстро и однозначно определяется запускаемый в данный момент поток.

  • Перевод
  • Tutorial

От переводчика: данная статья является седьмой в цикле переводов официального руководства по библиотеке SFML. Прошлую статью можно найти Данный цикл статей ставит своей целью предоставить людям, не знающим язык оригинала, возможность ознакомится с этой библиотекой. SFML - это простая и кроссплатформенная мультимедиа библиотека. SFML обеспечивает простой интерфейс для разработки игр и прочих мультимедийных приложений. Оригинальную статью можно найти . Начнем.

Что такое поток?

Большая часть из вас уже знает, что такое поток, однако объясним, что это такое, для новичков в данной теме.

Поток - это по сути последовательность инструкций, которые выполняются параллельно с другими потоками. Каждая программа создает по меньшей мере один поток: основной, который запускает функцию main(). Программа, использующая только главный поток, является однопоточной; если добавить один или более потоков, она станет многопоточной.

Так, короче, потоки - это способ сделать несколько вещей одновременно. Это может быть полезно, например, для отображения анимации и обработки пользовательского ввода данных во время загрузки изображений или звуков. Потоки также широко используется в сетевом программировании, во время ожидания получения данные будет продолжаться обновление и рисование приложения.

Потоки SFML или std::thread?

В своей последней версии (2011), стандартная библиотека C++ предоставляет набор классов для работы с потоками . Во время написания SFML, стандарт C++11 еще не был написан и не было никакого стандартного способа создания потоков. Когда SFML 2.0 был выпущен, было много компиляторов, которые не поддерживали этот новый стандарт.

Если вы работаете с компилятором, который поддерживает новый стандарт и содержит заголовочный файл, забудьте о классах потоков SFML и используйте стандартные классы C++ вместо них. Но, если вы работаете с компилятором, не поддерживающим данный стандарт, или планируете распространять ваш код и хотите добиться полной портируемости, потоковые классы SFML являются хорошим выбором

Создание потоков с помощью SFML

Хватит разглагольствований, давайте посмотрим на код. Класс, дающий возможность создавать потоки с помощью SFML, называется sf::Thread , и вот как это (создание потока) выглядит в действии:

#include #include void func() { // эта функция запускается когда вызывается thread.launch() for (int i = 0; i < 10; ++i) std::cout << "I"m thread number one" << std::endl; } int main() { // создание потока с функцией func в качестве точки входа sf::Thread thread(&func); // запуск потока thread.launch(); // главные поток продолжает быть запущенным... for (int i = 0; i < 10; ++i) std::cout << "I"m the main thread" << std::endl; return 0; }
В этом коде функции main и func выполняются параллельно после вызова thread.launch(). Результатом этого является то, что текст, выводимый обеими функциями, смешивается в консоли.

Точка входа в поток, т.е. функция, которая будет выполняться, когда поток запускается, должна быть передана конструктору sf::Thread . sf::Thread пытается быть гибким и принимать различные точки входа: non-member функции или методы классов, функции с аргументами или без них, функторы и так далее. Приведенный выше пример показывает, как использовать функцию-член, вот несколько других примеров.

  • non-member функция с одним аргументом:

    Void func(int x) { } sf::Thread thread(&func, 5);

  • метод класса:

    Class MyClass { public: void func() { } }; MyClass object; sf::Thread thread(&MyClass::func, &object);

  • функтор (функциональный объект):

    Struct MyFunctor { void operator()() { } }; sf::Thread thread(MyFunctor());

Последний пример, который использует функтор, является наиболее мощным, поскольку он может принимать любые типы функторов и поэтому делает класс sf::Thread совместимым со многими типами функций, которые напрямую не поддерживаются. Эта функция особенно интересна с лямбда-выражениями C++11 или std::bind.

// с лямбда-функцией sf::Thread thread((){ std::cout << "I am in thread!" << std::endl; });
// с std::bind void func(std::string, int, double) { } sf::Thread thread(std::bind(&func, "hello", 24, 0.5));
Если вы хотите использовать sf::Thread внутри класса, не забудьте, что он не имеет стандартного конструктора. Поэтому, вы должны инициализировать его в конструкторе вашего класса в списке инициализации:

Class ClassWithThread { public: ClassWithThread() : m_thread(&ClassWithThread::f, this) { } private: void f() { ... } sf::Thread m_thread; };
Если вам действительно нужно создать экземпляр sf::Thread после инициализации объекта, вы можете создать его в куче.

Запуск потока

После того, как вы создали экземпляр sf::Thread , вы должны запустить его с помощью запуска функции.

Sf::Thread thread(&func); thread.launch();
launch вызывает функцию, которую вы передали в конструктор нового потока, и сразу же завершает свою работу, так что вызывающий поток может сразу же продолжить выполнение.

Остановка потоков

Поток автоматически завершает свою работу, когда функция, служащая точкой входа для данного потока, возвращает свое значение. Если вы хотите ждать завершения потока из другого потока, вы можете вызвать его функцию wait.

Sf::Thread thread(&func); // запуск потока thread.launch(); ... // выполнение блокируется до тех пор, пока поток не завершится thread.wait();
Функция ожидания также неявно вызывается деструктором sf::Thread , так что поток не может оставаться запущенным (и бесконтрольным) после того, как его экземпляр sf::Thread уничтожается. Помните это, когда вы управляете вашими потоками (смотрите прошлую секцию статьи).

Приостановка потока

В SFML нет функции, которая бы предоставляла способ приостановки потока; единственный способ приостановки потока - сделать это из кода самого потока. Другими словами, вы можете только приостановить текущий поток. Что бы это сделать, вы можете вызвать функцию sf::sleep:

Void func() { ... sf::sleep(sf::milliseconds(10)); ... }
sf::sleep имеет один аргумент - время приостановки. Это время может быть выражено в любой единице, как было показано в статье про .

Обратите внимание, что вы можете приостановить любой поток с помощью данной функции, даже главный поток.

Sf::sleep является наиболее эффективным способом приостановить поток: на протяжении приостановки потока, он (поток) практически не потребляет ресурсы процессора. Приостановка, основанная на активном ожидании, вроде пустого цикла while, потребляет 100% ресурсов центрального процессора и делает… ничего. Однако имейте в виду, что продолжительность приостановки является просто подсказкой; реальная продолжительность приостановки (больше или меньше указанного вами времени) зависит от ОС. Так что не полагайтесь на эту функцию при очень точном отсчете времени.

Защита разделяемых данных

Все потоки в программе разделяют некоторую память, они имеют доступ ко всем переменным в области их видимости. Это очень удобно, но также опасно: с момента параллельного запуска потока, переменные или функции могут использоваться одновременно разными потоками. Если операция не является потокобезопасной, это может привести к неопределенному поведению (т. е. это может привести к сбою или повреждению данных).

Существует несколько программных инструментов, которые могут помочь вам защитить разделяемые данные и сделать ваш код потокобезопасным, их называют примитивами синхронизации. Наиболее распространенными являются следующие примитивы: мьютексы, семафоры, условные переменные и спин-блокировки. Все они - варианты одной и той же концепции: они защищают кусок кода, давая только определенному потоку право получать доступ к данным и блокируя остальные.

Наиболее распространенным (и используемым) примитивом является мьютекс. Мьютекс расшифровывается как «Взаимное исключение». Это гарантия, что только один поток может выполнять код. Посмотрим, как мьютексы работают, на примере ниже:

#include #include sf::Mutex mutex; void func() { mutex.lock(); for (int i = 0; i < 10; ++i) std::cout << "I"m thread number one" << std::endl; mutex.unlock(); } int main() { sf::Thread thread(&func); thread.launch(); mutex.lock(); for (int i = 0; i < 10; ++i) std::cout << "I"m the main thread" << std::endl; mutex.unlock(); return 0; }
Этот код использует общий ресурс (std::cout), и, как мы видим, это приводит к нежелательным результатам. Вывод потоков смешался в консоли. Чтобы убедиться в том, что вывод правильно напечатается, вместо того, чтобы быть беспорядочно смешанным, мы защищаем соответствующие области кода мьютексом.

Первый поток, который достигает вызова mutex.lock(), блокирует мьютекс и получает доступ к коду, который печатает текст. Когда другие потоки достигают вызова mutex.lock(), мьютекс уже заблокирован, и другие потоки приостанавливают свое выполнение (это похоже на вызов sf::sleep, спящий поток не потребляет время центрального процессора). Когда первый поток разблокирует мьютекс, второй поток продолжает свое выполнение, блокирует мьютекс и печатает текст. Это приводит к тому, что текст в консоли печатается последовательно и не смешивается.

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

Защита мьютексов

Не волнуйтесь: мьютексы уже потокобезопасны, нет необходимости их защищать. Но они не безопасны в плане исключений. Что происходит, если исключение выбрасывается, когда мьютекс заблокирован? Он никогда не может быть разблокирован и будет оставаться заблокированным вечно. Все потоки, пытающиеся разблокировать заблокированный мьютекс, будут заблокированы навсегда. В некоторых случаях, ваше приложение будет «заморожено».

Чтобы быть уверенным, что мьютекс всегда разблокирован в среде, в которой он (мьютекс) может выбросить исключение, SFML предоставляет RAII класс, позволяющий обернуть мьютекс в класс sf::Lock. Блокировка происходит в конструкторе, разблокировка происходит в деструкторе. Просто и эффективно.

Sf::Mutex mutex; void func() { sf::Lock lock(mutex); // mutex.lock() functionThatMightThrowAnException(); // mutex.unlock(), если функция выбросит исключение } // mutex.unlock()
Помните, что sf::Lock может также быть использован в функциях, которые имеют множество возвращаемых значений.

Sf::Mutex mutex; bool func() { sf::Lock lock(mutex); // mutex.lock() if (!image1.loadFromFile("...")) return false; // mutex.unlock() if (!image2.loadFromFile("...")) return false; // mutex.unlock() if (!image3.loadFromFile("...")) return false; // mutex.unlock() return true; } // mutex.unlock()

Распространенные заблуждения

Вещь, часто упускаемая из виду: поток не может существовать без соответствующего экземпляра

Суть проблемы.

Ваше запущенное приложение в какой то момент начинает активно грузить CPU, вас зовёт тестер и просит починить это!

Какие обычные действия программистов в таком случае?

  • Просят локализовать, если получается, то решить проблему вопрос времени.
  • Начинается добавление логов, счетчиков проходов и тому подобного. Все отдается тестеру или заказчику с требованием воспроизвести и вернуть лог на анализ. Хорошо если воспроизвести удастся и все станет ясно.
  • Предположить время, когда "все работало" и по изменениями в системе контроля версий искать возможные причины.


Как проще поступить вэтом случае?

означает, что какой то поток(и) обработки данных проснулся\запустился, и стал активно выполнять свою работу или иногда просто зациклился. Узнав стек выполнения в момент нагрузки, можно с высокой долей вероятности понять причину такого поведения.

Как же его можно узнать, ведь мы не находимся под отладчиком ?Лично я пользуюсь утилитой Process Explorer дающая возможность увидеть список потоков и их стек . Программа установки не требует.

Для демонстрации я запустил свое приложение с именем процесса "Qocr.Application.Wpf.exe ", в которое добавил фейковый код бесконечного цикла . Теперь давайте найдём причину загрузки ядра без отладчика . Для этого я иду в ствойства процесса, далее:

  1. Переходим на вкладку Threads и видим, что имеется 1 поток, который грузит на 16% CPU .
  2. Выделяем этот поток и жмем Stack, открылось окно "Stack for thread ID ".
  3. В окне видим, что наш поток был создан тут Qocr.Application.Wpf.exe!<>c. b__36_1+0x3a и в данный момент вызывает GetDirectories из метода InitLanguages().

Продемонстрирую действия выше на изображении со стрелками:

Открыв исходный код программы и перейдя к методу InitLanguages можно увидеть мой фейковый код. Зная эту информацию, а именно место отстановки, можно уже принимать меры.

Код стека (из примера выше) вызывающий бесконечный цикл (Можно проверить):

Private void InitLanguages() { new Thread (() => { while (true ) { var dir = Directory .GetDirectories(@"C:\" ); } ; }).Start(); }

Ложка дегтя в бочке с медом.

Два момента, которые стоит знать, если решите воспользоваться способом выше:
  1. Потоки созданные CLR (созданные в коде .NET приложения) после останова не продолжают выполнение. В результате чего поток останавливается и остается висеть до перезапуска программы.
  2. Если стек исполнения не содержит полезной информации, то стоит проделать остановку и просмотр стека несколько раз. Вероятность наткнуться на место зацикливания очень велика.

Рассмотрев методы сжатия, объединения, кэширования и создания параллельных соединений, разумно было бы заняться следующим вопросом: Какая часть страницы должна загружаться вместе с основным HTML-файлом, а какая — только с внешними файлами?

Было собрано тестовое окружение в виде одной страницы, для которой применены различные оптимизационные техники (заодно было получено реальное ускорение для загрузки произвольной страницы и показано, как все эти техники реально влияют на скорость загрузки страницы).

Кроме того, были проведены теоретические выкладки для определения оптимального распределения загрузки по стадиям с учетом всех аспектов.

Реальная ситуация

Рис. 29. Диаграмма загрузки (неизмененного) сайта WebHiTech.ru

Основная идея вариации потока загрузки заключалась в создании минимального количества «белых мест» на диаграмме загрузки. Как видно из рис. 29, около 80% при загрузке страницы составляют простои соединений (естественно, что данный график не отражает реальную загрузку открытых в браузере каналов загрузки, однако, при уточнении картины ситуация практически не меняется). Параллельные загрузки начинаются только после прохождения «узкого места», которое заканчивается (в данном случае) после предзагрузки страницы — после CSS-файла.

Для оптимизации скорости загрузки нам нужно уменьшить число файлов (вертикальные стрелки), загружающихся параллельно, и «сдвинуть» их максимально влево (горизонтальная стрелка). Уменьшение «белых мест» (фактически, уменьшение простоя каналов загрузки), по идее, должно увеличить скорость загрузки за счет ее распараллеливания. Давайте посмотрим, действительно ли это так и как этого добиться.

Шаг первый: простая страница

Вначале бралась обычная страница, для которой использовалось только gzip-сжатие HTML-файла. Это самое простое, что может быть сделано для ускорения загрузки страницы. Данная оптимизация бралась за основу, с которой сравнивалось все остальное. Для тестов препарировалась главная страница конкурса WebHiTech (http://webhitech.ru/) с небольшим количеством дополнительных картинок (чтобы было больше внешних объектов, и размер страницы увеличивался).

В самом начале (head) страницы замеряется начальное время, а по событию window.onload (заметим, что только по нему, ибо только оно гарантирует, что вся страница целиком находится в клиентском браузере) — конечное, затем вычисляется разница. Но этот очень простой пример, перейдем к следующим шагам.

Шаг второй: уменьшаем изображения

Для начала минимизируем все исходные изображения (основные прикладные техники уже были освещены во второй главе). Получилось довольно забавно: суммарный размер страницы уменьшился на 8%, и скорость загрузки возросла на 8% (т.е. получилось пропорциональное ускорение).

Дополнительно с минимизацией картинок была уменьшена таблица стилей (через CSS Tidy) и сам HTML-файл (убраны лишние пробелы и переводы строк). Скриптов на странице не было, поэтому общее время загрузки изменилось не сильно. Но это еще не конец, и мы переходим к третьему шагу.

Шаг третий: все-в-одном

Можно использовать data:URI и внедрить все изображения в соответствующие HTML/CSS-файлы, уменьшив, таким образом, размер страницы (за счет gzip-сжатия, по большому счету, потому что таблица стилей перед этим не сжималась) еще на 15%, однако, время загрузки при этом уменьшилось всего на 4% (при включенном кэшировании, уменьшилось число запросов с 304-ответом). При загрузке страницы в первый раз улучшения гораздо более стабильны: 20%.

CSS-файл, естественно, тоже был включен в HTML, поэтому при загрузке всей страницы осуществлялся только один запрос к серверу (для отображения целой страницы с парой десяткой объектов).

Шаг четвертый: нарезаем поток

Можно попробовать распределить первоначальный монолитный файла на несколько (5- 10) равных частей, которые бы затем собирались и внедрялись прямо в document.body.innerHTML. Т.е. сам начальный HTML-файл очень мал (фактически, содержит только предзагрузчик) и загружается весьма быстро, а после этого стартует параллельная загрузка еще множества одинаковых файлов, которые используют канал загрузки максимально плотно.

Однако, как показали исследования, издержки на XHR-запросы и сборку innerHTML на клиенте сильно превосходят выигрыш от такого распараллеливания. В итоге, страница будет загружаться в 2-5 раз дольше, размер при этом изменяется не сильно.

Можно попробовать использовать вместо XHR-запросов классические iframe, чтобы избежать части издержек. Это помогает, но не сильно. Страница все равно будет загружаться в 2-3 раза дольше, чем хотелось бы.

И немного к вопросу применения фреймов: очень часто наиболее используемые части сайта делают именно на них, чтобы снизить размер передаваемых данных. Как уже упомянуто выше, основная часть задержек происходит из-за большого количества внешних объектов на странице, а не из-за размера внешних объектов. Поэтому на данный момент эта технология далеко не так актуальна, как в 90-е годы прошлого столетия.

Также стоит упомянуть, что при использовании iframe для навигации по сайту встает проблема обновления этой самой навигации (например, если мы хотим выделить какой-то пункт меню как активный). Корректное решение этой проблемы требует от пользователя включенного JavaScript, и оно довольно нетривиально с технической стороны. В общем, если без фреймов можно обойтись при проектировании сайта — значит, их не нужно использовать.

Шаг пятый: алгоритмическое кэширование

Проанализировав ситуацию с первыми тремя шагами, мы видим, что часть ускорения может быть достигнута, если предоставить браузеру возможность самому загружать внешние файлы как отдельные объекты, а не как JSON-код, который нужно как-то преобразовать. Дополнительно к этому всплывают аспекты кэширования: ведь быстрее загрузить половину страницы, а для второй половины проверить запросами со статус- кодами 304, что объекты не изменились. Загрузка всей страницы клиентом в первый раз в данном случае будет немного медленнее (естественно, решение по этому поводу будет зависеть от числа постоянных пользователей ресурса).

В результате удалось уменьшить время загрузки еще на 5%, итоговое ускорение (в случае полного кэша) достигло 20%, размер страницы при этом уменьшился на 21%. Возможно вынесение не более 50% от размера страницы в загрузку внешних объектов, при этом объекты должны быть примерно равного размера (расхождение не более 20%). В таком случае скорость загрузки страницы для пользователей с полным кэшем будет наибольшей. Если страница оптимизируется под пользователей с пустым кэшем, то наилучший результат достигается только при включении всех внешних файлов в исходный HTML.

Итоговая таблица

Ниже приведены все результаты оптимизации для отдельной взятой страницы. Загрузка тестировалась на соединении 100 Кб/с, общее число первоначальных объектов: 23.

Номер шага

Описание

Общий размер (кб)

Время загрузки (мс)

1 Обычная страница. Ничего не сжато (только html отдается через gzip) 63 117
2 HTML/CSS файлы и картинки минимизированы 58 108
3 Один-единственный файл. Картинки вставлены через data:URI 49 104
4 HTML-файл параллельно загружает 6 частей с данными и собирает их на клиенте 49 233
4.5 HTML-файл загружает 4 iframe 49 205
5 Вариант #3, только JPEG-изображения (примерно одинаковые по размеру) вынесены в файлы и загружаются через (new Image()).src в head странице 49 98

Таблица 5. Различные способы параллельной загрузки объектов на странице

Шаг шестой: балансируем стадии загрузки

Итак, как нам лучше всего балансировать загрузку страницы между ее стадиями? Где та «золотая середина», обеспечивающая оптимум загрузки? Начнем с предположения, что у нас уже выполнены все советы по уменьшению объема данных. Это можно сделать всегда, это достаточно просто (в большинстве случаев нужны лишь небольшие изменения в конфигурации сервера). Также предположим, что статика отдается уже с кэширующими заголовками (чтобы возвращать 304-ответы в том случае, если ресурсный файл физически не изменился с момента последнего посещения).

Что дальше? Дальнейшие действия зависят от структуры внешних файлов. При большом (больше двух) числе файлов, подключаемых в страницы, необходимо объединить файлы стилей и файлы скриптов. Ускорение предзагрузки страницы будет налицо.

Если объем скриптов даже после сжатия достаточно велик (больше 10 Кб), то стоит их подключить перед закрывающим , либо вообще загружать по комбинированному событию window.onload (динамической загрузке скриптов посвящено начало седьмой главы). Тут мы, фактически, переносим часть загрузки из второй стадии в четвертую, ускоряется лишь «визуальная» загрузка страницы.

Общее количество картинок должно быть минимальным. Однако тут тоже очень важно равномерно распределить их объем по третьей стадии загрузки. Довольно часто одно изображение в 50-100 Кб тормозит завершение загрузки, разбиение его на 3-4 составляющие способно ускорить общий процесс. Поэтому при использовании большого количества фоновых изображений лучше разбивать их на блоки по 10-20, которые будут загружаться параллельно.

Шаг седьмой: балансируем кэширование

Если все же на странице присутствует больше 10 внешних объектов в третьей стадии (картинок и различных мультимедийных файлов), тут уже стоит вводить дополнительный хост для увеличения числа параллельных потоков. В этом случае издержки на DNS-запрос окупятся снижением среднего времени установления соединения. 3 хоста стоит вводить уже после 20 объектов, и т.д. Всего не более 4 (как показало исследование рабочей группы Yahoo! после 4 хостов издержки, скорее, возрастут, чем снизятся).

Вопрос о том, сколько объема страницы включать в сам HTML-файл (кода в виде CSS, JavaScript или data:URI), а сколько оставлять на внешних объектах, решается очень просто. Баланс в данном случае примерно равен соотношению числа постоянных и единовременных посещений. Например, если 70% пользователей заходят на сайт в течение недели, то примерно 70% страницы должно находиться во внешних объектах и только 30% — в HTML-документе.

Когда страницу должны увидеть только один раз, логично будет включить все в саму страницу. Однако тут уже вступают в силу психологические моменты. Если у среднего пользователя страница при этом будет загружаться больше 3-4 секунд (учитывая время на DNS-запрос и соединение с сервером), то будет необходимо разбиение на две части: первоначальная версия, которая отобразится достаточно быстро, и остальная часть страницы.

Очень важно понимать, какая стадия загрузки при этом оптимизируется и что видит реальный пользователь (с чистым кэшем и, может быть, небыстрым каналом). Подробнее об анализе процесса загрузки страницы на конкретных примерах рассказывается в восьмой главе.

Заключение

Вот так, на примере обычной страницы (уже достаточно хорошо сделанной, стоит отметить) мы добились ускорения ее загрузки еще на 15-20% (и это без учета применения gzip-сжатия для HTML, которое в данном случае дает примерно 10% от общей скорости). Наиболее важные методы уже приведены выше, сейчас лишь можно упомянуть, что при оптимизации скорости работы страницы лучше всегда полагаться на внутренние механизмы браузера, а не пытаться их эмулировать на JavaScript (в данном случае речь идет об искусственной «нарезке» потока). Может быть, в будущем клиентские машины станут достаточно мощными (или же JavaScript-движки — лучше оптимизированными), чтобы такие методы заработали. Сейчас же выбор один — алгоритмическое кэширование.

Понравилась статья? Поделитесь ей