безопасно разпространяване на актуализация на указателя между нишките

tl;dr:

 class Controller
 {
 public:
    volatile Netconsole* nc;
    void init();        //initialize the threads
    void calculate();   // handler for the "mothership app"
    void senderThreadLoop();  //also calls reinitNet() if connection is broken.
    void listenerThreadLoop();
    inline void reinitNet(){ delete nc; nc = new Netconsole(); }
 }

// вътре в Json::Value header = nc->Recv();

error: passing 'volatile Netconsole' as 'this' argument discards qualifiers [-fpermissive]

Указателят към екземпляр на помощен клас (Netconsole), споделен между две нишки, трябва да бъде актуализиран и в двете нишки, ако помощният клас е инстанциран отново, но декларирането му като непостоянен генерира горната грешка. Ако се актуализира само в една нишка, другата нишка все още може да използва стар, невалиден указател. Как да се гарантира, че е актуализиран и в двете, но използването на методи през показалеца не задейства горната грешка?

Разширена информация:

Библиотеката "smart glue logic", която пиша, се използва за предаване и конвертиране на съобщения между софтуер на трета страна и персонализирано устройство. Състои се от три основни нишки:

  • манипулатор: основната нишка на приложението на трета страна периодично извиква функция „изчисляване“ в моята библиотека, за да обработва нови актуализации – данни за изпращане, получени данни
  • подаваща нишка, която преобразува и изпраща всичко, което манипулаторът е избутал в изпращащия буфер
  • поток слушател, който преобразува и изпраща всички данни, получени от устройството, в буфер за получаване.

Както подателят, така и нишките слушател използват един и същ клас помощни програми, който управлява мрежовата комуникация с устройството; при инициализация класът създава връзка с устройството и двете нишки изпълняват блокиращи четения или съответно чакат нови данни за изпращане. В случай на проблеми изпращащата нишка извършва цялата работа по „поддръжка“, докато слушащата нишка влиза в безопасно състояние в очакване на връщане на връзката.

Сега, тъй като двете нишки споделят една връзка с устройството, и двете споделят едно и също копие на комуникационния клас като указател към този клас.

Проблемът е в процедурата за повторно свързване - тя включва унищожаване и създаване на екземпляр на помощен клас, използвайки безопасно изключване и инициализация, които вече присъстват в деструктора и конструктора. В резултат на това указателят се променя. Без volatile е много вероятно слушателят да не получи актуализирания указател. С volatile той протестира - ненужно, защото nc (указателят) няма да се промени в произволен момент - първо слушателят се уведомява за проблем, след това влиза в безопасно състояние, в което не извършва никакви операции върху 'nc' и уведомява изпращача, че е готов. Едва тогава подателят извършва поправката и уведомява слушателя да възобнови нормалната работа.

И така, какво е правилното решение в тази ситуация?


person SF.    schedule 14.09.2015    source източник
comment
Използвате std::atomic   -  person NathanOliver    schedule 14.09.2015
comment
volatile не е създаден за безопасност на нишки (и няма да го постигне). Използвайте std::mutex, за да го защитите от условия на състезание.   -  person πάντα ῥεῖ    schedule 14.09.2015
comment
Освен това би трябвало да бъде Netconsole* volatile nc;, за да се постигне заявената цел, дори ако volatile би означавало безопасен за нишки.   -  person MSalters    schedule 14.09.2015
comment
@MSalters: Mutex може да се използва за спиране и стартиране на нишки, а не за предаване на данни от нишка на друга. Тук не става дума за проблем с атомарност/състезание, а за кеширане/оптимизиране на променливи.   -  person SF.    schedule 14.09.2015
comment
(вземете този пример: int x=0; thread1(){ myMutex.lock(); x = 1 ; myMutex.unlock(); } thread2(){ int y=0; myMutex.lock(); y = x ; myMutex.unlock(); cout << y <<endl; } - абсолютно не е гарантирано, че ще отпечатате 1, тъй като старата стойност на x в thread2 може да бъде изтеглена от регистър/кеш, освен ако x не е обявено за непостоянно.   -  person SF.    schedule 14.09.2015
comment
@SF. Не сте прави, вижте: stackoverflow.com/questions/3208060/ и software.intel.com/en-us/blogs/2007/11/30/   -  person NathanOliver    schedule 14.09.2015
comment
@SF: Неправилно си насочил коментара, но πάντα ῥεῖ е прав. Мутексът не споделя данни, но свързаната бариера на паметта гарантира, че споделените данни са видими за другата нишка. Класът std::atomic е полезен, защото неговият .load метод ви позволява да укажете бариера на паметта, независима от мютекс.   -  person MSalters    schedule 14.09.2015
comment
SF, има едно просто правило. Ако използвате volatile, защото имате нужда от него за нишкова комуникация, грешите (освен ако не сте на Intel и знаете какво правите).   -  person SergeyA    schedule 14.09.2015
comment
@SergeyA: Освен ако не сте на Visual Studio и знаете какво правите. Това е разширение на Microsoft. Не мисля, че ICC дава същата гаранция.   -  person MSalters    schedule 14.09.2015
comment
@MSalters, не VisualStudio прави тази гаранция, а Intel. Тъй като компилаторът гарантира, че всички четения от volatile всъщност са четения (а не кеширани) и Intel гарантира кеширане на ядрото и атомарността на четене/запис за подравнени данни, на Intel voliatile ще действа като атомарно независимо от компилатора.   -  person SergeyA    schedule 14.09.2015
comment
@SergeyA: Е, Microsoft го гарантира за ARM, а Intel не го гарантира за SSE поточно зареждане/магазини (които случайно се използват в някои memcpy реализации). Освен това Intel го гарантира само до 8 байта IIRC.   -  person MSalters    schedule 14.09.2015


Отговори (1)


Това, от което се нуждаете, е последователност от операции. Произвеждащата нишка има 2 подходящи операции: „инициализиране на нов Netconsole“ и „запис на указател“. Консумиращата нишка също има две операции: "указател за четене" и "използване на нов Netconsole обект". Тези 4 операции трябва да бъдат подредени в точно този ред, за да бъде видима актуализацията.

Най-лесният начин да постигнете това са две бариери на паметта. Бариера за запис (std::memory_order_release върху записа на указателя) предотвратява пренареждането на първите две операции, а бариерата за четене (std::memory_order_acquire на зареждането на указателя) предотвратява пренареждането на последните две операции.

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

За да обобщим: Да, 4-те операции трябва да се извършат в точно правилния ред, за да бъде видим резултатът, но ако втората и третата операция се пренаредят, тогава актуализацията е напълно невидима към консумиращата нишка. Това е атомна актуализация, всичко или нищо.

Остава да разчистим стария обект. Произвеждащата нишка не може просто да приеме, че нишката консумираща вече е видяла актуализацията на указателя. Трябва да има синхронизация, за да се гарантира, че и двете нишки са съгласни, че старият обект е неизползван. Най-лесно е, ако произвеждащата нишка стриктно не използва стария обект, след като новият обект е създаден (тук помага бариерата на паметта), а нишката консумираща изчисти стария обект веднага щом разбере, че има нов обект (тъй като това се случва точно след бариерата за четене, следователно след бариерата за запис и на свой ред след последното използване от произвеждащата нишка)

person MSalters    schedule 14.09.2015