Каков правильный способ доступа к большому объему данных, которые являются частными для класса?

#c #multithreading #c 98

#c #многопоточность #c 98

Вопрос:

Мне просто интересно, как решить это с помощью C98 (так что нет shared_ptr ):

  • У меня действительно большой класс с большим количеством данных ( BigData )
  • У меня есть класс DataStorage , который отслеживает эти данные в map
  • Данные вряд ли изменятся, но могут
  • Я нахожусь в многопоточной среде

      class BigData;
    
     class DataStorage{
      public:
           const BigData *getStuff(int which_one) const{
              lock_guard<mutex> guard(mut);     
               return amp;myReallyBigDatas[which_one]; // thanks Donghui
           }
      protected:
         mutable mutex mut;
         map<int,BigData> myReallyBigDatas;
     }
      

(как предложил Кит М. Я не упомянул проблему, которую пытаюсь решить)

Я знаю, что этот код неверен, и, с моей точки зрения, я хочу решить две основные проблемы:

  1. Если возвращенный объект исчезнет, потому что эта позиция на карте удалена или перезаписана (у меня будет указатель, указывающий неизвестно куда, UB!)
  2. Если возвращаемый экземпляр BigData изменен

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

Я придумал эти решения:

  1. Включите мьютекс в BigData. Это решает проблему 2
  2. Измените функцию, чтобы она возвращала реальный объект, а не указатель (это очень приятно, но имеет недостаток, заключающийся в снижении производительности при создании копии действительно большого класса) Это решает обе проблемы
  3. Реализую свой собственный класс shared_ptr (не могу использовать C11 или boost). Это решает проблему 1
  4. Создайте блокировку / разблокировку в хранилище данных класса (действительно ужасно!). Это решает обе проблемы
  5. Продолжайте ошибаться и много молитесь. Это….

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

P.S. Я знаю, что использую мьютекс C11. В моем реальном коде этого нет, но так проще написать пример кода.

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

1. Мне не совсем понятно, о чем вы спрашиваете. В чем проблема с этим кодом, с которым вы столкнулись? Данные удаляются или изменяются при извлечении? Или просто плохая производительность? Или это просто выдает вам предупреждения компилятора? У вас уже есть два ответа, но оба они совершенно разные, потому что вы на самом деле не задавали конкретного вопроса.

2. @KeithM спасибо за ваш комментарий, я согласен с вами, что я не говорю точно, что мне нужно решить. У меня есть вопрос редактирования, пытающийся прояснить это

Ответ №1:

На самом деле, shared_ptr и mutex совершенно независимы друг от друга, и вам могут понадобиться оба — shared_ptr используется для гарантии освобождения ровно одного ресурса, в то время как mutex используется для гарантии отсутствия одновременных операций чтения / записи (или также одновременного чтения, зависит от типа мьютекса).

Использование shared_ptr означает в основном, что нет единого владельца данных. Хотя этим можно управлять (например, подсчетом ссылок), это не всегда лучшее решение (помните о циклических зависимостях, необходимости weak_ptr и т.д.) — Иногда лучше найти единственного владельца ресурса, который будет отвечать за его освобождение, когда в нем больше не будет необходимости (например, если у вас есть пул рабочих потоков, это может быть тот, который порождает другие) — конечно, тогда вы должны гарантировать, что он будет жить дольше, чем другие , чтобы сделать данные доступными для всех. Итак, у вас есть несколько вариантов управления временем жизни объекта:

  • «позаимствуйте» код из boost / стандартной библиотеки C 11 / Loki / какой-либо другой существующей реализации (ознакомьтесь с лицензиями, чтобы убедиться, что вы можете их использовать) без использования всей библиотеки — вам, вероятно, потребуется внести в них изменения
  • напишите свой собственный интеллектуальный указатель — жесткий и только для профессионалов — категорически не рекомендуется
  • выберите единственного владельца ресурса — это то, что я бы рекомендовал

Когда дело доходит до управления конфликтами доступа, в основном существует два подхода:

  • управляйте ими, используя какой-либо вид блокировки (я предполагаю, что у вас есть один доступный)
  • избегайте их, разрешая запись в объект только одному потоку. Другие, которые обычно могут захотеть, должны будут запросить запись у «потока владельца». Этот подход довольно хорошо подходит для одного владельца ресурса, но это скорее модель актора, чем типичная многопоточность с общей памятью, поэтому его может быть сложно внедрить в устаревшее приложение.

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

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

