Я читал файлы заголовков библиотеки pthreads и нашел это конкретное определение мьютекса (и других типов) в битах /pthreadtypes.h:
Это не совсем так, но я упростил это для ясности. Создание структуры с двумя разными определениями в заголовке и в файле реализации, являющейся реализацией определения реальной структуры и заголовка только символьного буфера размера реальной структуры, используется в качестве метода для скрытия реализации (непрозрачный тип), но по-прежнему выделяет правильный объем памяти при вызове malloc или распределении объекта в стеке.
Эта конкретная реализация использует объединение и все еще подвергает как определение структуры, так и символьного буфера, но, похоже, не дает никаких преимуществ с точки зрения скрытия типа, поскольку структура все еще отображается, а двоичная совместимость все еще зависящая от неизменности структур.
Ответ 2
Комитеты по стандартам, такие как IEEE и POSIX, разрабатывают и развивают стандарты с итерациями, которые обеспечивают большую функциональность или устраняют проблемы с предыдущими версиями стандартов. Этот процесс обусловлен потребностями людей, у которых есть проблемы с программным обеспечением, а также с поставщиками программных продуктов, поддерживающих этих людей. Как правило, внедрение стандарта в какой-то степени зависит от поставщика. Как и любое другое программное обеспечение, разные люди обеспечивают различия в реализации в зависимости от целевой среды, а также от своих собственных навыков и знаний. Однако по мере того, как стандарт созревает, существует своего рода дарвиновский отбор, в котором существует соглашение о лучших практиках, и различные реализации начинают сходиться.
Первые версии POSIX-библиотеки pthreads были в 1990-х годах, ориентированные на операционные системы в стиле UNIX, например, см. POSIX. 4: Программирование для реального мира и см. Также PThreads Primer: руководство по многопоточному программированию. Идеи и концепции для библиотеки возникли из работы, выполненной ранее, в попытке обеспечить совместную подпрограмму или тип потока типов, которые работали на более тонком уровне, чем уровень процесса операционной системы, чтобы уменьшить накладные расходы, которые создают, управляют и разрушают процессы участвует. Существовали два основных подхода к потоковому использованию, уровень пользователей с небольшой поддержкой ядра и уровнем ядра в зависимости от операционной системы для обеспечения управления потоком, с несколькими различными возможностями, такими как прерывание или прерывание потоковой передачи.
Кроме того, также требовались разработчики инструментов, такие как отладчики, чтобы обеспечить поддержку для работы в многопоточной среде и возможность видеть состояние потока и определять конкретные потоки.
Существует несколько причин использования непрозрачного типа в API для библиотеки. Основная причина заключается в том, чтобы позволить разработчикам библиотеки гибко изменять тип, не вызывая проблем для пользователей библиотеки. Существует несколько способов создания непрозрачных типов в C.
Один из способов - потребовать от пользователей API использовать указатель на некоторую область памяти, управляемую библиотекой API. Вы можете увидеть примеры этого подхода в стандартной библиотеке C с функциями доступа к файлу, например fopen()
, который возвращает указатель на тип FILE
.
В то время как это создает цель создания непрозрачного типа, для управления распределением памяти требуется библиотека API. Так как это указатели, вы можете столкнуться с проблемами выделения и никогда не выпускать память или пытаться использовать указатель, чья память уже выпущена. Это также означает, что специализированные приложения на специализированном оборудовании могут с трудом переносить функциональность, например, на специализированный датчик с поддержкой голых костей, который не включает в себя распределитель памяти. Подобные скрытые накладные расходы могут также влиять на специализированные приложения с ограниченными ресурсами и быть в состоянии прогнозировать или моделировать ресурсы, используемые приложением.
Второй способ - предоставить пользователям API структуру данных, которая имеет тот же размер, что и фактическая структура данных, используемая API, но которая использует буфер char для распределения памяти. Этот подход скрывает детали макета памяти, поскольку все пользователи API видят один буфер или массив char, но он также выделяет правильный объем памяти, который используется API. Затем API имеет свою собственную структуру, которая описывает, как фактически используется память, и API выполняет внутреннее преобразование указателя, чтобы изменить структуру, используемую для доступа к памяти.
Этот второй подход обеспечивает пару приятных преимуществ. Прежде всего, память, используемая API, теперь управляется пользователем API, а не самой библиотекой. Пользователь API может решить, хотят ли они использовать распределение стека или глобальное статическое распределение или какое-то другое распределение памяти, например malloc()
. Пользователь API может решить, хотите ли они обернуть выделение памяти в некотором виде отслеживания ресурсов, например, подсчет ссылок или какое-либо другое управление, которое пользователь хочет делать на своей стороне (хотя это также можно сделать с помощью указателей непрозрачных типов также). Этот подход также позволяет пользователю API лучше понимать потребление памяти и моделировать потребление памяти для специализированных приложений на специализированном оборудовании.
Дизайнер API также может предоставить некоторые типы данных пользователю API, который может быть удобен, например, информация о состоянии. Цель этой информации о статусе заключается в том, чтобы позволить пользователю API запрашивать то, что равнозначно только чтению только членов структуры, а не накладные расходы какой-либо вспомогательной функции в интересах эффективности. Хотя члены не указаны как const
(чтобы побудить компилятор C ссылаться на фактический элемент, а не кэшировать значение в какой-то момент времени, в зависимости от того, что он не изменится), API может обновлять поля во время операций, чтобы предоставить информацию пользователю API, не зависящему от значений этих полей для собственного использования.
Однако любые такие поля данных подвержены риску возникновения проблем с обратной совместимостью, а также изменений, связанных с проблемами памяти. Компилятор AC может вводить прокладку между членами структуры, чтобы обеспечить эффективные машинные инструкции при загрузке и хранении данных в этих элементах или из-за архитектуры ЦП, требующей какой-то границы адреса начальной памяти для некоторых видов инструкций.
В частности, для библиотеки pthreads мы имеем влияние программирования стиля UIX в стиле 1980-х и 1990-х годов, которое, как правило, имеет открытые и видимые структуры данных и заголовочные файлы, позволяющие программистам читать определения структур и определенные константы с комментариями, поскольку большая часть доступной документацией был источник.
Краткий пример непрозрачной структуры будет следующим. Существует файл include, thing.h, который содержит непрозрачный тип и который включен любым, кто использует API. Затем есть библиотека, исходный файл, thing.c, содержит фактическую используемую структуру.
вещь .h может выглядеть как
#define MY_THING_SIZE 256
typedef struct {
char array[MY_THING_SIZE];
} MyThing;
int DoMyThing (MyThing *pMyThing, int stuff);
Тогда в файле реализации, thing.c, у вас может быть источник, похожий на следующий
typedef struct {
int thingyone;
int thingytwo;
char aszName[32];
} RealMyThing;
int DoMyThing (MyThing *pMyThing, int stuff)
{
RealMyThing *pReal = (RealMyThing *)pMyThing;
// do stuff with the real memory layout of MyThing
return 0;
}
Относительно "до основных" потоков
Когда приложение, использующее время запуска C, запускается, загрузчик использует точку входа для времени выполнения C в качестве стартового места приложения. Время выполнения C затем выполняет инициализацию и настройку среды, которые необходимо выполнить, а затем вызывает назначенную точку входа для фактического приложения. Исторически эта назначенная точка входа является функцией main()
, однако то, что использует время выполнения C, может варьироваться между операционными системами и средами разработки. Например, для приложения Windows GUI назначенная точка входа WinMain()
(см. точка входа WinMain), а не main()
.
Для определения условий, при которых вызывается назначенная точка входа для приложения, зависит время выполнения C. Будут ли выполняться "pre-main" потоки будут зависеть от времени выполнения C и целевой среды.
В приложении Windows, использующем элементы управления Active-X со своим собственным насосом сообщений, вполне могут быть "пре-основные" потоки. Я работаю с большим Windows-приложением, которое использует несколько элементов управления, обеспечивающих различные типы интерфейсов устройств, и когда я смотрю в отладчике, я вижу несколько потоков, которые источник моего приложения не создает с определенным вызовом создания потока. Эти потоки запускаются по времени запуска, когда используемые элементы управления Active-X загружаются и запускаются.
Ответ 3
Да, обычно реализация скрывала бы большинство деталей такой структуры, как это (где предположительно __SIZEOF_PTHREAD_MUTEX_T
определяется в некотором ранее включенном системном заголовочном файле):
typedef union
{
char __size[__SIZEOF_PTHREAD_MUTEX_T];
long int __align;
} pthread_mutex_t;
Или вот так:
typedef union
{
#if __COMPILE_FOR_SYSTEM
struct __pthread_mutex_s
{
...internal struct member declarations...
} __data;
#endif
char __size[__SIZEOF_PTHREAD_MUTEX_T];
long int __align;
} pthread_mutex_t;
Первая форма полностью изолирует внутренности объявления структуры от кода клиента. Таким образом, получение доступа к фактическим внутренним элементам структуры потребует включения файла заголовка системного ядра с полным объявлением структуры, к которому обычно не будет иметь обычный клиентский код. Поскольку клиентский код должен иметь дело только с указателями на этот тип struct/union, фактические члены могут оставаться скрытыми от всего кода клиента.
Вторая форма предоставляет внутренности структуры программисту, но не компилятору (предположительно, __COMPILE_FOR_SYSTEM
определяется в каком-то другом системном заголовочном файле, который будет использоваться только при компиляции кода ядра).
Вопрос остается, почему разработчики этой библиотеки решили оставить внутренние детали видимыми компилятору? В конце концов, похоже, что второе решение будет очень легко обеспечить.
Я предполагаю, что либо разработчики просто забыли об этом в этом конкретном случае. Или, возможно, их исходный код и код файла заголовка расположены несовершенно, так что им нужно, чтобы члены были открыты, чтобы их компиляторы работали (но это довольно сомнительно).
Извините, что это не отвечает на ваш вопрос.