В Go, какие типы данных безопасны для чтения и записи после извлечения их из одновременно доступной структуры?

Вероятно, не ясно из самого вопроса, что я здесь, поэтому позвольте мне уточнить. В качестве упражнения по параллелизму я пытаюсь написать кеш, который должен быть доступен для нескольких одновременных запросов. Содержимое кэша имеет тип interface{}, поэтому оно может включать в себя все, что угодно, включая срезы, карты и структуры. Когда я получаю что-то с помощью метода Get, я RLock при чтении, затем возвращаю содержимое и заканчиваю отложенным RUnlock.

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

Конечно, это проблема в условиях гонки. Поэтому я не хочу возвращать что-то из Get, которое небезопасно изменять, а затем возвращаться к методу Set для обновления. Итак, вот вопросы:

1) Правильно ли я предположил, что эти типы данных представляют проблемы для сценария, подобного этому?

2) Как можно решить эту проблему, чтобы создать метод Get, значениями которого можно свободно манипулировать, не опасаясь неудачи в условиях гонки?

2 ответа

Решение

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

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

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

Так какие у тебя варианты?

  1. Только типы значений хранилища, которые, как вы заметили, могут быть ограничивающими и / или дорогими, потому что все должно быть скопировано.
  2. Храните только некоторые пользовательские типы, которые синхронизируются, также удостоверившись, что они принимают соответствующие блокировки на себя, когда они видоизменяются или читаются.
  3. Сделайте ваш кэш умнее, чтобы у него была концепция владения, при которой он с радостью вернет объект из кеша и позволит только одной подпрограмме "удерживать" его, пока эта подпрограмма не будет завершена. Другие программы должны были ждать, пока этот объект не будет выпущен, пока предыдущая программа не закончила с ним. Либо так, либо вы можете создать метод Get to fail и немедленно вернуться, если он попытался получить элемент, который в данный момент недоступен. Эта концепция широко используется для построения распределенных блокировок в архитектуре клиент-сервер, где может быть много клиентов, которым нужен доступ к объекту, а распределенная блокировка гарантирует, что только один клиент может когда-либо удерживать блокировку.

Считайте, что концепция собственности важна. Кто-то может сказать: просто используйте каналы, которые все исправят. Но вы даже можете оказаться в одной лодке, если отправите ссылочный тип или указатель на структуру на 5 разных каналов. Эти 5 разных каналов могут мутировать один и тот же объект, на котором они держатся. Ой! та же проблема проявляется снова. Вот почему важно, чтобы при передаче элемента на канал вы отказывались от владения, чтобы не изменять его.

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

1) Правильно ли я предположил, что эти типы данных представляют проблемы для сценария, подобного этому?

Да. Карты и фрагменты имеют указатели на внутренние структуры данных, которые не копируются во время назначения. Но так как вы используете interface{} Вы могли бы также иметь структуры с указателями, которые могли бы указывать на структуры с большим количеством указателей и так далее.

2) Как можно решить эту проблему, чтобы создать метод Get, значениями которого можно свободно манипулировать, не опасаясь неудачи в условиях гонки?

Самое простое решение - разрешить возврат объекта только одному клиенту за раз, так что существует только одна живая, изменяемая версия. Вы также можете сериализовать объекты, чтобы все всегда было копией.

Все, что должно быть записано и прочитано одновременно, должно иметь правильную синхронизацию. Период.

Другие вопросы по тегам