Потоки (threads) и многопоточное выполнение программ (multi-threading)

Пото́к выполне́ния (тред; от англ. thread - нить) - наименьшая единица обработки, исполнение которой может быть назначено ядром операционной системы . Реализация потоков выполнения и процессов в разных операционных системах отличается друг от друга, но в большинстве случаев поток выполнения находится внутри процесса. Несколько потоков выполнения могут существовать в рамках одного и того же процесса и совместно использовать ресурсы, такие как память , тогда как процессы не разделяют этих ресурсов. В частности, потоки выполнения разделяют инструкции процесса (его код) и его контекст (значения переменных, которые они имеют в любой момент времени). В качестве аналогии потоки выполнения процесса можно уподобить нескольким вместе работающим поварам. Все они готовят одно блюдо, читают одну и ту же кулинарную книгу с одним и тем же рецептом и следуют его указаниям, причём не обязательно все они читают на одной и той же странице.

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

Многие современные операционные системы поддерживают как временные нарезки от планировщика процессов, так и многопроцессорные потоки выполнения. Ядро операционной системы позволяет программистам управлять потоками выполнения через интерфейс системных вызовов . Некоторые реализации ядра называют потоком ядра , другие же - легковесным процессом (англ. light-weight process , LWP), представляющим собой особый тип потока выполнения ядра, который совместно использует одни и те же состояния и данные.

Программы могут иметь пользовательское пространство потоков выполнения при создании потоков с помощью таймеров, сигналов или другими методами, позволяющими прервать выполнение и создать временную нарезку для конкретной ситуации (Ad hoc).

