НИТИ И СТАНДАРТНЫЕ БИБЛИОТЕКИ Unix Программирование с использованием POSIX thread library По завершении этого раздела вы сможете: • использовать стандартные библиотеки или их аналоги в многопоточных программах • находить в документации информацию о том, является ли данная функция или группа функций thread-safe • использовать сигналы и fork в многопоточных программах fork(2) в многопоточной среде • нити создаются в рамках процесса • fork(2) дублирует все состояние процесса • что происходит с нитями? – fork1 (дублируется только та нить, которая позвала fork) – forkall (дублируются все нити процесса) • в стандарте POSIX и Solaris 10 fork(2)≡fork1 • в старых версиях Solaris, fork(2)≡forkall. • в других реализациях POSIX threads эквивалента forkall может вообще не быть fork1(2) • дублируется только нить, вызвавшая fork1(2) • сохраняются все блокировки, установленные остальными нитями (в том числе блокировки, скрытые внутри библиотечных функций) • вызов таких функций может привести к мертвой блокировке (будете ждать нити, которой не существует) pthread_atfork(3C) ИСПОЛЬЗОВАНИЕ #include <sys/types.h> #include <unistd.h> int pthread_atfork(void (*prepare) (void), void (*parent) (void), void (*child) (void)); ОПИСАНИЕ Регистрирует обработчики, вызываемые перед fork1 (prepare) и после него (parent, child). Порядок вызовов atfork имеет значение. Обработчики prepare вызываются в порядке LIFO, обработчики parent/child в порядке FIFO. Как сделать библиотеку fork-safe • Определите все блокировки, используемые библиотекой и порядок их захвата (L1..Ln) • Напишите функции f1, f2 и f3 – f0() { lock(L1); … lock(Ln); } – fp() { unlock(L1); … unlock(Ln); } – fc() { unlock(L1); … unlock(Ln); } • Включите вызов pthread_atfork(f0, fp, fc) в код инициализации библиотеки (секцию .init для библиотек ELF) Почему так? • Если функция использует блокировку, значит, она использует внутренние данные, которые могут быть в несогласованном состоянии • Прежде чем снимать блокировку, нам нужно дождаться завершения этой функции (если она вызвана в какой-то другой нити). Сигналы и потоки • Сигналы делятся на синхронные и асинхронные • Синхронные сигналы возникают при исполнении определенного кода в вашей программе (напр. SIGFPE при делении на 0) • Асинхронные сигналы возникают по внешним причинам Сигналы и потоки (продолжение) • синхронные сигналы обрабатываются в том потоке, в котором возникли • асинхронные сигналы обрабатываются в любом потоке • необработанные сигналы вызывают реакцию по умолчанию для всего процесса (завершение всего процесса, засыпание всего процесса и т.д.) signal(2) и sigset(2) • вызовы signal(2) и sigset(2) устанавливают глобальный обработчик сигнала (во всех нитях процесса) • установить собственный обработчик сигнала нить не может. Проблемы, связанные с сигналами • возможность мертвой блокировки – нить держит блокировку – прилетает сигнал – обработчик сигнала вызывает функцию, которая пытается захватить ту же блокировку – нить ждет сама себя • Атрибут MT-Level==Async-Signal-Safe Проблемы, связанные с сигналами (продолжение) • вызов setjmp/longjmp – если setjmp вызывался в одной нити, – а longjmp в другой – это приведет к разрушению стека (скорее всего, SIGSEGV, но не обязательно, программа может исполнить какой-то еще код и, например, записать мусор в файлы и т.д.) pthread_sigmask(3C) ИСПОЛЬЗОВАНИЕ #include <pthread.h> #include <signal.h> int pthread_sigmask(int how, const sigset_t *set, sigset_t *oset); ОПИСАНИЕ функционально аналогична sigprocmask(2), но устанавливает маску сигналов нити маска сигналов нити наследуется при pthread_create sigprocmask/pthread_sigmask how SIG_BLOCK - Множество сигналов, на которое указывает set, будет добавлено к текущей маске. SIG_UNBLOCK - Множество set будет удалено из текущей маски. SIG_SETMASK - Текущая маска будет заменена на set. sigsetops(3C) ИСПОЛЬЗОВАНИЕ #include <signal.h> int sigemptyset(sigset_t * set); int sigfillset(sigset_t * set); int sigaddset(sigset_t * set, int signo); int sigdelset(sigset_t * set, int signo); int sigismember(sigset_t * set, int signo); ВОЗВРАЩАЕМОЕ ЗНАЧЕНИЕ успех - sigismember: 1 если истинно, 0 если ложно; остальные функции: 0 неуспех - -1 и errno установлена ЧТО ОЗНАЧАЕТ THREAD-SAFE ? • словосочетание thread-safe (MT-safe) не имеет общепринятого русского перевода • дословный перевод – безопасно [для] использования в многопоточной программе • стандартные библиотеки Unix и ANSI C разрабатывались до появления многопоточности • не все функции стандартных библиотек корректно работают в многопоточной среде Пример – strtok(3C) ИСПОЛЬЗОВАНИЕ #include <strings.h> char *strtok(char *restrict s1, const char *restrict s2); ВОЗВРАЩАЕМОЕ ЗНАЧЕНИЕ при первом вызове, возвращает первое слово в строке s1, используя разделители, указанные в s2. При втором вызове возвращает второе слово и т.д. Если достигнут конец строки, возвращает NULL Пример – strtok_r(3C) ИСПОЛЬЗОВАНИЕ #include <strings.h> char *strtok_r(char *restrict s1, const char *restrict s2, char **lasts); ПРИМЕЧАНИЕ хранит состояние строки в ячейке памяти, на которую указывает параметр lasts Примечание • В действительности, strtok в Solaris использует thread-specific data для хранения указателя на строку, поэтому в Solaris эта функция thread-safe. Но это не обязательно верно для других реализаций. Как узнать, является ли функция thread-safe? • Секция ATTRIBUTES в странице руководства ATTRIBUTES See attributes(5) for descriptions of the butes: following attri- ____________________________________________________________ | ATTRIBUTE TYPE | ATTRIBUTE VALUE | |_____________________________|_____________________________| | Interface Stability | See below. | |_____________________________|_____________________________| | MT-Level | See below. | |_____________________________|_____________________________| The strlcat() and strlcpy() functions remaining functions are Standard. are Stable. The The strtok() and strdup() functions are MT-Safe. The remaining functions are Async-Signal-Safe. Значения атрибута MT-level • • • • • • • • Unsafe Safe MT-Safe Async-Signal-Safe MT-Safe with exceptions Safe with exceptions Fork-Safe Cancel-Safety (Deferred- и Asynchronous-) MT Level Unsafe • Функция или группа функций использует статические или глобальные переменные, не защищенные примитивами синхронизации. • Требует явной защиты мутексами или другими средствами синхронизации MT Level Safe • Функции библиотеки сами по себе реентерабельны, но могут обеспечивать недостаточный уровень параллелизма. • Например, Safe функция может использовать внутренние мутексы, удерживаемые длительное время или в удерживаемые в промежутках между вызовами функций MT Level MT-Safe • Функция или группа функций полностью готова для работы в многопоточной среде. • Обеспечивает разумный уровень параллелизма, т.е оптимизирована так, чтобы удерживать внутренние блокировки (если они есть) минимально возможное время MT Level Async-Signal-Safe • Подразумевает MT-Safe • Может вызываться из обработчика сигнала. • Для MT-Safe это не всегда так. Если MT-Safe функция держит блокировку и в это время в той же нити вызовется обработчик сигнала, это может привести к мертвой блокировке. MT Level Fork-Safe • подразумевает MT-Safe • при fork(2) в дочернем процессе остается только та нить, которая вызвала fork • блокировки, удерживаемые исчезнувшими нитями, остаются • если библиотека MT-Safe за счет использования блокировок, она может быть не Fork-Safe Пример – readdir(3C) ИСПОЛЬЗОВАНИЕ #include <sys/types.h> #include <dirent.h> struct dirent *readdir(DIR *dirp); ВОЗВРАЩАЕМОЕ ЗНАЧЕНИЕ Возвращает указатель на статический буфер, перезаписывает его при последующих вызовах. readdir_r(3C) ИСПОЛЬЗОВАНИЕ #include <sys/types.h> #include <dirent.h> struct dirent *readdir_r(DIR *dirp, struct dirent *entry); ВОЗВРАЩАЕМОЕ ЗНАЧЕНИЕ Возвращает NULL, если достигнут конец каталога, и entry, если есть следующая запись readdir_r(3C) – стандартная форма ИСПОЛЬЗОВАНИЕ #include <sys/types.h> #include <dirent.h> cc file ... -D_POSIX_PTHREAD_SEMANTICS int readdir_r(DIR *restrict dirp, struct dirent *restrict entry, struct dirent **restrict result); ВОЗВРАЩАЕМОЕ ЗНАЧЕНИЕ успешное завершение – 0 ошибка - -1 Более сложный пример Int fd; fd=open(fname, flags); pthread_create(thread1, …); pthread_create(thread2, …); Thread1: Thread2: lseek(fd, SEEK_SET, pos1); write(fd, buf, size); lseek(fd, SEEK_SET, pos2); write(fd, buf2, size); pread (2) ИСПОЛЬЗОВАНИЕ #include <unistd.h> int pread( int fildes, void *buf, unsigned nbyte, off_t offset); ВОЗВРАЩАЕМОЕ ЗНАЧЕНИЕ успех - количество прочитанных байт неуспех - -1 и errno установлена не перемещает указатель в файле pwrite (2) ИСПОЛЬЗОВАНИЕ #include <unistd.h> int pwrite( int fildes, const void *buf, unsigned nbyte, off_t offset); ВОЗВРАЩАЕМОЕ ЗНАЧЕНИЕ успех - количество записанных байт неуспех - -1 и errno установлена