РЕДАКТИРОВАТЬ 1

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

Самое простое, что вы можете сделать, это вернуть BigDataamp; вместо BigData* — тогда никто не должен его удалять (конечно, это возможно, но практически все есть на C ). В противном случае вы также можете:

  • разрешить только одному потоку использовать один BigData экземпляр — (например, путем сохранения дополнительной std::map<int, thread_id> информации об использованном BigData экземпляре — только если вам не требуется одновременный доступ к одному экземпляру из нескольких потоков
  • верните что-то вроде BigDataProxy вместо BigData — у Proxy должна быть специальная функция для удаления ресурсов, которая затем будет выполнена «последним заинтересованным» — это действительно просто частный случай shared_ptr , но может быть проще в реализации.

Концептуально Proxy это было бы что-то вроде (псевдокод — игнорирование частных / общедоступных членов и т.д.):

 class BigDataProxy {
  public:
  BigDataProxy(data_, instanceId_): data(data_), instanceId(instanceId_) {
    std::lock_guard l(data.mutex);
    data.interestedThreads[instanceId].insert(this_thread::thread_id);
  }

  ~BigDataProxy() {
    std::lock_guard l(data.mutex);
    data.interestedThreads[instanceId].remove(this_thread::thread_id)
    if(data.interestedThreads[instanceId].empty() amp;amp; data.toDelete.contains(instanceId) {
      data.elems.remove(instanceId);
      data.toDelete.remove(instanceId);
    }
  }

  BigDataamp; operator*() {
    return data.elems[instanceId];
  }
  void remove() {
     std::lock_guard l(data.mutex);
     data.toDelete.add(instanceId);
  }

  private: 
    DataStorageamp; data;
    int instanceId;
}
  

с изменениями в DataStorage , требующими, чтобы это выглядело следующим образом:

 class DataStorage {

  std::mutex mutex;
  std::map<int, BigData> elems;
  std::set<int> toDelete;
  std::map<int, std::set<thread_id> > interested_threads;
}
  

Обратите внимание, что это псевдокод, и обработка исключений здесь будет сложной.

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

1. спасибо за ваше объяснение. Что касается управления временем жизни, я предполагаю, что тот, кто создал этот код, выбрал «выбрать единственного владельца ресурса» , но я не могу понять, как это гарантировать. Если у какого-то другого класса есть указатель на мои внутренние данные, как я не могу отследить время жизни этого указателя без smartpointer? (Я почти уверен, что вы уже ответили на этот вопрос в своем сообщении, но, пожалуйста, проясните этот момент).

2. Некоторые вопросы: Если я вернусь BigDataamp; , возможно ли, что кто-то установил другой экземпляр на моей карте, и эта ссылка никуда не указывает?. И еще один, контроль доступа по потоку, я не знаю, как этого добиться. Вы имеете в виду, что я возвращаю значение NULL, если поток уже обращается к этому элементу?

3. 1.) Теоретически это возможно, выполнив что-то вроде const_cast<void*>(amp;myData) = nullptr; , но писать это совершенно неразумно. Когда вы возвращаете ссылку, становится ясно, что вы не передаете права собственности на объект (это неясно, когда вы возвращаете указатель). 2.) Это действительно зависит от вашего случая — вы можете вернуть nullptr , создать исключение, заблокировать мьютекс во время запроса или во время первого использования — все это допустимые решения.

4. Большое спасибо за вашу помощь!, теперь у меня есть лучшие способы дополнить этот код!

Ответ №2:

Ваша идея верна: сохранить конфиденциальность данных и предоставить средство получения для получения данных или части данных.

В вашем коде ошибка. Подпись GetStuff() возвращает указатель на BigData, но реализация возвращает ссылку на BigData. Несоответствие типов.

Вы правы в том, что не хотите создавать копии BigData. Итак, у вас есть три варианта:

  • верните myReallyBigDatas[which_one]; // верните BigDataamp;
  • return amp;myReallyBigData[which_one]; // вернуть большие данные*
  • верните myReallyBigDatas.find(which_one); // верните map::iterator

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

1. Можете ли вы объяснить свой последний возврат? возврат итератора выглядит как очень плохое решение

2. Последний возврат — это возврат map::iterator . Что заставляет вас думать, что возврат итератора — это очень плохое решение?