Здесь много чего происходит: различия между gcc и clang, различия между gnu ld и gold, флаг компоновщика --as-needed
, два разных режима сбоя и, возможно, даже некоторые проблемы с синхронизацией.
Давайте начнем скак связать программу, используя потоки POSIX.
Флаг -pthread
компилятора - все, что вам нужно.Это флаг компилятора, поэтому вы должны использовать его как при компиляции кода, использующего потоки, так и при компоновке окончательного исполняемого файла.Когда вы используете -pthread
на этапе компоновки, компилятор автоматически предоставит флаг -lpthread
в нужном месте в строке компоновки.
Как правило, вы будете использовать его только при компоновке конечного исполняемого файла., а не при связывании общей библиотеки.Если вы просто хотите сделать поток вашей библиотеки безопасным, но не хотите принудительно заставлять каждую программу, которая использует вашу библиотеку, связываться с pthreads, вам нужно использовать проверку во время выполнения, чтобы проверить, загружена ли библиотека pthreads, и вызватьPthread API, только если это так.В Linux это обычно делается путем проверки «канарейки» - например, сделать слабую ссылку на произвольный символ, такой как __pthread_key_create
, который будет определен только при загрузке библиотеки и будет иметь значение 0, еслиПрограмма была связана без нее.
В вашем случае, однако, ваша библиотека libodr.so
в значительной степени зависит от потоков, поэтому разумно связать ее с флагом -pthread
.
Это приноситПерейдем к первому режиму сбоя: если вы используете g ++ и gold для обоих шагов ссылки, программа выдает std::system_error
и говорит, что вам нужно включить многопоточность.Это связано с флагом --as-needed
.GCC передает компоновщику --as-needed
по умолчанию, а clang (по-видимому) - нет.При --as-needed
компоновщик будет записывать только зависимости библиотеки, которые разрешают строгую ссылку.Поскольку все ссылки на API-интерфейсы pthread являются слабыми, ни одной из них недостаточно, чтобы сообщить компоновщику, что libpthread.so следует добавить в список зависимостей (через запись DT_NEEDED
в динамической таблице).Изменение clang или добавление флага -Wl,--no-as-needed
решает эту проблему, и программа загрузит библиотеку pthread.
Но, подождите, почему вам не нужно делать это при использовании компоновщика Gnu?Он использует то же правило: только сильная ссылка приводит к тому, что библиотека записывается как зависимость.Разница в том, что Gnu ld также рассматривает ссылки из других общих библиотек, в то время как gold рассматривает ссылки только из обычных объектных файлов.Оказывается, что библиотека pthread предоставляет переопределенные определения нескольких символов libc, и существуют строгие ссылки от libstdc++.so
на некоторые из этих символов (например, write
).Этих сильных ссылок достаточно, чтобы Gnu ld записал libpthread.so
в качестве зависимости.Это скорее случайность, чем дизайн;Я не думаю, что изменение золота для учета ссылок из других общих библиотек было бы надежным решением.Я думаю, что правильное решение для GCC - поставить --no-as-needed
перед флагом -lpthread
, когда вы используете -pthread
.
. Возникает вопрос, почему эта проблема не возникает постояннопри использовании потоков POSIX и золотого компоновщика.Но это небольшая тестовая программа;большая программа почти наверняка будет содержать сильные ссылки на некоторые из тех символов libc, которые переопределяются libpthread.so
.
Теперь давайте рассмотрим второй режим сбоя, когда и Notify()
, и Get()
блокируются на неопределенный срок, если выссылка libodr.so
с g ++, gold и -lpthread
.
В Notify()
вы удерживаете блокировку до конца функции, пока вызываете cv.notify_one()
.Вам действительно нужно только удерживать блокировку, чтобы установить флаг готовности;если мы изменим его так, чтобы мы сняли блокировку до этого, то поток, вызывающий Get()
, истечет через 300 мс и не будет блокироваться.Так что это действительно вызов notify_one()
, который блокирует, и программа взаимоблокируется, потому что Get()
ожидает этой же блокировки.
Так почему же он блокируется, только если __pthread_key_create
равно FUNC
вместо NOTYPE
?Я думаю, что тип символа - красная сельдь, и что настоящая проблема вызвана тем фактом, что золото не записывает версии символов для ссылок, разрешенных библиотекой, которая не добавлена в качестве необходимой библиотеки.Реализация wait_for
вызывает pthread_cond_timedwait
, которая имеет две версии: libpthread
и libc
.Возможно, загрузчик привязывает ссылку к неверной версии, что приводит к тупику из-за невозможности разблокировать мьютекс.Я сделал временный патч к золоту для записи этих версий, и это заставило программу работать.К сожалению, это не решение, так как этот патч может привести к сбою ld.so при других обстоятельствах.
Я попытался изменить cv.wait_for(...)
на cv.wait(lock, []{ return ready; })
, и программа отлично работает во всех сценариях, что также предполагаетпроблема в pthread_cond_timedwait
.
Суть в том, что добавление флага --no-as-needed
решит проблему в этом очень маленьком тестовом примере.Все, что больше, вероятно, будет работать без дополнительного флага, так как вы увеличите шансы сделать сильную ссылку на символ в libpthread
.(Например, добавление вызова к std::this_thread::sleep_for
в любом месте в odr.cpp
добавляет сильную ссылку на nanosleep
, что ставит libpthread
в нужный список.)
Обновление: Я убедился, что сбойная программа ссылается на неправильную версию pthread_cond_timedwait
.Для glibc 2.3.2 тип pthread_cond_t
был изменен, а старые версии API, использующие этот тип, были изменены для динамического выделения новой (большей) структуры и сохранения указателя на нее в исходном типе.Так что теперь, если поток потребления достигает cv.wait_for
до того, как поток производства достигает cv.notify_one
, реализация cv.wait_for
вызывает старую версию pthread_cond_timedwait
, которая инициализирует то, что она считает старой pthread_cond_t
в cv
с указателем на новый pthread_cond_t
.После этого, когда другой поток достигает cv.notify_one
, его реализация предполагает, что cv
содержит pthread_cond_t
нового стиля, а не указатель на один, поэтому он вызывает pthread_mutex_lock
с указателем на новый pthread_cond_t
вместоуказателя на мьютекс.Он блокирует этот потенциальный мьютекс, но он никогда не разблокируется, потому что другой поток разблокирует настоящий мьютекс.