Лекция Синхронизация (продолжение)




Скачать 174.96 Kb.
Дата 05.10.2016
Размер 174.96 Kb.

Лекция 3. Синхронизация (продолжение).


Mutex и conditional variable представляют собой два базовых механизма на основе которых можно решить практически любую задачу связанную с синхронизацией. В частности на основе этих двух механизмов можно построить все механизмы синхронизации которые мы рассмотрим ниже. Эти механизмы отсутствуют в рассматриваемом нами в качестве базового стандарте POSIX 1003.1c - 1995, но есть в готовящемся к выходу стандарте POSIX 1003 – 2001, и в ряде других уже принятых стандартов, и что самое главное они присутствуют в ряде реализаций.
Первый механизм, который мы рассмотрим это read/write lock (блокировка чтения/записи). Фактически он, как и mutex позволяет организовывать взаимоисключение при доступе к общим данным, однако он в отличие от mutex’а позволяет учитывать то, модифицируем ли мы данные или же только читаем их. Действительно, одновременный доступ по чтению к общим данным из разных потоков выполнения не вызывает никаких проблем, и следовательно вполне логично было бы допускать до одновременной работы с общими данными несколько нитей, которые только читают, или же одну нить, которая модифицирует общие данные. Именно такую логику и реализует read/write lock, он позволяет нескольким нитям одновременно читать общие данные, при условии, что нет нити, которая модифицирует их, и допускает только одну нить до модификации данных.

Как определяется, что данная нить будет только читать данные? Просто при захвате read/write lock’а нить обязана указать, что она будет только читать данные или желает модифицировать их. То есть здесь в отличие от mutex’а появляются два типа захватов – захват на чтение и захват на запись. Часто соответствующие два типа доступа называют также разделяемый доступ (по чтению) и эксклюзивный (исключительный) доступ (по записи). При этом если нить нарушит свои обещания, то общие данные могут испортиться, например, может нарушиться какой либо из инвариантов, что естественно может привести к ошибке.

Read/write lock’и целесообразно использовать в тех ситуациях, когда мы часто читаем общие данные и сравнительно редко модифицируем их. Типичным примером такого рода данных может служить кэш, из которого в основном читают данные, а обновляют его только если какие-то данные отсутствуют в нем. В случае если чтение и модификация данных происходит с одинаковой частотой то мы практически приходим к ситуации чистого взаимоисключения и, следовательно, выгоднее использовать более быстрый и дешевый чем read/write lock mutex.

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






T1

T2

T3

T1 получ. блок.

чтения


T2 получ. блок.

чтения


T3 ожидает блок.

записи


T1 освобожд. блокировку

T1 ожидает блок.

чтения


T2 освобожд. блокировку

T3 освобожд. блокировку

T3 получ. блок.

записи


T1 получ. блок.

чтения


Read/write lock



Рис. 1. Временная диаграмма взаимодействия 3 нитей и rwlock’а.
Готовящийся к принятию стандарт POSIX.1003-2001 включает поддержку блокировок чтения/записи. Согласно этому стандарту такие блокировки будут представляться в виде объектов типа pthread_rwlock_t. Как и в случае mutex’ов так и в случае condition variables этот тип непрозрачен, то есть объекты этого типа не подлежат копированию и нельзя сравнивать. Как и в предыдущих случаях перед использованием по назначению объект этого типа должен быть проинициализирован при помощи функции:
int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr);
При этом после инициализации блокировка оказывается в свободном состоянии.

Установить разделяемую блокировку чтения можно при помощи функции:


int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
При этом согласно стандарту блокировка будет установлена, если нет активной блокировки по записи и нет нитей ожидающих возможности установки блокировки записи. В других случаях нить будет спать до появления возможности установить блокировку. Таким образом, блокировки чтения/записи предлагаемые стандартом это блокировки с предпочтением писателей, так как читатели всегда будут стараться уступать им дорогу. Установить блокировку записи можно при помощи функции:
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
Эта функция как и следует ожидать устанавливает блокировку записи в случае если для данного read/write lock’а не установлено никаких других блокировок, в остальных случаях вызвавшая ее нить будет ожидать освобождения lock’а.
Нить может снять любую установленную ею ранее блокировку при помощи функции:
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
При этом, поскольку в данный момент времени для данной блокировки чтения/записи данная нить может обладать либо только правом чтения, либо правом записи, то при снятии блокировки всегда известно, какого именно типа блокировку мы сейчас снимаем, и соответственно эта функция не нуждается в вариантах.

И, наконец, как и в случае mutex’а ресурсы занимаемые свободной блокировкой чтения/записи могут быть освобождены при помощи функции:


int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
Рассмотрим теперь, как можно было бы реализовать блокировки чтения записи при помощи mutex’а и переменных условий. Наша реализация будет упрощенной, так как не будет заниматься проверкой ошибок и учитывать возможность прерывания работы нити (cancellation). Объект, соответствующий блокировке чтения/записи, будет представляться типом rwl_t.
typedef struct rwl_tag

{

pthread_mutex_t lock;



pthread_cond_t readReady;

pthread_cond_t writeReady;

int readersActive;

int writerActive;

int readersWaiting;

int writersWaiting;

} rwl_t;
void rwl_init(rwl_t *rwl)

{

pthread_mutex_init(&(rwl->lock),NULL);



pthread_cond_init(&(rwl->readReady),NULL);

pthread_cond_init(&(rwl->writeReady),NULL);

rwl->readersActive=rwl->writerActive=0;

rwl->readersWaiting=rwl->writersWaiting=0;

}
void rwl_rdlock(rwl_t *rwl)

{

pthread_mutex_lock(&(rwl->lock));



if(rwl->writersWaiting > 0 || rwl->writerActive)

{

rwl->readersWaiting++;



while(rwl->writersWaiting > 0 || rwl->writerActive)

pthread_cond_wait(&(rwl->readReady),&(rwl->lock));

rwl->readersWaiting--;

}

rwl->readersActive++;



pthread_mutex_unlock(&(rwl->lock));

}
void rwl_wrlock(rwl_t *rwl)

{

pthread_mutex_lock(&(rwl->lock));



if(rwl->readersActive > 0 || rwl->writerActive)

{

rwl->writersWaiting++;



while(rwl->readersActive > 0 || rwl->writerActive)

pthread_cond_wait(&(rwl->writeReady),&(rwl->lock));

rwl->writersWaiting--;

}

rwl->writerActive = 1;



pthread_mutex_unlock(&(rwl->lock));

}
void rwl_unlock(rwl_t *rwl)

{

pthread_mutex_lock(&(rwl->lock));



if(rwl->readersActive > 0)

rwl->readersActive--;

else

rwl->writerActive=0;



if(rwl->writersWaiting > 0 && rwl->readersActive == 0)

pthread_cond_signal(&(rwl->writeReady));

if(rwl->writersWaiting == 0 && rwl->readersWaiting > 0)

pthread_cond_broadcast(&(rwl->readReady));

pthread_mutex_unlock(&(rwl->lock));

}
void rwl_destroy(rwl_t *rwl)

{

pthread_mutex_destroy(&(rwl->lock),NULL);



pthread_cond_destroy(&(rwl->readReady),NULL);

pthread_cond_destroy(&(rwl->writeReady),NULL);

}
Как и блокировки предусмотренные стандартом блокировки реализованные таким образом предпочитают ждущих писателей, читателям. Однако они могут быть легко превращены в блокировки с предпочтением читателей, для этого достаточно изменить предикаты которые используются для принятия решений в функциях rwl_rdlock, rwl_wrlock и rwl_unlock.
Второй примитив, про который хотелось бы поговорить, это так называемый барьер (barrier). Что он делает? – останавливает нити. Точнее говоря, при инициализации барьера задается какое количество нитей он должен остановить, соответственно, нити преодолевающие барьер (дошедшие до него) не могут продолжать свое выполнение пока на этот барьер не наткнется указанное при инициализации количество нитей.

Барьеры предназначены для того чтобы держать нити - члены определенной группы вместе. Наткнувшись на барьер, нить из группы не сможет продолжать выполнение, пока все члены группы не дойдут до барьера. Обычно барьеры используют для того, чтобы все нити выполняющие какой-то параллельный алгоритм достигли определенной его точки прежде чем какая-либо из них двинулась дальше.

Особо часто барьеры используются при автоматическом распараллеливании. Действительно рассмотрим цикл:

for(int i =0; i

sum+=A[i];

Ясно, что при достаточно большом N и достаточном числе процессоров имеет смысл разбить этот цикл на несколько кусков выполняющихся в разных нитях, потом просуммировать результаты их работы. Таким образом, получаем несколько нитей, каждая из которых выполняет один и тот же код, но работает со своими данными. В конце цикла мы имеем точку где мы должны дождаться когда все нити завершат цикл обрабатывающий данные, а потом в одной из них просуммировать результат работы каждой из нитей. То есть, получаем область, где какая-то одна нить должна выполнять свой специфический код.

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


Барьеры






Параллельные секции

Последовательные секции, подготовка к новой параллельной секции



Рис 2. Вычисление как последовательность параллельных и последовательных секций.
Работу барьера можно изобразить с помощью следующей временной диаграммы:


T1

T2

T3

T1 достигает барьера, ожидание

T2 достигает барьера, ожидание

T3 достигает барьера, и все нити продолжают работу

Barrier (3)


Рис 3. Временная диаграмма прохождения барьера тремя нитями.
POSIX.1003-2001 поддерживает барьеры в качестве опции. В программе они представляются при помощи непрозрачного типа pthread_barrier_t, объекты которого можно инициализировать при помощи вызова функции:
int pthread_barrier_init(pthread_barrier_t *barrier, const pthread_barrierattr_t * attr, unsigned count);
Где последний параметр count задает число нитей, которые должны дойти до барьера (вызвать функцию pthread_barrier_wait) прежде чем они смогут продолжить выполнение. Раз существует функция инициализирующая барьер, то существует и комплиментарная функция которая уничтожает его:
int pthread_barrier_destroy(pthread_barrier_t *barrier);
Как и в случае остальных примитивов синхронизации данная функция должна вызываться только в случае если на уничтожаемом барьере не ожидает ни одной нити. Ну и наконец существует функция вызов которой собственно и соответствует преодолению барьера:
int pthread_barrier_wait(pthread_barrier_t *barrier);
Как сказано в стандарте она синхронизирует выполнение нитей участников барьера. Нить вызвавшая ее блокируется до тех пор пока count нитей не вызовут ее для данного барьера. При возврате из pthread_barrier_wait в одной из нитей преодолевших барьер будет возвращено отличное от нуля значение PTHREAD_BARRIER_SERIAL, что позволяет выделить единственную нить в которой выполнять последовательный код (смотри наш пример с суммированием). После возврата всех нитей из pthread_barrier_wait барьер приходит в исходное состояние, то есть оказывается снова готов к использованию.

Как и блокировки чтения записи барьеры легко реализуются на основе mutex’а, condition variable и набора целых счетчиков.


Третий примитив синхронизации, про который хотелось бы поговорить это spin lock. С точки зрения функциональности этот примитив ничем не отличается от mutex’а. Основная стоящая за ними идея состоит в том, чтобы вместо того чтобы заблокировать нить в случае если блокировка уже захвачена, крутиться, проверяя не освободилась ли блокировка (отсюда и слово spin в названии). Фактически spin lock это mutex основанный на использовании активного ожидания. Какой тогда смысл в таком примитиве? В случае если мы имеем мультипроцессор (так как на однопроцессорной машине spin lock будет бесполезно съедать циклы процессора до тех пор, пока не произойдет переключение на другую владеющую блокировкой нить) и крайне малый размер критической секции защищаемой этим примитивом (чтобы опять же не ждать долго и снизить опасность переключения на другую нить) spin lock может быть использован эффективнее чем mutex. Это обусловлено тем что он не вызывает долгой операции перевода нити в сон, вообще считается что spin lock реализуется на основе самых быстрых механизмов синхронизации доступных в системе. В случае если какое-либо из двух вышеприведенных условий нарушается, spin lock не даст выигрыша в производительности и выгоднее воспользоваться mutex’ом.

Spin lock’и также как и барьеры будут поддерживаться стандартом POSIX.1003-2001 в виде опции. Согласно стандарту данный примитив представляется при помощи типа pthread_spinlock_t и набора операций над ним, которые практически совпадают с соответствующими операциями для mutex’а:


int pthread_spin_init(pthread_spinlock_t *lock, int pshared);

int pthread_spin_destroy(pthread_spinlock_t *lock);

int pthread_spin_lock(pthread_spinlock_t *lock);

int pthread_spin_trylock(pthread_spinlock_t *lock);

int pthread_spin_unlock(pthread_spinlock_t *lock);
Если spin lock не поддерживается данной реализацией библиотеки поддержки нитей, то его легко реализовать, используя mutex и pthread_mutex_trylock для организации активного ожидания. Даже такая реализация останется при приведенных выше условиях эффективнее mutex’а, поскольку вызов pthread_mutex_trylock для захваченного mutex’а как правило сильно дешевле вызова pthread_mutex_lock.
Если мы вспомним приведенное выше описание mutex’а, то увидим что поведение функций pthread_mutex_lock и pthread_mutex_unlock недетерминировано, в случае если их вызывают ошибочно (например, если нить пытается освободить mutex захваченный другой нитью). Иногда такое поведение неудобно. В качестве решения этой проблемы было предложено ввести несколько типов mutex’ов каждый из которых обладает определенным поведением в случае ошибочных ситуаций:


  • PTHREAD_MUTEX_NORMAL – обычный mutex, который не контролирует ошибки. Для него попытка повторного захвата уже захваченного данной нитью mutex’а ведет к тупику (deadlock). Поведение в случае остальных ошибок таких как освобождение чужого или не захваченного mutex’а не определено.

  • PTHREAD_MUTEX_ERRORCHECK – mutex с проверкой ошибок. В случае любой ошибки (повторный захват, освобождение свободного или чужого) операция, которая ведет к ней возвращает не 0 а код ошибки.

  • PTHREAD_MUTEX_RECURSIVE – так называемый рекурсивный mutex. Попытка повторного захвата mutex’а нитью которая уже владеет им не считается ошибкой и всегда выполняется успешно. При этом подразумевается что для освобождения блокировки необходимо столько же раз вызвать операцию unlock, сколько раз для нее была вызвана операция lock. В случае же освобождения свободного или чужого mutex’а операция pthread_mutex_unlock для данного типа возвращает ошибку. Этот тип mutex’а крайне удобно использовать, в случае если мы работаем с некоторым набором объектов каждый из которых защищен своей блокировкой, и мы не хотим отслеживать какие из необходимых нам для работы объектов мы уже захватили, а какие нет.

  • PTHREAD_MUTEX_DEFAULT – тип mutex’а используемый по умолчанию, реально вместо него библиотека поддержки нитей будет использовать один из вышеприведенных типов, а более конкретно тот который она считает наиболее подходящим с точки зрения нужд пользователя и стоимости реализации.

Тип mutex’а является одним из его атрибутов, среди тех что можно задать при его инициализации. Сделать это можно воспользовавшись двумя функциями:
int pthread_mutexattr_gettype(const pthread_mutexattr_t * attr, int *type);

int pthread_mutexattr_settype(pthread_mutexattr_t *attr, int type);


Которые позволяют узнать тип mutex’а, который задан в данном объекте атрибутов, и установить этот тип соответственно. Естественно любой из этих типов mutex’а может быть реализован на основе стандартного, с неопределенным поведением.
Правила видимости памяти.
На самом деле примитивы синхронизации обеспечивают не только взаимоисключение при доступе к общим данным, но и задают правила видимости памяти между нитями. В чем здесь проблема? В любой современной архитектуре ЭВМ присутствует иерархия памяти.



Процессор

Регистры

Кэш

Оперативная память



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

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

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



CPU1

CPU2

Внешний


кэш 1

Внешний

кэш 2


ОП

Буфера записи

Bus snooper

Шина памяти



Рис 5. Архитектура многопроцессорной системы на основе Sparc V8.
У каждого процессора в многопроцессорной системе есть буфер записи (store buffer) который представляет собой маленький кэш для записи. Содержимое этого буфера не переписывается в кэш с какой-либо определенной частотой и соответственно информация об изменении данных может быть долго не видна другому процессору. При помощи команды ldstub (кстати говоря атомарной) содержимое этого буфера выталкивается во внешний кэш. Синхронизация внешних кэшей достигается за счет того что в этих машинах для каждого процессора есть специальное устройство bus snooper которое фиксирует какие данные движутся от другого процессора к его кэшу, и приводит в соответствие данные в своем кэше. То есть второй процессор видит все что пишет в свой кэш первый процессор, и приводит свой кэш в согласованное состояние и обратно.

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


Можно говорить и о другом взгляде на решение этой проблемы. Пусть доступы к памяти (чтения или записи) ставятся в очередь на исполнение к контроллеру памяти и могут быть выполнены в любом порядке какой контроллер сочтет наиболее удобным. Например, чтение по адресу которого нет в кэше может быть задержано до тех пор пока данные не окажутся в кэше, в то время как более поздние в очереди чтения успешно выполнятся. Аналогично, запись которая требует выталкивания строки кэша в память может быть отложена до лучших времен, в то время как более поздние операции будут выполняться. Вводится специальная инструкция барьер (memory barrier) MEMBAR, которую можно использовать в наших программах, для того чтобы гарантировать последовательность чтений-записей. MEMBAR требует чтобы все доступы к памяти которые затребовал процессор до нее были завершены прежде чем начнут выполняться доступа находящиеся за барьером.


Контроллер памяти



R

W

R

R

W

R



Рис 6. Контроллер памяти, его очередь и барьер
Если рассматривать ситуацию с точки зрения контроллера памяти то барьер это особый элемент в очереди через который ему нельзя переступать при выполнении операций, если еще остались незавершенные операции до барьера. Надо подчеркнуть то, что типичная ошибка ассоциировать барьер с cache flush, так как он не выталкивает данные из кэша в память, а скорее обеспечивает определенный порядок между множествами операций.

При использовании такой модели, захват mutex собственно начинается с захвата mutex’а, а заканчивается MEMBAR. Как результат ни одна операция, запрошенная после захвата, не сможет завершиться до тех пор, пока другие нити не увидят, что mutex захвачен. То есть доступ к общим данным начнется только тогда, когда он гарантированно будет единственным. То же и с освобождением mutex’а – он начинается с MEMBAR, давая тем самым завершиться всем операциям с общими данными, и кончается собственно освобождением mutex’а. То есть другие нити не смогут получить mutex пока не пройдут все операции с общими данными.


Именно эта модель барьеров и будет положена в основу описания правил видимости памяти в pthreads. У нас всегда будет некое исходное событие такое как освобождение mutex’а и целевое событие такое как его захват. За счет расстановки MEMBAR’ов обеспечивается передача вида памяти от первого события ко второму.

Каковы же правила видимости памяти согласно стандарту?



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

  2. Все что нить видит, когда освобождает mutex, будет видно и в нити, которая захватит этот mutex после того как она это сделает. При этом имеется в виду, как явное освобождение блокировки, так и неявное происходящее, например, в pthread_cond_wait. Опять же модификации данных произошедшие после освобождения mutex’а не всегда будут видны в нити захватившей mutex.

  3. Состояние памяти которое видит нить во время своего завершения, либо в следствие вызова pthread_exit, либо в следствие прерывания выполнения нити, будут видны и в нити которая присоединится к этой нити при помощи вызова pthread_join.

  4. Любые значения ячеек памяти которые нить видит когда посылает сигнал или широковещательный сигнал для conditional variable будет видеть нить которая будет пробуждена этим сигналом. Опять же данные записанные после pthread_cond_signal или pthread_cond_broadcast не обязательно будут видны в пробудившейся нити.

Подобные правила легко распространяются и на другие примитивы синхронизации типа барьеров и блокировок чтения/записи (особо интересующихся отсылаю к стандарту).

Кроме того, эти правила можно переформулировать следующим образом: Если мы будем организовывать взаимоисключение при доступе к общим данным, и читать или модифицировать эти данные, только захватывая mutex, то мы всегда будем видеть адекватную версию этих данных. Фактически о правилах видимости памяти вообще можно было бы не говорить, если бы не возникающий время от времени соблазн, записать без всякой синхронизации в одной нити некое значение во флаг, а в остальных нитях прочитать этот флаг. Вышеизложенное объясняет, почему это фокус не проходит.


Далее следует заметить, что данные могут находиться не только в памяти и в кэшах процессоров, но и в их регистрах, которые компилятор языка высокого уровня фактически использует как кэш. Соответственно когда мы модифицируем значение переменной, то мы модифицируем значение в регистре, и его на самом деле еще нужно вытолкнуть в память. К счастью это происходит согласно стандарту языка при вызове любой функции и в частности при вызове функций синхронизации. Таким образом автоматически обеспечивается выполнение этих правил. Кстати говоря эта проблема показывает что правила видимости памяти актуальны не только для многопроцессорных машин, но и для однопроцессорных машин.
Ко всему вышеизложенному хочется добавить, что проблемы с видимостью памяти специфичны не только для многонитевых приложений, а скорее проявляются в любом приложении, где есть общая память и много параллельно или псевдопараллельно выполняющихся сущностей. Более того, они скорее следствие асинхронности и точно также возникают при вызове обработчиков сигналов.
На протяжении данной лекции мы активно пользовались термином общие данные, однако ни разу четко не сформулировали что это такое. К сожалению точно такая же ситуация и со стандартом, то есть он не определят что есть общие данные. В частности неразрешенным остается вопрос о том, являются ли две переменные некого типа A, к каждой из которых осуществляется доступ из своей отдельной нити, общими данными для этих нитей. Возможно два варианта ответа: если машина позволяет обращаться к этим переменным независимо (модификация одной переменно не подразумевает чтения –записи данных в другой) то они будут независимыми, если же машина не позволяет осуществлять такой доступ, например если каждая из переменных занимает по байту и они находятся в соседних ячейках памяти, а машина не позволяет считывать из памяти байты, а только слова (скажем по 4 байта), то они являются общими данными, так как любая модификация одной из них может повлечь неявное чтение и запись другой.


База данных защищена авторским правом ©infoeto.ru 2022
обратиться к администрации
Как написать курсовую работу | Как написать хороший реферат
    Главная страница