C — преодоление использования атомарных операций

#c #atomic

#c #атомарный

Вопрос:

Мне было интересно. Если у меня есть переменная int, которую я хочу синхронизировать во всех моих потоках — разве я не смог бы зарезервировать один бит, чтобы узнать, обновляется значение или нет?
Чтобы избежать выполнения операции записи фрагментами, что означало бы, что потоки потенциально могут обращаться к среднему записанному значению, что неверно, или, что еще хуже, перезаписывать его, делая его полностью неправильным, я хочу, чтобы потоки сначала были проинформированы о том, что переменная записывается. Я мог бы просто использовать атомарную операцию для записи нового значения, чтобы другие потоки не вмешивались, но эта идея не кажется такой уж глупой, и я хотел бы сначала использовать базовые инструменты.
Что, если я просто выполню одну операцию, которая достаточно мала, чтобы сохранить ее в одном блоке, операцию, подобную изменению одного бита (что все равно приведет к изменению целого байта (ов), но это не изменение всего значения, верно), и пусть бит указывает, в переменную записывается или нет? Это вообще сработает, или в него будет записан весь int?
Я имею в виду, даже если бы весь int должен был измениться, у этого все равно был бы шанс сработать — если бы бит, указывающий, изменяется ли значение, был записан первым.

Есть мысли по этому поводу?

РЕДАКТИРОВАТЬ: я чувствую, что я не указал, что я на самом деле планирую делать, и почему я подумал об этом в первую очередь.
Я пытаюсь реализовать функцию тайм-аута, аналогично setTimeout в JavaScript. Это довольно просто для тайм-аута, который вы никогда не хотите отменять — вы создаете новый поток, переводите его в режим ожидания на определенное количество времени, затем даете ему функцию для выполнения, в конечном итоге с некоторыми данными. Проще простого. Закончил писать, возможно, за полчаса, будучи совершенно новичком в C.
Сложная часть возникает, когда вы хотите установить тайм-аут, который может быть отменен в будущем. Таким образом, вы делаете то же самое, что и тайм-аут, без отмены, но когда поток просыпается и планировщик процессора включает его, поток должен проверить, не указано ли значение в памяти, которое было присвоено при его запуске, «вы должны прекратить выполнение». Значение потенциально может быть изменено другим потоком, но это будет сделано только один раз, по крайней мере, в лучшем случае. Я буду беспокоиться о разных решениях, когда дело дойдет до попытки изменить значение из нескольких потоков одновременно. Базовое предположение прямо сейчас заключается в том, что только основной поток или один из других потоков может изменять значение, и это произойдет только один раз. Контролировать это, происходящее только один раз, можно, установив другую переменную, которая может изменяться несколько раз, но всегда на одно и то же значение (то есть начальное значение равно 0, и это означает, что оно еще не отменено, но затем, когда оно должно быть отменено, значение изменяется на 1, поэтому можно не беспокоиться о том, что значение фрагментируется на несколько операций записи и только его часть обновляется во время чтения другим потоком).
Учитывая это предположение, я думаю, что текст, который я изначально написал в начале этого поста, должен быть более понятным. В двух словах, не нужно беспокоиться о том, что значение записывается несколько раз, только один раз, но любым потоком, и значение должно быть доступно для чтения любым другим потоком, или должно быть указано, что оно не может быть прочитано.
Теперь, когда я думаю об этом, поскольку само значение всегда будет только 0 или 1, трюк с указанием того, когда оно уже было отменено, тоже должен сработать, не так ли? Поскольку 0 или 1 всегда будут в одной операции, поэтому нет необходимости беспокоиться о том, что они будут фрагментированы и прочитаны неправильно. Пожалуйста, поправьте меня, если я ошибаюсь.
С другой стороны, что, если значение записывается с конца, а не с начала? Если это невозможно, тогда не нужно беспокоиться, и сообщение будет разрешено, но я хотел бы знать о каждой опасности, которая может возникнуть при преодолении атомарных операций, подобных этой, в этом конкретном контексте. В случае, если это записывается с конца, и поток хочет получить доступ к переменной, чтобы узнать, следует ли ей продолжать выполнение, он заметит, что это действительно так, в то время как ожидаемым поведением было бы прекратить выполнение. Вероятность того, что это возможно, должна быть абсолютно минимальной, но все же это так, что означает, что это опасно, и я хочу, чтобы это было на 100% предсказуемо.