Энциклопедичный YouTube

  • 1 / 5

    Потоки выполнения отличаются от традиционных процессов многозадачной операционной системы тем, что:

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

    Многопоточность

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

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

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

    Операционные системы планируют выполнение потоков одним из двух способов:

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

    До конца 1990-х процессоры в настольных компьютерах не имели поддержки многопоточности, так как переключение между потоками, как правило, происходило быстрее, чем полное переключение контекста процесса. Процессоры во встраиваемых системах , которые имеют более высокие требования к поведению в реальном времени , могут поддерживать многопоточность за счёт уменьшения времени на переключение между потоками, возможно, путём распределения выделенных регистровых файлов для каждого потока выполнения, вместо сохранения/восстановления общего регистрового файла. В конце 1990-х идея выполнения инструкций нескольких потоков одновременно, известная как одновременная многопоточность, под названием Hyper-Threading, достигла настольных компьютеров с процессором Intel Pentium 4 . Потом она была исключена из процессоров архитектуры Intel Core и Core 2 , но позже восстановлена в архитектуре Core i7 .

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

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

    Процессы, потоки выполнения ядра, пользовательские потоки и файберы

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

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

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

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

    Проблемы потоков выполнения и файберов

    Параллелизм и структуры данных

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

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

    Ввод-вывод и планирование

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

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

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

    N:1 (потоки выполнения уровня пользователя)

    В модели N:1 предполагается, что все потоки выполнения уровня пользователя отображаются на единую планируемую сущность уровня ядра, и ядро ничего не знает о составе прикладных потоков выполнения. При таком подходе переключение контекста может быть сделано очень быстро, и, кроме того, он может быть реализован даже на простых ядрах, которые не поддерживают многопоточность. Однако, одним из главных недостатков его является то, что в нём нельзя извлечь никакой выгоды из аппаратного ускорения на многопоточных процессорах или многопроцессорных компьютерах, потому что только один поток выполнения может быть запланирован на любой момент времени. Эта модель используется в GNU Portable Threads.

    M:N (смешанная потоковость)

    В модели M:N некоторое число M прикладных потоков выполнения отображаются на некоторое число N сущностей ядра или «виртуальных процессоров». Модель является компромиссной между моделью уровня ядра («1:1») и моделью уровня пользователя («N:1»). Вообще говоря, «M:N» потоковость системы являются более сложной для реализации, чем ядро или пользовательские потоки выполнения, поскольку изменение кода как для ядра, так и для пользовательского пространства не требуется. В M:N реализации библиотека потоков отвечает за планирование пользовательских потоков выполнения на имеющихся планируемых сущностях. При этом переключение контекста потоков делается очень быстро, поскольку модель позволяет избежать системных вызовов. Тем не менее, увеличивается сложность и вероятность инверсии приоритетов, а также неоптимальность планирования без обширной (и дорогой) координации между пользовательским планировщиком и планировщиком ядра.

    POSIX Threads

    Примеры реализаций смешанных потоков

    • «Scheduler activations» используется в собственной библиотеке приложений потоков POSIX для NetBSD (модель M:N в противоположность модели 1:1 ядра или модели приложений пользовательского пространства)
    • Marcel из проекта PM2
    • ОС для суперкомпьютера Tera/Cray MTA

    Примеры реализаций файберов

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

    …). Некоторые языки разрабатываются специально для параллелизма (Ateji PX, CUDA).

    Некоторые интерпретирующие языки программирования, такие, как Руби и CPython (реализация Python), поддерживают потоки, но имеют ограничение, которое известно как глобальная блокировка интерпретатора (GIL). GIL является взаимной блокировкой исключений, выполняемых интерпретатором, которая может уберечь интерпретатор от одновременной интерпретации кода приложений в двух или более потоках одновременно, что фактически ограничивает параллелизм на многоядерных системах (в основном для потоков, связанных через процессор, а не для потоков, связанных через сеть).

    Хотя добросовестное и регулярное проведение сплит-тестов может дать до 10% увеличения конверсии, существуют другие стратегии, способные приносить более ощутимые результаты. В этой статье мы рассмотрим одну из них, основанную на «капитальном ремонте» офферов и структуры маркетинговых воронок с учетом (user flow), проходящего через сайт.

    Вашему вниманию — кейс от Бреда Смита (Brad Smith), являющегося одним из партнеров-основателей агентства Codeless Interactive и завсегдатаем таких проектов, как Kissmetrics, WordStream, AdEspresso и др. Он описал реальные примеры и пошаговый процесс оптимизации пользовательского потока . По результатам внедренных им мероприятий был достигнут 166% прирост новых лидов в течение 3 месяцев (см. иллюстрацию и комментарий ниже):

    Результаты оптимизации пользовательского потока за 3 месяца по сравнению с предыдущим периодом (на средней части картинки показано, что количество привлеченных новых лидов (Contacts) увеличилось с 574 до 1 528, что составляет 166.2% рост)

    Почему сплит-тестирование не всегда приносит ощутимый результат?

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

    Однако зачастую такая радужная ситуация характерна только для частных случаев. Общая же картина на практике выглядит несколько иначе.

    1. Во-первых, «крошечные изменения» в подавляющем большинстве приводят к «крошечным результатам» (если таковые вообще имеются). Более того, полученные в ходе сплит-тестирования результаты показывают одну стойкую, но неутешительную тенденцию: со временем показатели конверсии «откатываются» к средним значениям (см. график и комментарий ниже). Такой вывод был сделан сервисом PPC-аналитики WordStream на основании анализа деятельности тысяч аккаунтов контекстной рекламы AdWords, годовой рекламный бюджет которых составил свыше $3 000 000 000.

    Пример результатов А/Б-теста: на графике синим цветом показан всплеск роста конверсии в начале тестирования и постепенное снижение показателей до среднего уровня, которое произошло в течение определенного временного промежутка

    2. Во-вторых, практика тестирования требует наличия определенного объема данных для анализа. Это нужно, чтобы выборка считалась репрезентативной (достоверной). Например, если ваша посадочная страница не генерирует за месяц хотя бы 1 000 подписчиков, то вы не можете тестировать ее элементы. Точнее говоря, конечно же, это возможно, но полученные результаты нельзя будет считать достоверными из-за недостаточного объема данных. Другой пример: вы проводите А/Б-тест, имея 500 конверсий в месяц; результаты теста дают показатель 250 на 250 по испытываемым вариантам. Очевидно, что такой исход событий не позволяет выявить победителя. Нужен больший объем выборки.

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

    Но и это еще не все…

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

    Как уже отмечалось выше, в качестве альтернативы можно использовать CRO стратегию, позволяющую получить более чем 10%-ный прирост коэффициента конверсии, фокусируясь при этом не на мелочах, а на усовершенствовании всего пользовательского потока, или так называемой « ».

    Как работает оптимизация пользовательского потока?

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

    — слева (для сайта электронной коммерции): контекстная реклама → товарный лендинг или карточка товара в интернет-магазине → покупка товара;
    — справа (для сайта с платной подпиской или посадочной страницы): входящий трафик из социальных сетей → посадочная страница → подписка на email-рассылку

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

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

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

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

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

    При идеальном стечении обстоятельств такой пользовательский опыт может позволить отдельным посетителям найти то, за чем они пришли, и совершить покупку здесь и сейчас. Однако не стоит забывать, что по данным авторитетного маркетингового агентства Moz наиболее лояльными клиентами становятся те, кто посетил сайт порядка 10 раз, а не совершил конверсионное действие в первые 1-3 визита.

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

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

    Но если вы начнете анализировать пользовательские потоки, то сможете неоднократно убедиться, что многие люди даже не заходят на главную страницу. Они либо переходят с объявления контекстной рекламы прямо на landing page, либо вводят в поисковую систему коммерческий запрос и направляются сразу на товарный лендинг или страницу с описанием запрашиваемой услуги.

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

    Три примера пользовательских потоков, где в качестве отправных точек выступают разные источники:
    — слева: ссылка с результатов выдачи поисковых систем → landing page → подписка на рассылку;
    — в центре: прямая ссылка на сайт из закладок или введенная прямо в адресную строку браузера → главная страница → страница с описанием товара или услуги → добавление в корзину → оплата;
    — справа: объявление контекстной рекламы → лендинг → покупка

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

    Как анализировать пользовательский поток вашего сайта?

    Итак, одним из наилучших способов повышения конверсии лендинга является приведение в соответствие рекламного объявления и оффера на посадочной странице. Это означает, что человек, вводящий запрос в поисковую систему, видит релевантное запросу контекстное объявление, и, кликая по нему, он попадает на landing page, контент которой полностью соответствует его содержанию. Таким образом посетитель получает именно то, что он запрашивал в поисковике.

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

    Вот примерный алгоритм того, как это может выглядеть на практике.

    Шаг #1. Определите, откуда приходят пользователи (чтобы понять, что они ищут)

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

    Чтобы смоделировать путешествие потребителя применительно к определенному онлайн-бизнесу, можно использовать сервис Customer Journey to Online Purchase от компании Google, помогающий выяснить, какие маркетинговые каналы чаще всего используют пользователи в той или иной маркетинговой нише (см. иллюстрацию и комментарий ниже):

    Пример интерфейса Customer Journey to Online Purchase, иллюстрирующего, какие каналы используют потребители небольших (опция «Small») сайтов электронной коммерции (опция «Shopping»), действующих с таргетингом на США (опция «The USA»)

    Комментарий к иллюстрации . Перечисленные выше опции можно задавать применительно к разным сферам и географическим локациям веб-сайтов. Чем левее на графике канал, тем менее готовым к покупке будет приходящий из него посетитель. На приведенном примере таковыми являются пользователи, привлеченные из органической выдачи поисковиков (Organic Research). Наиболее горячая целевая аудитория в этой нише — те, кто переходят на сайт по прямой ссылке (Direct), вводя ее в адресную строку браузера или активируя ее из закладок. Поэтому канал Direct расположен крайним справа. Подобным же образом анализируются , показанные ближе к центру графика.

    По словам разработчиков Customer Journey to Online Purchase, исходными данными для этого сервиса являются результаты анализа миллионов пользовательских взаимодействий, собираемых Google Analytics.

    Поэтому логичным продолжением этого шага оптимизации пользовательских потоков на лендинге или сайте должно быть рассмотрение собранных в системе Google Analytics данных вашего проекта (см. колонку «Источники/Каналы» (Sources/Mediums)). Вот как это может выглядеть на реальном примере:

    Несложно заметить, что большинство пользователей приходят на сайт из первых 3 источников: органическая выдача поисковой системы (в таблице — «google/organic»), переходы по прямым ссылкам (в таблице — «direct») и платный поисковый трафик (в таблице — «google/cpc»).

    Если наложить информацию об источниках трафика на данные о популярных страницах — для этого в системе Google Analytics есть специальный отчет «Top Content»,— то можно визуализировать и проследить, как перемещаются по сайту посетители, впервые привлеченные на веб-ресурс:

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

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

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

    2. Второй сегмент — это люди, привлекаемые с помощью платного поискового трафика. Они сразу направляются на landing page. С этой группой все ясно и понятно.

    Шаг #2. Проанализируйте, какие элементы посещаемых страниц работают, а какие — нет

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

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

    Схема, иллюстрирующая систему микроконверсий, которую должен пройти посетитель, прежде чем перейти на более высокий уровень лояльности: (1) главная страница → (2) переход на вложенную страницу через меню сайта → (3) реакция на полезный контент → (4) подписка на лендинге, «заточенном» под посетителей верхнего уровня воронки (TOFU) → (5) благодарственная страница с новым оффером (для перевода на средний уровень воронки (MOFU)) и email с полезным контентом → (6) переход и подписка на лендинге, «заточенном» под посетителей среднего уровня воронки (MOFU) → (7) благодарственная страница с новым оффером (для перевода на нижний уровень воронки (BOFU)) и email с полезным контентом → (8) лояльный подписчик и вход в продажу

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

    Представленная выше схема может показаться слишком усложненной, однако практика показывает, что в подавляющем большинстве случаев необходимо 6-8 «касаний» с посетителем, чтобы добиться окончательной конверсии в покупку. Игнорирование этого принципа часто можно наблюдать, когда через настроенные в Facebook или ВКонтакте рекламные объявления холодный трафик направляется сразу на landing page с коммерческим оффером. Оптимизация пользовательского потока в таком случае предполагает внедрение так называемой системы взращивания клиентов (lead nurturing) — специально разработанной кампании (drip campaign) — включая последовательную серию писем,— направленной на повышение лояльности новых посетителей сайта.

    Когда проблема выявлена, остальное — дело техники. Например, можно использовать доступный функционал в соцсетях, чтобы автоматически добавлять новые контакты для дальнейшего взаимодействия с ними через сервисы почтовой рассылки или Saas-платформы автоматизации маркетинга (MailChimp, Infusionsoft, HubSpot и др.):

    На иллюстрации показано, как с помощью полезной CRM AdEspresso настраивается автоматическое добавление новых контактов из Facebook в HubSpot для последующей работы с лидами

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

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

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

    Карта прокрутки позволяет оценить, насколько далеко посетитель опускается вниз страницы в ходе изучения контента, задействуя скроллинг. Проиллюстрированные выше результаты показывают, что никто не переходит так называемую «линию сгиба» (below the fold), а читает лишь то, что представлено на первом экране

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

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

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

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

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

    Шаг #3. Оптимизируйте дизайн и контент страниц с учетом особенностей пользовательского потока

    Что возникает в вашем воображении, когда вы слышите слово «дизайнер» (designer)? Большинство скажут, что это человек, создающий внешний вид лендинга. Однако у английского слова «designer» есть еще несколько значений, два из которых — конструктор, проектировщик. Поэтому (в глубинном смысле) дизайн посадочной страницы — это не то, как она выглядит, а то, как она работает .

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

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

    На иллюстрации показана тепловая карта лендинга, внешний вид которого разрабатывался без учета того, какое конверсионное действие должен совершить пользователь

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

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

    На иллюстрации изображена таблица распределения кликов среди всех кликабельных элементов страницы (СТА-кнопки, текстовые ссылки, картинки и др.). В подавляющем большинстве случаев (45%) посетители кликают именно на СТА-кнопку (Download Now) в лид-форме. Это является показательным признаком конверсионной страницы

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

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

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

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

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

    Обычно выделяют две общие категории потоков: потоки на уровне пользователя (user-level threads - ULT) и потоки на уровне ядра (kernel-level threads - KLT). Потоки второго типа в литературе иногда называются потоками, поддерживаемыми ядром, или облегченными процессами.

    Потоки на уровне пользователя

    В программе, полностью состоящей из ULT-потоков, все действия по управлению потоками выполняются самим приложением; ядро, по сути, и не подозревает о существовании потоков. На рис. 4.6,а проиллюстрирован подход, при котором используются только потоки на уровне пользователя. Чтобы приложение было многопоточным, его следует создавать с применением специальной библиотеки, представляющей собой пакет программ для работы с потоками на уровне ядра. Такая библиотека для работы с потоками содержит код, с помощью которого можно создавать и удалять потоки, производить обмен сообщениями и данными между потоками, планировать их выполнение, а также сохранять и восстанавливать их контекст.

    Рис. 4.6. Потоки на пользовательском уровне и на уровне ядра

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

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

    1. Приложение, в котором выполняется поток 2, может произвести системный вызов, например запрос ввода-вывода, который блокирует процесс В. В результате этого вызова управление перейдет к ядру. Ядро вызывает процедуру ввода-вывода, переводит процесс В в состояние блокировки и передает управление другому процессу. Тем временем поток 2 процесса В все еще находится в состоянии выполнения в соответствии со структурой данных, поддерживаемой библиотекой потоков. Важно отметить, что поток 2 не выполняется в том смысле, что он работает с процессором; однако библиотека потоков воспринимает его как выполняющийся. Соответствующие диаграммы состояний показаны на рис. 4.7,6.
    2. В результате прерывания по таймеру управление может перейти к ядру; ядро определяет, что интервал времени, отведенный выполняющемуся в данный момент процессу В, истек. Ядро переводит процесс В в состояние готовности и передает управление другому процессу. В это время, согласно структуре данных, которая поддерживается библиотекой потоков, поток 2 процесса В по-прежнему будет находиться в состоянии выполнения. Соответствующие диаграммы состояний показаны на рис. 4.7,в.
    3. Поток 2 достигает точки выполнения, когда ему требуется, чтобы поток 1 процесса В выполнил некоторое действие. Он переходит в заблокированное состояние, а поток 1 - из состояния готовности в состояние выполнения. Сам процесс остается в состоянии выполнения. Соответствующие диаграммы состояний показаны на рис. 4.7,г.


    Рис. 4.7. Примеры взаимосвязей между состояниями потоков пользовательского уровня и состояниями процесса

    В случаях 1 и 2 (см. рис. 4.7,6 и в) при возврате управления процессу В возобновляется выполнение потока 2. Заметим также, что процесс, в котором выполняется код из библиотеки потоков, может быть прерван либо из-за того, что закончится отведенный ему интервал времени, либо из-за наличия процесса с более высоким приоритетом. Когда возобновится выполнение прерванного процесса, оно продолжится работой процедуры из библиотеки потоков, которая завершит переключение потоков и передаст управление новому потоку процесса.

    Использование потоков на пользовательском уровне обладает некоторыми преимуществами перед использованием потоков на уровне ядра. К этим преимуществам относятся следующие:

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

    Использование потоков на пользовательском уровне обладает двумя явными недостатками по сравнению с использованием потоков на уровне ядра.

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

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

    Потоки на уровне ядра

    В программе, работа которой полностью основана на потоках, работающих на уровне ядра, все действия по управлению потоками выполняются ядром. В области приложений отсутствует код, предназначенный для управления потоками. Вместо него используется интерфейс прикладного программирования (application programming interface - API) средств ядра, управляющих потоками. Примерами такого подхода являются операционные системы OS/2, Linux и W2K.
    На рис. 4.6,6" проиллюстрирована стратегия использования потоков на уровне ядра. Любое приложение при этом можно запрограммировать как многопоточное; все потоки приложения поддерживаются в рамках единого процесса. Ядро поддерживает информацию контекста процесса как единого целого, а также контекстов каждого отдельного потока процесса. Планирование выполняется ядром исходя из состояния потоков. С помощью такого подхода удается избавиться от двух упомянутых ранее основных недостатков потоков пользовательского уровня. Во-первых, ядро может одновременно осуществлять планирование работы нескольких потоков одного и того же процесса на нескольких процессорах. Во-вторых, при блокировке одного из потоков процесса ядро может выбрать для выполнения другой поток этого же процесса. Еще одним преимуществом такого подхода является то, что сами процедуры ядра могут быть многопоточными.
    Основным недостатком подхода с использованием потоков на уровне ядра по сравнению с использованием потоков на пользовательском уровне является то, что для передачи управления от одного потока другому в рамках одного и того же процесса приходится переключаться в режим ядра. Результаты исследований, проведенных на однопроцессорной машине VAX под управлением UNIX-подобной операционной системы, представленные в табл. 4.1, иллюстрируют различие между этими двумя подходами. Сравнивалось время выполнения таких двух задач, как (1) нулевое ветвление (Null Fork) - время, затраченное на создание, планирование и выполнение процесса/потока, состоящего только из нулевой процедуры (измеряются только накладные расходы, связанные с ветвлением процесса/потока), и (2) ожидание сигнала (Signal-Wait) - время, затраченное на передачу сигнала от одного процесса/потока другому процессу/потоку, находящемуся в состоянии ожидания (накладные расходы на синхронизацию двух процессов/потоков). Чтобы было легче сравнивать полученные значения, заметим, что вызов процедуры на машине VAX, используемой в этом исследовании, длится 7 us, а системное прерывание - 17 us. Мы видим, что различие во времени выполнения потоков на уровне ядра и потоков на пользовательском уровне более чем на порядок превосходит по величине различие во времени выполнения потоков на уровне ядра и процессов.

    Таблица 4.1. Время задержек потоков (ка)

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

    Комбинированные подходы

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

    Другие схемы

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

    Таблица 4.2. Соотношение между потоками и процессами

    Соответствие нескольких потоков нескольким процессам

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

    1. Реализовать всю программу в виде единого процесса. Такой прямолинейный подход является вполне обоснованным. Недостатки этого подхода связаны с управлением памятью. Эффективно организованный как единое целое процесс может занимать в памяти много места, в то время как для подпрограммы ввода-вывода требуется относительно небольшое адресное пространство. Из-за того что подпрограмма ввода-вывода выполняется в адресном пространстве более объемной программы, во время выполнения ввода-вывода весь процесс должен оставаться в основной памяти, либо операция ввода-вывода будет выполняться с применением свопинга. То же происходит и в случае, когда и основная программа, и подпрограмма ввода-вывода реализованы в виде двух потоков в одном адресном пространстве.
    2. Основная программа и подпрограмма ввода-вывода реализуются в виде двух отдельных процессов. Это приводит к накладным затратам, возникающим в результате создания подчиненного процесса. Если ввод-вывод производится достаточно часто, то необходимо будет либо оставить такой подчиненный процесс активным на все время работы основного процесса, что связано с затратами на управление ресурсами, либо часто создавать и завершать процесс с подпрограммой, что приведет к снижению эффективности.
    3. Реализовать действия основной программы и подпрограммы ввода-вывода как единый поток. Однако для основной программы следует создать свое адресное пространство (свой домен), а для подпрограммы ввода-вывода - свое. Таким образом, поток в ходе выполнения программы будет переходить из одного адресного пространства к другому. Операционная система может управлять этими двумя адресными пространствами независимо, не затрачивая никаких дополнительных ресурсов на создание процесса. Более того, адресное пространство, используемое подпрограммой ввода-вывода, может использоваться совместно с другими простыми подпрограммами ввода-вывода.

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

    Соответствие одного потока нескольким процессам

    В области распределенных операционных систем (разрабатываемых для управления распределенными компьютерными системами) представляет интерес концепция потока как основного элемента, способного переходить из одного адресного пространства в другое (В последние годы активно исследуется тема перехода процессов и потоков из одного адресного пространства в другое (миграция). Эта тема рассматривается в главе 14, "Управление распределенными процессами". ). Заслуживают упоминания операционная система Clouds и, в особенности, ее ядро, известное под названием Ra . В качестве другого примера можно привести систему Emerald .

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

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

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

    Вступление

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

    Вы можете захотеть загрузить файлы из необычного места, такого как сжатый/зашифрованный архив, или, например, из удаленной сетевой папки. Для этих особых ситуаций, SFML предоставляет третью функцию: loadFromStream . Эта функция считывает данные, используя абстрактный интерфейс sf::InputStream , который позволяет вам иметь собственную реализацию класса потока, который будет работать с SFML.

    В этой статье будет рассказано, как писать и использовать ваши собственные классы потоков.

    Стандартные потоки C++?

    Как и многие другие языки, C++ уже содержит класс потока данных: std::istream . По факту, таких классов два: std::istream является только front-end решением, абстрактным интерфейсом к данным, получаемым из std::streambuf .

    К сожалению, эти классы не дружелюбны к пользователю - решение нетривиальной задачи с использованием данных классов может стать очень сложным. Библиотека Boost.Iostreams стремится обеспечить простой интерфейс для стандартных потоков, но Boost - это большая зависимость.

    По этой причине SFML предоставляет собственный потоковый класс, который, как мы надеемся, более простой и быстрый, нежели перечисленные выше.

    В лекции рассматриваются понятие потока (thread) и многопоточное выполнение (multi-threading); модели многопоточности; пользовательские потоки и потоки ядра; потоки в "Эльбрусе", Solaris, Linux, POSIX, Windows 2000, Java.

      Введение

      Однопоточные и многопоточные процессы

      История многопоточности

      Пользовательские потоки и потоки ядра

      Проблемы многопоточности

      Потоки POSIX (Pthreads)

      Потоки и процессы в Solaris

      Потоки в Windows 2000

      Потоки в Linux

      Потоки в Java

      Ключевые термины

      Краткие итоги

      Набор для практики

      • Упражнения

        Темы для курсовых работ, рефератов, эссе

    Введение

    Многопоточность (multi-threading) – одна из наиболее интересных и актуальных тем в данном курсе и, по-видимому, в области ИТ вообще, и, кроме того, одна из излюбленных тем автора. Актуальность данной темы особенно велика, в связи с широким распространением многоядерных процессоров. В лекции рассмотрены следующие вопросы:

      Исторический обзор многопоточности

      Модели многопоточного исполнения

      Проблемы, связанные с потоками

      Потоки в POSIX (Pthreads)

      Потоки в Solaris 2

      Потоки в Windows 2000/XP

      Потоки в Linux

      Потоки в Java и.NET.

    Однопоточные и многопоточные процессы

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

    Рис. 10.1. Однопоточный и многопоточный процессы.

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

    Многопоточность имеет большие преимущества:

      Увеличение скорости (по сравнению с использованием обычных процессов). Многопоточность основана на использовании облегченных процессов (lightweight processes), работающих в общем пространстве виртуальной памяти. Благодаря многопоточности, не возникает больше неэффективных ситуаций, типичных для классической системе UNIX, в которой каждая команда shell (даже команда вывода содержимого текущей директории ls исполнялась как отдельный процесс , причем в своем собственном адресном пространстве. В противоположность облегченным процессам, обычные процессы (имеющие собственное адресное пространство) часть называют тяжеловесными (heavyweight).

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

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

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

    История многопоточности

    Как небезынтересно отметить, один из первых шагов на пути к широкому использованию многопоточности, по-видимому, был сделан в 1970-е годы советскими разработчиками компьютерной аппаратуры и программистами. МВК "Эльбрус-1", разработанный в 1979 году, поддерживал в аппаратуре и операционной системе эффективную концепцию процесса, которая была близка к современному понятию облегченного процесса. В частности, процесс в "Эльбрусе" однозначно характеризовался своим стеком. Иначе говоря, все процессы были облегченными и исполнялись в общем пространстве виртуальной памяти – других процессов в "Эльбрусе" просто не было!

    Концепция многопоточности начала складываться, по-видимому, с 1980-х гг. в системе UNIX и ее диалектах. Наиболее развита многопоточность была в диалекте UNIX фирмы AT&T, на основе которого, как уже отмечалось в общем историческом обзоре, была разработана система Solaris. Все это отразилось и в стандарте POSIX, в который вошла и многопоточность, наряду с другими базовыми возможностями UNIX.

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

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

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

    Платформа.NET, появившаяся в 2000 г., предложила свой механизм многопоточности, который фактически является развитием идей Java.

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

    Пользовательские потоки и потоки ядра

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

      POSIX Pthreads – потоки, специфицированные стандартом POSIX и используемые в POSIX-приложениях (рассмотрены позже в данной лекции);

      Mac C-threads – пользовательские потоки в системе MacOS;

      Solaris threads – пользовательские потоки в ОС Solaris (рассмотрены позже в данной лекции).

    Низкоуровневые потоки, в которые отображаются пользовательские потоки, называются потоками ядра (kernel threads). Они поддержаны и используются на уровне ядра операционной системы. Как и подходы к пользовательским потокам, подходы к архитектуре и реализации системных потоков и к отображению пользовательских потоков в системные в разных ОС различны.Например, собственные модели потоков ядра со своей спецификой реализованы в следующих ОС:

      Windows 95/98/NT/2000/XP/2003/2008/7;

    Существуют различные модели многопоточности – способы отображения пользовательских потоков в потоки ядра. Теоретически возможны (и на практике реализованы) следующие модели многопоточности:

    Модель много / один (many-to-one) – отображение нескольких пользовательских потоков в один и тот же поток ядра. Используется в операционных системах, не поддерживающих множественные системные потоки (например, с целью экономии памяти). Данная модель изображена на рис. 10.2 .

    Рис. 10.2. Схема модели многопоточности "много / один".

    Модель один / один (one-to-one) – взаимно-однозначное отображение каждого пользовательского потока в определенный поток ядра. Примеры ОС, использующих данную модель, - Windows 95/98/NT/2000/XP/2003/2008/7; OS/2. Данная модель изображена на рис. 10.3 .

    Рис. 10.3. Схема модели многопоточности "один / один".

    Модель много / много (many-to-many) – модель, допускающая отображение нескольких пользовательских потоков в несколько системных потоков. Такая модель позволяет ОС создавать большое число системных потоков. Характерным примером ОС, использующей подобную модель, является ОС Solaris, а также Windows NT / 2000 / XP / 2003 / 2008 / 7 с пакетом ThreadFiber . Данная модель изображена на рис. 10.4 .

    Рис. 10.4. Схема модели многопоточности "много / много".

    Проблемы многопоточности

    Многопоточность – весьма сложная, еще не полностью изученная и, тем более, не полностью формализованная область, в которой имеется много интересных проблем. Рассмотрим некоторые из них.

    Семантика системных вызовов fork() и exec(). Как уже отмечалось, в классической ОС UNIX системный вызов fork создает новый "тяжеловесный" процесс со своим адресным пространством, что значительно "дороже", чем создание потока. Однако, с целью поддержания совместимости программ снизу вверх, приходится сохранять эту семантику, а многопоточность вводить с помощью новых системных вызовов.

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

    Обработка сигналов . Сигналы в UNIX – низкоуровневый механизм обработки ошибочных ситуаций. Примеры сигналов: SIGSEGV - нарушение сегментации (обращение по неверному адресу, чаще всего по нулевому); SIGKILL – сигнал процессу о выполнении команды kill его уничтожения. Пользователь может определить свою процедуру-обработчик сигнала системным вызовом signal . Проблема в следующем: как распространяются сигналы в многопоточных программах и каким потоком они должны обрабатываться? В большинстве случаев этот вопрос решается следующим образом: сигнал обрабатывается потоком, в котором он сгенерирован, и влияет на исполнение только этого потока. В более современных ОС (например, Windows 2000 и более поздних версиях Windows), основанных на объектно-ориентированной методологии, концепция сигнала заменена более высокоуровневой концепцией исключения (exception). Исключение распространяется по стеку потока в порядке, обратном порядку вызовов методов, и обрабатывается первым из них, в котором система находит подходящий обработчик. Аналогичная схема обработки исключений реализована в Java и в.NET.

    Группы потоков . В сложных задачах, например, задачах моделирования, при числе разнородных потоков, возникает потребность в их структурировании и помощью концепции группы потоков – совокупности потоков, имеющей свое собственное имя, над потоками которой определены групповые операции. Наиболее удачно, с нашей точки зрения, группы потоков реализованы в Java (с помощью класса ThreadGroup ). Следует отметить также эффективную реализацию пулов потоков (ThreadPool) в.NET.

    Локальные данные потока (thread-local storage - TLS) – данные, принадлежащие только определенному потоку и используемые только этим потоком. Необходимость в таких данных очевидна, так как многопоточность – весьма важный метод распараллеливания решения большой задачи, при котором каждый поток работает над решением порученной ему части. Все современные операционные системы и платформы разработки программ поддерживают концепцию локальных данных потока.

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

    Тупики (deadlocks) и их предотвращение . Как и процессы (см. "Методы взаимодействия процессов " ), потоки могут взаимно блокировать друг друга (т.е. может создаться ситуация deadlock ), при их неаккуратном программировании. Меры по борьбе с тупиками подробно рассмотрены позже в данном курсе.

    Потоки POSIX (Pthreads)

    В качестве конкретной модели многопоточности рассмотрим потоки POSIX (напомним, что данная аббревиатура расшифровывается как Portable Operating Systems Interface of uniX kind – стандарты для переносимых ОС типа UNIX). Многопоточность в POSIX специфицирована стандартом IEEE 1003.1c, который описывает API для создания и синхронизации потоков. Отметим, что POSIX-стандарт API определяет лишь требуемое поведение библиотеки потоков. Реализация потоков оставляется на усмотрение авторов конкретной POSIX-совместимой библиотеки. POSIX-потоки распространены в ОС типа UNIX, а также поддержаны, с целью совместимости программ, во многих других ОС, например, Solaris и Windows NT.

    Стандарт POSIX определяет два основных типа данных для потоков: pthread_t – дескриптор потока ; pthread_attr_t – набор атрибутов потока.

    Стандарт POSIX специфицирует следующий набор функций для управления потоками:

      pthread_create(): создание потока

      pthread_exit(): завершение потока (должна вызываться функцией потока при завершении)

      pthread_cancel(): отмена потока

      pthread_join(): заблокировать выполнение потока до прекращения другого потока, указанного в вызове функции

      pthread_detach(): освободить ресурсы занимаемые потоком (если поток выполняется, то освобождение ресурсов произойдёт после его завершения)

      pthread_attr_init(): инициализировать структуру атрибутов потока

      pthread_attr_setdetachstate(): указать системе, что после завершения потока она может автоматически освободить ресурсы, занимаемые потоком

      pthread_attr_destroy(): освободить память от структуры атрибутов потока (уничтожить дескриптор).

    Имеются следующие примитивы синхронизации POSIX-потоков с помощью мюьтексов (mutexes) – аналогов семафоров – и условных переменных (conditional variables) – оба эти типа объектов для синхронизации подробно рассмотрены позже в данном курсе:

      Pthread_mutex_init() – создание мюьтекса;

      Pthread_mutex_destroy() – уничтожение мьютекса;

      Pthread_mutex_lock() – закрытие мьютекса;

      Pthread_mutex_trylock() – пробное закрытие мьютекса (если он уже закрыт, вызов игнорируется, и поток не блокируется);

      Pthread_mutex_unlock() – открытие мьютекса;

      Pthread_cond_init() – создание условной переменной;

      Pthread_cond_signal() – разблокировка условной переменной;

      Pthread_cond_wait() – ожидание по условной переменной.

    Рассмотрим пример использования POSIX-потоков на языке Си.

    #include

    #include

    #include

    #include

    static void wait_thread(void)

    time_t start_time = time(NULL);

    while (time(NULL) == start_time)

    // никаких действий, кроме занятия процессора на время до 1 с.

    static void *thread_func(void *vptr_args)

    for (i = 0; i < 20; i++) {

    fputs(" b\n", stderr);

    pthread_t thread;

    if (pthread_create(&thread, NULL, thread_func, NULL) != 0) {

    return EXIT_FAILURE;

    for (i = 0; i < 20; i++) {

    if (pthread_join(thread, NULL) != 0) {

    return EXIT_FAILURE;

    return EXIT_SUCCESS;

    Пример иллюстрирует параллельное выполнение основного потока, выдающего в стандартный вывод последовательность букв "a", и дочернего потока, выдающего в стандартный поток ошибок (stderr) последовательность букв "b". Обратите внимание на особенности создания потока (pthread_create), указания его тела (исполняемой процедуры потока thread_func) и ожидания завершения дочернего потока (pthread_join).

    Потоки и процессы в Solaris

    В ОС Solaris, как уже было отмечено, используется модель потоков много / много . Кроме того, в системе используется также уже известное нам понятие облегченный процесс (lightweight process промежуточное между концепцией пользовательского потока и системного потока. Таким образом, в ОС Solaris каждый пользовательский поток отображается в свой облегченный процесс, который, в свою очередь, отображается в поток ядра; последний может исполняться на любом процессоре (или ядре процессора) компьютерной системы. Схема организации потоков в Solaris изображена на рис. 10.5 .

    Рис. 10.5. Потоки в Solaris.

    На рис. 10.6 изображена схема организации процесса в ОС Solaris.

    Рис. 10.6. Процессы в Solaris.

    На схеме видно, что каждый процесс содержит, кроме стандартной информации блока управления процессом, также список всех своих облегченных процессов для управления ими.