Pthreads и непрозрачные типы

Я читал файлы заголовков библиотеки pthreads и нашел это конкретное определение мьютекса (и других типов) в битах /pthreadtypes.h:

typedef union
{
  struct __pthread_mutex_s
  {
    int __lock;
    unsigned int __count;
    int __owner;
    /* KIND must stay at this position in the structure to maintain
       binary compatibility.  */
    int __kind;
    unsigned int __nusers;
    __extension__ union
    {
      int __spins;
      __pthread_slist_t __list;
    };
  } __data;
  char __size[__SIZEOF_PTHREAD_MUTEX_T];
  long int __align;
} pthread_mutex_t;

Это не совсем так, но я упростил это для ясности. Создание структуры с двумя разными определениями в заголовке и в файле реализации, являющейся реализацией определения реальной структуры и заголовка только символьного буфера размера реальной структуры, используется в качестве метода для скрытия реализации (непрозрачный тип), но по-прежнему выделяет правильный объем памяти при вызове malloc или распределении объекта в стеке.

Эта конкретная реализация использует объединение и все еще подвергает как определение структуры, так и символьного буфера, но, похоже, не дает никаких преимуществ с точки зрения скрытия типа, поскольку структура все еще отображается, а двоичная совместимость все еще зависящая от неизменности структур.

  • Почему типы, определенные в pthreads, следуют этому шаблону?
  • Каковы преимущества наличия непрозрачных типов, если вы не предоставляете двоичную совместимость (как в непрозрачном шаблоне указателя)? Я понимаю, что безопасность - это один из них, поскольку вы не позволяете пользователю вмешиваться в поля структуры, но есть ли что-нибудь еще?
  • Существуют ли типы pthread, предназначенные, в основном, для статических инициализаций или существует ли какая-либо другая конкретная причина для этого?
  • Возможно ли реализовать реализацию pthreads после непрозрачного шаблона указателя (т.е. не подвергать любые типы вообще и не допускать статических инициализаций)? или, более конкретно, есть ли ситуация, когда проблема может быть решена только с помощью статических инициализаций?
  • И полностью не связаны между собой, есть ли "до основных" потоков в C?

Ответ 1

Я считаю, что поля __size и __align определяют (угадывают, что:-)) размер и выравнивание структуры независимо от структуры __data. Таким образом, данные могут иметь меньший размер и иметь меньшие требования к выравниванию, его можно свободно модифицировать, не нарушая эти основные предположения об этом. И наоборот, эти основные характеристики могут быть изменены без изменения структуры данных, например здесь.

Важно отметить, что если размер __data становится больше, чем указано __SIZEOF_PTHREAD_MUTEX_T, утверждение25 > не выполняется:

assert (sizeof (pthread_mutex_t) <= __SIZEOF_PTHREAD_MUTEX_T);

Рассмотрим это утверждение как существенную часть этого подхода.

Итак, вывод заключается в том, что это было сделано, чтобы не скрывать детали реализации, а чтобы сделать структуру данных более предсказуемой и управляемой. Это очень важно для широко используемой библиотеки, которая должна много заботиться о обратной совместимости и влиянии производительности на другие коды из изменений, которые могут быть внесены в эту структуру.

Ответ 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 определяется в каком-то другом системном заголовочном файле, который будет использоваться только при компиляции кода ядра).

Вопрос остается, почему разработчики этой библиотеки решили оставить внутренние детали видимыми компилятору? В конце концов, похоже, что второе решение будет очень легко обеспечить.

Я предполагаю, что либо разработчики просто забыли об этом в этом конкретном случае. Или, возможно, их исходный код и код файла заголовка расположены несовершенно, так что им нужно, чтобы члены были открыты, чтобы их компиляторы работали (но это довольно сомнительно).

Извините, что это не отвечает на ваш вопрос.

Ответ 4

  • Я видел объединение структуры и буфера данных раньше, чтобы поддерживать команды сравнения слов и свопа с двойным словом (которые имеют конкретные требования к выравниванию); эта инструкция может быть тем, что они используют для реализации функций мьютекса.

  • Чтобы позволить разработчикам больше свободы реализовать свое видение быстрой и эффективной библиотеки pthreads, в то же время предоставляя конечному пользователю единый вывод.

  • Main - это внутренняя концепция, обычно перед вызовом функции обычно вызывается стандартная дескриптор файла. В GCC вы можете добавить атрибут '__attribute__ ((constructor))' к функции, и он будет вызываться до main (затем он может запустить кучу потоков, а затем выйти). Однако корневой процесс/поток, который порождает другие процессы или потоки, всегда должен быть первым (в случае, если это был ваш вопрос).