Another edit to explain what steps I imagine the program to make.
Main thread spawns a new thread, aka ‘cancelable timeout’. It passes a function to execute along with data, time to sleep, and memory address, pointing to a value. After the thread wakes up after given time, it must check the value to see if it should execute the function it has been given. 0 means it should continue, 1 means it should stop and exit. The value (thread’s ‘state’, canceled or not canceled) can be manipulated by either the main thread, or any other thread, ‘timeout’, which’s job is to cancel the first thread.
Sample code:

 struct Timeout {
  void (*function)(void* data);
  void* data;
  int milliseconds;
  int** base;
  int cancelID;
};
DWORD WINAPI CTimeout(const struct Timeout* data) {
  Sleep(data->milliseconds);
  if(*(*(data->base)   sizeof(int) * data->cancelID) == 0) {
    data->function(data->data);
  }
  free(data);
  return 0;
}
  

Где CTimeout — это функция, предоставляемая вновь созданному потоку. Пожалуйста, обратите внимание, что я написал часть этого кода на go и не тестировал его. Игнорируйте любые потенциальные ошибки.
Тайм-аут.base — это указатель на указатель на массив целых чисел, поскольку одновременно может существовать множество таймаутов. Timeout.cancelID — это идентификатор текущего потока в списке таймаутов. Тот же идентификатор имеет значение, если обрабатывается как индекс в базовом массиве. Если значение равно 0, поток должен выполнить свою функцию, в противном случае очистите данные, которые ему были предоставлены, и аккуратно верните. Причина, по которой база является указателем на указатель, заключается в том, что в любой момент массив состояний тайм-аутов может быть изменен. В случае изменения места массива нет возможности передать его начальное место. Потенциально это может привести к ошибке сегментации (если нет, поправьте меня, пожалуйста) для доступа к памяти, которая нам больше не принадлежит.
При необходимости к базе можно получить доступ из основного потока или других потоков, а состояние нашего потока можно изменить, чтобы отменить его выполнение.
Если какой-либо поток хочет изменить состояние (состояние как состояние тайм-аута, которое мы создали в начале и которое хотим отменить), он должен изменить значение в массиве ‘base’. Я думаю, что пока это довольно просто.
Была бы огромная проблема, если бы значения для продолжения и остановки были чем-то большим, чем просто 1 байт. Операция записи в память на самом деле может потребовать нескольких операций, и, таким образом, слишком ранний доступ к памяти приведет к неожиданным результатам, а это не то, что мне нравится. Хотя, как я уже упоминал ранее, что, если значение очень мало, 0 или 1? Разве это вообще не имело бы значения, в какое время осуществляется доступ к значению? Нас интересует только 1 байт, или даже 2 или 4 байта, или целое число, даже 8 байт в данном случае не имели бы никакого значения, не так ли? В конце концов, можно не беспокоиться о получении недопустимого значения, поскольку нас волнует не 32-битное значение, а всего 1 бит, независимо от того, сколько байт мы бы читали.
Возможно, это не совсем понятно, что я имею в виду. Операции записи / чтения состоят не из чтения отдельных битов, а из байта (ов). То есть, если наше значение не больше 255, или 65535, или 4 миллионов миллионов, независимо от количества байт, которые мы записываем / считываем, мы не должны беспокоиться о чтении его в середине записи. То, о чем мы заботимся, — это только один фрагмент того, что записывается, последний или первые байты. Остальное для нас совершенно бесполезно, поэтому не нужно беспокоиться о том, что все это синхронизируется во время доступа к значению. Настоящая проблема начинается, когда значение записывается в, но первый байт, в который записывается значение, находится в конце, что для нас бесполезно. Если мы прочитаем значение в этот момент, мы получим то, чего не должны — состояние «нет отмены» вместо «отменить». Если бы первый байт с заданным маленьким порядковым номером должен был быть прочитан первым, мы получили бы допустимое значение, даже если бы читали в середине записи.
Возможно, я все путаю и путаю. Я не профессионал, вы знаете. Возможно, я читал дрянные статьи, что угодно. Если я вообще в чем-то ошибаюсь, пожалуйста, поправьте меня.

