один интерфейс функции не в состоянии сделать ее безопасной. Кроме того, программист может столкнуться с увеличением числа потоков в среде, которая изначально использовала функции, предназначенные для функционирования в однопоточной среде. В таких условиях обычно используются мьютексы. Например, программа имеет три параллельно выполняемых потока. Два из них, thread1 и thread2, параллельно выполняют функцию funcA(), которая не является безопасной для одновременной работы потоков. Третий поток, thread3, выполняет функцию funcB (). Для решения проблемы, связанной с функцией funcA (), возможно, достаточно заключить в защитную оболочку мьютекса доступ к ней со стороны потоков threadl и thread2:
thread1 thread2 thread3
{ { {
lock() lock() funcB()
funcA() funcA() }
unlock() unlock()
} }
При реализации таких защитных мер к функции funcA () в любой момент времени может получить доступ только один поток. Но проблемы на этом не исчерпываются. Если обе функции funcA() и funcB() небезопасны для выполнения потоками, они могут обе модифицировать глобальные или статические переменные. И хотя потоки thread1 и thread2 используют мьютексы для функции funcA (), поток thread3 может выполнять функцию funcB одновременно с любым из остальных потоков. В такой ситуации вполне вероятно возникновение условий «гонок», поскольку функции funcA () и funcB () могут модифицировать одну и ту же глобальную или статическую переменную.
Проиллюстрируем еще один тип условий «гонок», возникающих при использовании библиотеки
Если неизвестно, какие функции из библиотеки являются безопасными, а какие -нет, программист может воспользоваться одним из следующих вариантов действий.
• Ограничить использование всех опасных функций одним потоком.
• Не использовать безопасные функции вообще.
• Собрать все потенциально опасные функции в один набор механизмов синхронизации.
Еще один вариант — создать интерфейсные классы для всех опасных функций, которые должны использоваться в многопоточном приложении, т.е. опасные функции инкапсулируются в одном интерфейсном классе. Такой интерфейсный класс может быть скомбинирован с соответствующими объектами синхронизации с помощью наследования или композиции и использован специализированным классом. Такой подход устраняет возможность возникновения условий «гонок».
Разбиение программы на несколько потоков
Выше в этой главе мы рассматривали делегирование работы в соответствии с конкретной стратегией или потоковой моделью. Итак, используются следующие распространенные модели:
• делегирование («управляющий-рабочий»');
• сеть с равноправными узлами;
• конвейер;
• «изготовитель-потребитель».
Каждая модель характеризуется собственной
Использование модели делегирования
Мы рассмотрели два подхода к реализации модели делегирования при разделении мы на потоки. Вспомним: в
// Листинг 4.5. Подход 1: скелет программы реализации
//...
pthread_mutex_t Mutex = PTHREAD_MUTEX_INITIALIZER
int AvailableThreads
pthread_t Thread[Max_Threads]
void decrementThreadAvailability(void)
void incrementThreadAvailability(void)
int threadAvailability(void);
// boss thread
{
//...
if(sysconf(_SC_THREAD_THREADS_MAX) > 0){
AvailableThreads = sysconf(_SC_THREAD_THREADS_MAX)
}
else{
AvailableThreads = Default
}