Стыковка с нитью означает ожидание ее завершения. Это можно рассматривать как один из способов применения переменных условия.
Функция pthread_join блокирует вызывающую нить до завершения указанной нити. При этом целевая (т.е. вызываемая) нить не должна быть автономной (помеченной на освобождение ресурсов). Если целевая нить уже завершилась, но не является автономной, то возврат из процедуры pthread_join происходит немедленно. После стыковки с целевой нитью она автоматически становится автономной, и отведенную ей память можно восстановить.
В приведенной ниже таблице описаны два возможных случая вызова процедуры pthread_join из нити, в зависимости от состояния и значения атрибута detachstate (автономности) целевой нити.
Целевая нить не автономна
Целевая нить автономна
Целевая нить активна
Вызывающая нить блокирована до завершения целевой нити.
Возврат из процедуры происходит немедленно с выдачей сообщения об ошибке.
Целевая нить завершена
Возврат из процедуры происходит немедленно с выдачей сообщения об успешном завершении.
Приведенный ниже пример представляет собой доработанный вариант первой программы с несколькими нитями. Эта программа завершается после того, как будут выданы пять сообщений на каждом языке. Это достигается за счет блокировки главной нити до завершения нити «writer».
Процедура pthread_join позволяет также передавать информацию из одной нити в другую. При вызове функции pthread_exit или завершении основной процедуры нить возвращает указатель (см. Нормальное завершение работы нити). Этот указатель хранится до тех пор, пока нить не станет автономной, и процедура pthread_join может вернуть его.
Например, команда grep с несколькими нитями может быть реализована следующим образом. Главная нить создает по одной нити для каждого просматриваемого файла, причем процедура точки входа одна и та же для всех нитей. После этого главная нить ожидает завершения всех остальных нитей. Каждая дочерняя нить заносит найденные строки в динамический буфер (свой для каждой нити) и возвращает указатель на этот буфер. Главная нить печатает содержимое всех буферов и освобождает их.
Возвращенный указатель может ссылаться на данные любого типа. Следует соблюдать осторожность при определении типа памяти, на которую ссылается указатель. После завершения нити и освобождения ее памяти значение указателя должно оставаться действительным. Не следует передавать Данные нитей, поскольку при освобождении памяти нити вызывается деструктор.
Кроме того, следует соблюдать осторожность при возврате указателя на область динамической памяти, отведенную для нескольких нитей. Рассмотрим следующий фрагмент кода:
Если этот фрагмент присутствует только в одной нити, то указатель returned_data будет освобожден, как и планировалось. Если же этот фрагмент выполняется одновременно несколькими нитями, указатель returned_data будет освобожден несколько раз, что недопустимо. В подобной ситуации можно ввести флаг, указывающий, что указатель returned_data уже освобожден, и защитить его с помощью взаимной блокировки. Следовательно, строку
следует заменить на следующую (в предположении, что начальное значение переменной flag равно 0)
где для блокирования доступа к критической области может применяться взаимная блокировка (Использование взаимных блокировок). В исправленном примере указатель returned_data будет освобожден только один раз.
При возврате указателя на область динамической памяти, отведенную для нескольких нитей, которые выполняют различный код, следует освободить указатель в одной и только в одной нити.
Современные операционные системы и микропроцессоры уже давно поддерживает многозадачность и вместе с тем, каждая из этих задач может выполняться в несколько потоков. Это дает ощутимый прирост производительности вычислений и позволяет лучше масштабировать пользовательские приложения и сервера, но за это приходится платить цену — усложняется разработка программы и ее отладка.
В этой статье мы познакомимся с POSIX Threads для того, чтобы затем узнать как это все работает в Linux. Не заходя в дебри синхронизации и сигналов, рассмотрим основные элементы Pthreads. Итак, под капотом потоки.
Общие сведения
У всех исполняемых процессов есть как минимум один поток исполнения. Некоторые процессы этим и ограничиваются в тех случаях, когда дополнительные нити исполнения не дают прироста производительности, но только усложняют программу. Однако таких программ с каждым днем становится относительно меньше.
В чем польза множественных потоков исполнения? Возьмем какой-нибудь загруженный веб сервер, например habrahabr.ru. Если бы сервер создавал отдельный процесс для обслуживания каждого http запроса, мы бы ожидали вечно пока загрузится наша страница. Создания нового процесса — дорогостоящее удовольствие для ОС. Даже учитывая оптимизацию за счет копирования при записи, системные вызовы fork и exec создают новые копии страниц памяти и списка файловых описателей. В целом ядро ОС может создать новый поток на порядок быстрее, чем новый процесс.
Таблицы страниц до и после изменения общей страницы памяти во время копирования при записи.
Существует закономерность между количеством параллельных нитей исполнения процесса, алгоритмом программы и ростом производительности. Это зависимость называется Законом Амдаля.
Закон Амдаля для распараллеливания процессов.
Используя уравнение, показанное на рисунке, можно вычислить максимальное улучшение производительности системы, использующей N процессоров и фактор F, который указывает, какая часть системы не может быть распараллелена. Например 75% кода запускается параллельно, а 25% — последовательно. В таком случае на двухядерном процессоре будет достигнуто 1.6 кратное ускорение программы, на четырехядерном процессоре — 2.28571 кратное, а предельное значение ускорения при N стремящемся к бесконечности равно 4.
Отображение потоков в режим ядра
Практически все современные ОС — включая Windows, Linux, Mac OS X, и Solaris — поддерживают управление потоками в режиме ядра. Однако потоки могут быть созданы не только в режиме ядра, но и в режиме пользователя. При использовании этого уровня ядро не знает о существовании потоков — все управление потоками реализуется приложением с помощью специальных библиотек. Пользовательские потоки по разному отображаются на потоки в режиме ядра. Всего существует три модели, из которых 1:1 является наиболее часто используемой.
Отображение N:1
В данной модели несколько пользовательских потоков отображаются на один поток ядра ОС. Все управление потоками осуществляет особая пользовательская библиотека, и в этом преимущество такого подхода. Недостаток же в том, что если один единственный поток выполняет блокирующий вызов, то тогда тормозится весь процесс. Предыдущие версии Solaris OS использовали такую модель, но затем вынуждены были от нее отказаться.
Отображение 1:1
Это самая проста модель, в которой каждый поток созданный в каком-нибудь процессе непосредственно управляется планировщиком ядра ОС и отображается на один единственный поток в режиме ядра. Чтобы приложение не плодило бесконтрольно потоки, перегружая ОС, вводят ограничение на максимальное количество потоков поддерживаемых в ОС. Данный способ отображения потоков поддерживают ОС Linux и Windows.
Отображение M:N
При таком подходе M пользовательских потоков мультиплексируются в такое же или меньшее N количество потоков ядра. Преодолеваются негативные эффекты двух других моделей: нити по-настоящему исполняются параллельно и нет необходимости в ОС вводить ограничения на их общее количество. Вместе с тем данную модель довольно трудно реализовать с точки зрения программирования.
Потоки POSIX
В конце 1980-х и начале 1990-х было несколько разных API, но в 1995 г. POSIX.1c стандартизовал потоки POSIX, позже это стало частью спецификаций SUSv3. В наше время многоядерные процессоры проникли даже в настольные ПК и смартфоны, так что у большинства машин есть низкоуровневая аппаратная поддержка, позволяющая им одновременно выполнять несколько потоков. В былые времена одновременное исполнение потоков на одноядерных ЦПУ было лишь впечатляюще изобретательной, но очень эффективной иллюзией.
Pthreads определяет набор типов и функций на Си.
Из man errno Переменная errno определена в стандарте ISO C как изменяемое lvalue int и не объявляемая явно; errno может быть и макросом. Переменная errno является локальным значением нити; её изменение в одной нити не влияет на её значение в другой нити.
Создание потока
При удачном завершении pthread_create() возвращает код 0, ненулевое значение сигнализирует об ошибке.
Рассмотрим теперь пример многопоточной программы.
Завершение потока
Поток завершает выполнение задачи когда:
Синтаксис проще, чем при создании потока.
Ожидание потока
При удачном завершении pthread_join() возвращает код 0, ненулевое значение сигнализирует об ошибке.
Досрочное завершение потока
При удачном завершении pthread_cancel() возвращает код 0, ненулевое значение сигнализирует об ошибке.
Небольшая иллюстрация создания и отмены потока.
Чтобы не создалось впечатление, что тут царит произвол и непредсказуемость результатов данного вызова, рассмотрим таблицу параметров, которые определяют поведение потока после получения вызова на досрочное завершение.
Отсоединение потока
При удачном завершении pthread_detach() возвращает код 0, ненулевое значение сигнализирует об ошибке.
Потоки versus процессы
Напоследок предлагаю рассмотреть несколько соображений на тему, следует ли проектировать приложение многопоточным или запускать его в несколько процессов с одним потоком? Сперва выгоды параллельных множественных потоков.
В начальной части статьи мы уже указывали на эти преимущество, поэтому вкратце их просто перечислим.
Теперь немного о недостатках.
Тема потоков практически бездонна, даже основы работы с потоками может потянуть на пару лекций, но мы уже знаем достаточно, чтобы изучить структуру многопоточных приложений в Linux.
В данном примере внутри основного потока, в котором работает функция main, создаётся новый поток, внутри которого вызывается функция helloWorld. Функция helloWorld выводит на дисплей приветствие. Внутри основного потока также выводится приветствие. Далее потоки объединяются.
Новый поток создаётся с помощью функции pthread_create
Функция получает в качестве аргументов указатель на поток, переменную типа pthread_t, в которую, в случае удачного завершения сохраняет id потока. pthread_attr_t – атрибуты потока. В случае если используются атрибуты по умолчанию, то можно передавать NULL. start_routin – это непосредственно та функция, которая будет выполняться в новом потоке. arg – это аргументы, которые будут переданы функции.
Поток может выполнять много разных дел и получать разные аргументы. Для этого функция, которая будет запущена в новом потоке, принимает аргумент типа void*. За счёт этого можно обернуть все передаваемые аргументы в структуру. Возвращать значение можно также через передаваемый аргумент.
В случае успешного выполнения функция возвращает 0. Если произошли ошибки, то могут быть возвращены следующие значения
Пройдём по программе
Здесь мы задаём набор значений, необходимый для обработки возможных ошибок.
Это функция, которая будет работать в отдельном потоке. Она не будет получать никаких аргументов. По стандарту считается, что явный выход из функции вызывает функцию pthread_exit, а возвращаемое значение будет передано при вызове функции pthread_join, как статус.
Здесь создаётся и сразу же исполняется новый поток. Поток не получает никаких атрибутов или аргументов. После создания потока происходит проверка на ошибку.
Приводит к тому, что основной поток будет ждать завершения порождённого. Функция
Откладывает выполнение вызывающего (эту функцию) потока, до тех пор, пока не будет выполнен поток thread. Когда pthread_join выполнилась успешно, то она возвращает 0. Если поток явно вернул значение (это то самое значение SUCCESS, из нашей функции), то оно будет помещено в переменную value_ptr. Возможные ошибки, которые возвращает pthread_join
Пример создания потоков с передачей им аргументов
П усть мы хотим передать потоку данные и вернуть что-нибудь обратно. Скажем, передавать потоку будем строку, а возвращать из потока длину этой строки.
Так как функция может получать только указатель типа void, то все аргументы следует упаковать в структуру. Определим новый тип структуру:
Здесь id – это идентификатор потока (он в общем-то не нужен в нашем примере), второе поле это строка, а третье длина строки, которую мы будем возвращать.
Внутри функции приводим аргумент к нужному типу, выводим на печать строку и засовываем в структуру обратно вычисленную длину строки.
В том случае, если всё прошло удачно, то в качестве статуса возвращаем значение SUCCESS, а если была допущена ошибка (в нашем случае, если передана нулевая строка), то выходим со статусом BAD_MESSAGE.
В этом примере создадим 4 потока. Для 4-х потоков понадобятся массив типа pthread_t длинной 4, массив передаваемых аргументов и 4 строки, которые мы и будем передавать.
Первым делом заполняем значения аргументов.
Далее создаём в цикле новые потоки
Затем ждём завершения
Под конец ещё выводим аргументы, которые теперь хранят возвращённые значения. Заметьте, что один из аргументов «плохой» (строка равна NULL). Вот полный код
Выполните его несколько раз. Заметьте, что порядок выполнения потоков не детерминирован. Запуская программу, можно каждый раз получить другой порядок выполнения.
Поток — это подпроцесс, который обрабатывает определенную часть кода и владеет его буфером. В этом руководстве мы обсудим «pthread_join» и некоторые из его примеров. Набор потоков — это набор потоков, которые выполняются в очень похожей операции. Внутри метода поток представляет собой отдельный последовательный поток. Потоки часто называют несерьезными процессами, поскольку они имеют несколько общих характеристик процессов. Потоки, в отличие от процессов, на самом деле не автономны друг от друга, поэтому они связывают свои сценарии, информацию и службы ОС, такие как открытые документы и триггеры, с другими потоками. Выполнение pthread можно получить через компилятор gcc. Прежде чем идти дальше, вы должны понять две концепции многопоточности POSIX, которые мы будем использовать в сегодняшней теме.
Pthread_create
Когда бы ни запускался многопоточный код, в нем работает только один процесс, который выполняет операцию main () программы. У этого потока есть идентификатор процесса, и теперь он является заполненным потоком. Для создания нового потока в скрипте необходимо использовать метод pthread_create ().
Pthread_join
Для потоков метод pthread_join () идентичен ожиданию функций. Вызывающий поток блокируется до завершения потока со спецификатором, эквивалентным первому оператору.
Установите компилятор GCC
При работе в системе Linux у вас должен быть установлен какой-нибудь компилятор для компиляции кода C. Наиболее рекомендуемый — компилятор GCC. Поэтому войдите в систему из системы Linux и откройте консольный терминал, используя «Ctrl + Alt + T». Вы также можете открыть его из строки поиска в области действий. Теперь терминал открыт, выполните следующую команду установки, чтобы компилятор «gcc» установил его. Добавьте пароль своей учетной записи по запросу и нажмите клавишу «Ввод». Теперь компилятор gcc установлен; мы попробуем несколько примеров, чтобы развить концепцию «pthread_join».
Пример 1
Мы должны создать новый файл «one» в редакторе GNU Nano с расширением «c». Это потому, что мы будем работать над языком C. Попробуйте выполнить приведенную ниже инструкцию.
Введите показанный ниже сценарий в файл nano. Код состоит из некоторых библиотек, которые будут использоваться для многопоточности POSIX, особенно «pthread.h». Мы создали метод «Поток». Поток засыпает на 1 секунду и печатает выписку. После этого основная функция была создана. Переменная «id» использовалась как тип «pthread_t» для распознавания потока. Затем будет выполнен оператор печати, и будет создан поток POSIX с использованием функции «pthread_create». Эта функция имеет 4 значения аргумента. Один из них — это указатель на переменную id, а третий — на выполняемую функцию Thread. Все остальные по умолчанию. Был использован другой оператор печати, и основной метод завершился.
Сохраните файл nano и выйдите, используя «Ctrl + S» и «Ctrl + X» соответственно. Скомпилируем код с помощью компилятора «gcc». Но убедитесь, что на этот раз вы должны использовать в команде флаг «-lpthread». В противном случае код не будет компилироваться и выполняться. Выполните следующий запрос.
Теперь запустите сценарий с помощью инструкции «a.out», как показано ниже. Всякий раз, когда код был выполнен, основная функция работает первой. Итак, оператор печати был выполнен, и на терминале отобразилось «Перед потоком». Затем была выполнена функция «pthread_create», и она создала новый поток, который использует функцию «Thread». После этого метод «pthread_join» был использован для перемещения элемента управления в функцию «Thread». В методе «Thread» программа находится в спящем режиме на 1 секунду, а затем выполняет оператор печати, из-за чего терминал отображает «Within Thread». После выполнения функции «Резьба» элемент управления снова переместился в основную функцию. И оператор печати в основной функции был выполнен как «After Thread».
Пример 2
Возьмем еще один пример функции «pthread_join». На этот раз мы не будем использовать значения по умолчанию в качестве аргументов для потока. Мы присвоим потоку правильные значения. Создайте еще один файл «two.c» в редакторе nano, который будет использоваться для скрипта языка C следующим образом:
Запишите в редакторе приведенный ниже код C. Мы определили функцию «Thread» без какой-либо реализации. Основная функция запускается с указанием некоторых целочисленных переменных «i1» и «i2». Эти две переменные целочисленного типа будут использоваться как дескрипторы. Были использованы два идентификатора типа «pthread», «t1» и «t2», и другие переменные символьного типа. Две функции «pthread_create» указаны для создания двух потоков по отдельности с использованием идентификатора потока и «сообщений» в качестве их параметров. Функция «Thread» определяется как функция потока, в которую были переданы параметры. Метод «Thread» примет аргументы и распечатает сообщение. Затем используются два метода pthread_join для ограничения текущей функции. Два оператора печати покажут несколько сообщений, и основная функция завершится.
Скомпилируйте файл «two.c» с «gcc» вместе с флагом «-lpthread» следующим образом:
Давайте выполним код с помощью приведенной ниже команды в консоли. Вывод отображает результат первых двух операторов печати основной функции как «Поток 1» и «Поток 2». Затем, в связи с созданием потоков, управление переходит к функции «Thread». После выполнения метода «Thread» он возвращается к основной функции, и два других оператора печати выполняются.
Заключение
Помимо реальной машины, поток обычно разделяет свое хранилище с несколькими другими потоками (хотя для задач у нас обычно есть полюсные зоны хранения для каждого из них). Все они имеют ссылки на очень идентичные глобальные переменные, пространство кучи, дескрипторы документов и т.д., Поскольку они совместно используют хранилище.
printf(«Истекли 25 секунд, поток %d все еще выполняется»
printf(«Поток %d завершен (как и ожидалось!) «,
Мы применили макроопределение SIGEV_UNBLOCK_INIT() для инициализации структуры события, но можно было установить sigev_notify в SIGEV_UNBLOCK и «вручную». Можно было даже сделать еще более изящно, передав NULL вместо struct sigevent — функция TimerTimeout() понимает это как знак, что нужно использовать SIGEV_UNBLOCK.
Если поток (заданный в thread_id) остается работающим более 10 секунд, то системный вызов завершится по тайм-ауту — функция pthread_join() возвратится с ошибкой, установив errno в ETIMEDOUT.
Вы можете использовать и другую «стенографию», указав NULL в качестве значения тайм-аута (параметр ntime в декларации выше), что предпишет ядру не блокироваться в данном состоянии. Этот прием можно использовать для организации программного опроса. (Хоть программный опрос и считается дурным тоном, его можно весьма эффективно использовать в случае с pthread_join(), периодически проверяя, завершился ли нужный поток. Если нет, можно пока сделать что-нибудь другое.)
Ниже представлен пример программы, в которой демонстрируется неблокирующий вызов pthread_join():
int pthread_join_nb(int tid, void **rval) <
return (pthread_join(tid, rval));
Тайм-ауты ядра при обмене сообщениями
Все становятся несколько сложнее, когда вы используете тайм-ауты ядра при обмене сообщениями. Вспомните главу «Обмен сообщениями», раздел «Обмен сообщениями и модель «клиент/сервер») — на момент отправки клиентом сообщения сервер может как ожидать его, так и нет. Это означает, что клиент может заблокироваться как по передаче (если сервер еще не принял сообщение), так и по ответу (если сервер принял сообщение, но еще не ответил). Основной смысл здесь в том, что вы должны предусмотреть оба блокирующих состояния в параметре flags функции TimerTimeout(), потому что клиент может оказаться в любом из них.
Чтобы задать несколько состояний, сложите их операцией ИЛИ (OR):
Это вызовет тайм-аут всякий раз, когда ядро переведет клиента в состояние блокировки по передаче (SEND) или по ответу (REPLY). В тайм-ауте SEND-блокировки нет ничего особенного — сервер еще не принял сообщение, значит, ничего для этого клиента он не делает. Это значит, что если ядро генерирует тайм-аут для SEND-блокированного клиента, сервер об этом информировать не обязательно. Функция MsgSend() клиента возвратит признак ETIMEDOUT и обработка тайм-аута завершится.
Однако, как было упомянуто в главе «Обмен сообщениями» (параграф «_NTO_CHF_UNBLOCK»), если сервер уже принял сообщение клиента, и клиент желает разблокироваться, для сервера существует два варианта реакции. Если сервер не указал флаг _NTO_CHF_UNBLOCK на канале, по которому было принято сообщение, клиент будет разблокирован немедленно, и сервер не получит об этом никакого оповещения. У большинства серверов, которые мне доводилось встречать, флаг _NTO_CHF_UNBLOCK был всегда установлен. В этом случае ядро посылает серверу импульс, а клиент остается заблокированным до тех пор, пока сервер ему не ответит! Как было показано в вышеупомянутом разделе главы «Обмен сообщениями», это сделано для того, чтобы сервер мог узнать о запросе клиента на разблокирование и выполнить по этому поводу какие-то действия.
Читайте также
Тайм-ауты ядра
Тайм-ауты ядра QNX/Neutrino позволяет вам получать тайм-ауты по всем блокированным состояниям. Мы обсуждали эти состояния в главе «Процессы и потоки» в разделе «Состояния потоков». Наиболее часто у вас может возникнуть потребность в этом при обмене сообщениями: клиент,
14.2. Тайм-ауты сокета
14.2. Тайм-ауты сокета Существует три способа установки тайм-аута для операции ввода-вывода через сокет.1. Вызов функции alarm, которая генерирует сигнал SIGALRM, когда истекает заданное время. Это подразумевает обработку сигналов, которая может варьироваться от одной
Тайм-аут для функции recvfrom (сигнал SIGALRM)
Тайм-аут для функции recvfrom (сигнал SIGALRM) В листинге 14.2 показана новая версия функции dg_cli, приведенной в листинге 8.4, в которую добавлен вызов функции alarm для прерывания функции recvfrom при отсутствии ответа в течение 5 с.Листинг 14.2. Функция dg_cli, в которой при установке тайм-аута
Тайм-аут для функции recvfrom (функция select)
Тайм-аут для функции recvfrom (функция select) Мы демонстрируем вторую технологию для установки тайм-аута (использование функции select) в листинге 14.3. Здесь показана наша функция readable_timeo, которая ждет, когда дескриптор станет готов для чтения, но не более заданного числа
Функция pthread_join
Функция pthread_join Мы можем приостановить выполнение текущего потока и ждать завершения выполнения какого-либо другого потока, используя функцию pthread_join. Сравнивая потоки и процессы Unix, можно сказать, что функция pthread_create аналогична функции fork, а функция pthread_join — функции
8.3.2 Внутренние системные тайм-ауты
8.3.2 Внутренние системные тайм-ауты Некоторым из процедур ядра, в частности драйверам устройств и сетевым протоколам, требуется вызов функций ядра в режиме реального времени. Например, процесс может перевести терминал в режим ввода без обработки символов, при котором
6.14.5 Тайм-аут сборки датаграммы
6.14.5 Тайм-аут сборки датаграммы Рассмотрим следующую последовательность событий:? Пересылается датаграмма.? Пославший ее процесс аварийно завершается.? Датаграмма фрагментируется при пересылке.? По пути следования теряется один из фрагментов.При потере отправленного
10.11.1 Тайм-аут
10.11.1 Тайм-аут Работа партнера по соединению может завершиться крахом либо полностью прерваться вследствие неисправности шлюза или связи. Чтобы предотвратить повторную пересылку данных в TCP, существует несколько механизмов.Достигнув первого порогового значения для
10.13.5 Тайм-аут повторной пересылки
10.13.5 Тайм-аут повторной пересылки После отправки сегмента TCP устанавливает таймер и отслеживает поступление ACK. Если ACK не получен в течение периода тайм-аута, TCP выполняет повторную пересылку сегмента (ретрансляцию). Однако каким должен быть период тайм-аута?Если он
16.5. Тайм-аут и повторная передача
16.5. Тайм-аут и повторная передача Рассмотрим стратегию обработки тайм-аутов и повторной передачи, используемую в средствах Sun RPC. Существуют два значения тайм-аутов:1. Общий тайм-аут определяет время ожидания ответа сервера клиентом. Это значение используется протоколами
19.3. Установка тайм-аута выбора операционной системы. Редактирование параметров ядра Linux
19.3. Установка тайм-аута выбора операционной системы. Редактирование параметров ядра Linux По умолчанию GRUB2 не отображает меню выбора операционной системы. Следовательно, вы не можете ни выбрать другую операционную систему (в том числе и Windows), ни изменить параметры ядра Linux,
1 Принципы тайм-менеджмента
1 Принципы тайм-менеджмента Погодите! Прежде чем начать, давайте кое-что сделаем для уверенности, что мы действительно закончим.Я прекрасно понимаю, что вы как системный администратор постоянно подвергаетесь прерываниям. То телефон зазвонит, то клиент[1] обратится с
Трудности тайм-менеджмента
Трудности тайм-менеджмента Вот теперь можно начинать!Тайм-менеджмент труден для сисадминов в первую очередь потому, что нашу работу постоянно прерывают. Как довести дело до конца, если нам все время приходится бросать его, чтобы устранить проблему или ответить на вопрос,
Принципы тайм-менеджмента для системных администраторов
Принципы тайм-менеджмента для системных администраторов Есть шесть принципов, на которых я основываю все свои приемы тайм-менеджмента. Не утверждаю, что какой-либо из них открыт мною, но я определенно причастен к их развитию. Вы легко проследите эти принципы на