Комментарии:

1. Не пытайтесь создавать свои собственные механизмы синхронизации. Используйте мьютекс.

2. вы задаете два разных вопроса. один из них касается атомарности. еще один вопрос касается потокобезопасности. @dbush дал тебе лучший совет.

3. Вы могли бы попробовать реализовать алгоритм bakery. Если вы любите приключения…

4. @wildplasser очень интересный алгоритм, спасибо, что предложили его! я рассмотрю возможность его реализации.

5. Клиенты должны быть пронумерованы (1..N) и должны знать свой собственный номер, IIRC.

Ответ №1:

За исключением некоторых специализированных встроенных сред с выделенным оборудованием, не существует такого понятия, как «одна операция, которая достаточно мала, чтобы сохранить ее в одном блоке, операция, подобная изменению одного бита». Вам нужно иметь в виду, что вы не хотите просто перезаписывать специальный бит на «1» (или «0»). Потому что даже если бы вы могли это сделать, это могло бы просто совпасть с каким-то другим потоком, делающим то же самое. Что вам на самом деле нужно сделать, так это проверить, является ли это уже 1, и ТОЛЬКО если это НЕ так, напишите 1 самостоятельно и ЗНАЙТЕ, что вы не перезаписывали существующий 1 (или что запись вашего 1 не удалась из-за того, что 1 уже был там).

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

Простого способа обойти это нет.

Комментарии:

1. Ваш комментарий очень хорош, но не учитывает того, что я на самом деле имел в виду. Я отредактировал сообщение из-за этого, чтобы немного прояснить контекст моего вопроса. Хотя спасибо за ответ!

2. Я думаю, что ваша правка ничего не меняет. Пока переменная потенциально записывается более чем одним потоком, даже если в общей сложности только одним потоком, каждый поток не знает, записывался ли другой ранее. Может быть, вы можете описать хронологическую последовательность того, что происходит. Обязательно укажите информацию о каждом потоке (например, «поток A хочет записать, но сначала нужно выяснить, записал ли уже поток B»). Я думаю, что если вы сделаете это чисто, вы увидите, что ваша идея не работает. Если вы можете сделать это так, чтобы это, казалось, работало, я сделаю все возможное, чтобы найти и выделить gap.

3. Основной поток решает установить отменяемый тайм-аут. Появляется новый поток, он переходит в спящий режим, скажем, на 1 секунду. Когда он просыпается, он должен проверить значение по указанному адресу памяти. 0 означает продолжение, 1 означает остановку выполнения. Основной поток и, возможно, другой поток, который был создан для отмены первого потока, могут манипулировать значением. Возможно, они могли бы изменить его на 1, и даже если бы это происходило несколько раз, при условии, что это происходит путем записи младшего значащего байта (ов) первым, а не последним (учитывая маленький порядковый номер), отменяемый тайм-аут должен знать об этом мгновенно. Верно?

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

5. Пожалуйста, используйте пространство и возможности форматирования, предлагаемые путем редактирования этого в вопросе. Я рекомендую пронумерованный список шагов. Так что ссылаться на проблемный шаг будет удобно. Попробуйте воспроизвести все возможные комбинации того, какой поток когда читает / записывает. По моему опыту, стоит считать «другой» поток неприятным подстерегателем, который намеренно выжидает наихудшего момента, чтобы выполнить свою часть работы, с идеальным временем в наносекунды, если это необходимо. Т.Е. попытайтесь опровергнуть вашу идею, а не пытаться описать, как это работает.