Դուք չեք օգտագործում ընդհանուր հիշողության հատկացուցիչ տողերի համար: Այդ առումով, ձեր հարցը նույնն է, ինչ circular_buffer and manager_mapped_file հատվածավորման սխալ: Դուք կարող եք կարդալ դա ընդհանուր ներածության համար:
Ձեր օրինակը բարդացնում է իրավիճակը՝ փաթաթելով տողերը ձեր սեփական կառուցվածքների մեջ: Դա նշանակում է, որ դուք ստանում եք շատ հոգնեցուցիչ աշխատանք, որը անցնում է բաշխիչների շուրջը: Uses_allocator մոտեցման համար, որը scoped_allocator_adaptor
-ի հետ միասին կարող է մեղմացնել այդ ցավը, տես, օրինակ. boost::interprocess-ի չհամօգտագործվող պատճենների ստեղծում ընդհանուր հիշողության օբյեկտներ:
Կարդալով ձեր ծածկագրի մնացած մասը, ես մի փոքր շփոթված եմ: Ինչո՞ւ կաղապարեք ձեր SharedMemory
տիպը հատկացնողով: Ես նկատի ունեմ, որ SharedMemory
-ը պետք է լինի մեկ կետը, որը պատասխանատու է ճիշտ բաշխիչ ընտրելու և անցնելու համար, այնպես չէ՞: Ինչպե՞ս կարող էր այն աշխատել արտաքին տրամադրված հատկացնողի հետ:
Կան typedef-ներ, որոնք չօգտագործված են, դուք նոր հատված եք կազմում յուրաքանչյուր օբյեկտի համար, թեև այն կարող է լինել նույն ընդհանուր հիշողությունից (միևնույն էջերը մի քանի անգամ քարտեզագրելով հիշողության մեջ): Այնուամենայնիվ, դուք ինչ-որ կերպ կարծում եք, որ կարևոր է կիսել սեփականության իրավունքը նման օրինակի վրա (make_shared
):
Չափի հաշվարկները պարզապես սխալ են. դրանք հաշվի են առնում միայն ձեր Message
կառուցվածքի չափը, ոչ թե հատկացված տողային տվյալները: Դուք կարծես մոռացել եք, որ քարտեզագրված հիշողությունը նույնպես վիրտուալ հիշողություն է: Հիմքում ընկած պահեստը կկարողանա սակավ հատկացնել: Այսպիսով, ինչո՞ւ չպահպանել մեծ քանակությամբ հիշողություն և պարզապես պատասխանել, երբ սպառվի:
Դուք խոսում և կոդավորում եք (որոշ) շարժման իմաստաբանությունը, բայց հետո գրում եք.
for (std::size_t i = 0; i < max_number_of_elements_in_container; ++i) {
auto msg = feed[i];
shmem_vec[i].message_ = std::move(msg);
}
Դա շփոթված է: Ինչ լավ է քայլը (եթե այն աշխատեց, տե՛ս ստորև), եթե առաջին հերթին, այնուամենայնիվ, բացահայտ պատճենեք.
auto msg = feed[i];
Սրանք մտահոգիչ նշաններ են.
uint_fast64_t init_add_index_;
int_fast64_t init_handle_index_;
Կարծես թե դուք պլանավորում եք դրանք միաժամանակ օգտագործել բազմաթիվ գործընթացներից/թելերից²: Նման դեպքում դուք պետք է ավելացնեք համաժամացում կամ օգտագործեք առնվազն atomic<>
տեսակներ:
Ամփոփելով ինձ թվում է, որ դուք կարող եք այնքան շատ եք փորձում թաքցնել բարդությունը, որ պատահաբար ավելացրել եք այն:
Շարժվելու մասին
Դուք հարցնում եք ընդհանուր հիշողության մեջ ընդհանուր տողը տեղափոխելու մասին: Հարցի այս մասի համար ենթադրենք, որ իրականում ձեր տողերը տեղաբաշխված են եղել ընդհանուր հիշողության մեջ:
Տեսնելով, թե ինչպես են աշխատում շարժվող տողերը, դժվար չէ տեսնել, որ շարժվող տողերը ընդհանուր հիշողության մեջ կաշխատեն ճիշտ այնպես, ինչպես կաշխատեն դրանք կույտի ներսում տեղափոխելը. օբյեկտի հասցեն տարբեր կլինի, բայց հատկացված հիշողության ներքին ցուցիչը նույնը կլինի:
Այնուամենայնիվ, կոդը այլ բան է անում. այն չի տեղափոխվում համօգտագործվող հիշողության մեջ: Այն փորձում է տեղափոխել կույտից ընդհանուր հիշողություն: Սա ակնհայտորեն անվտանգ չի լինի, քանի որ ընդհանուր հիշողության օբյեկտները չեն կարող օգտակար մատնանշել ընդհանուր հիշողության հատվածից դուրս որևէ բան (ցանկացած այլ գործընթաց կառաջարկի չսահմանված վարքագիծ, որն անուղղակի է նման ցուցիչի միջոցով): .
Ինչպես հաճախ, C++-ում դուք մասամբ ձերն եք՝ նման վթարները կանխելու համար՝ C++11 basic_string<>::swap
նշում է.
Վարքագիծը որոշված չէ, եթե Allocator
-ը չի տարածվում swap-ում, և *this
-ի և other
-ի բաշխիչները անհավասար են:
Move-constructor նշված է, որ ունի բարդություն.
մշտական. Եթե տրված է alloc և alloc != other.get_allocator(), ապա գծային
Նկատի ունեցեք, որ բաշխիչների իմաստաբանությունը բեռնարկղերը պատճենելիս/տեղափոխելիս (basic_string<>
-ը կոնտեյներ է, որը նման է std::vector<>
-ին) ավելի շատ է ներգրավված.
Ինչ անել?
Ընդհանուր առմամբ, եթե ձեր բախտը բերել է, քայլը չի կազմվի, քանի որ հատկացուցիչները անհամատեղելի տիպի են և ոչ մեկը չի մատակարարվում (օրինակ՝ use_allocator արձանագրությամբ):
Եթե դուք ավելի քիչ հաջողակ եք, այն կկազմվի, բայց այն (բարեբախտաբար) չի կատարի այդ քայլը, քանի որ հայտնաբերում է, որ հատկացնողները հավասար չեն, և հետևաբար, այն վերադառնում է պահեստի պատճենմանը:
Եթե դուք բացարձակապես անհաջող եք, դուք ընտրել եք այնպիսի կոնֆիգուրացիա, որտեղ տեսակները համատեղելի են, և բաշխիչները կազմաձևված չեն, որպեսզի ապահով կերպով տարածվեն բեռնարկղերի տեղափոխման/պատճենման ժամանակ, կամ մեկ այլ հանգամանք հանգեցնում է նրան, որ հատկացուցիչները չեն կարողանում հայտնաբերել անհամատեղելիությունը¹, և դուք հայտնվում եք UB-ի հետ:
Այս դեպքում շատ ավելի հեշտ տարբերակ կա՝ դուք գիտեք, որ չեք կարող շարժվել: Հետևաբար, մի պահանջեք տեղափոխություն:
Ռիսկը կանխվեց.
Մեր վերքերը բուժելու որոշ ծածկագիր
Կոդի և հարցի բարդությունը խախտելուց հետո, եկեք կառուցողական լինենք և ցույց տանք, թե ինչ կարող ենք անել՝ խնդիրները շտկելու համար.
#include <exception>
#include <iomanip>
#include <iostream>
#include <random>
#include <boost/interprocess/allocators/allocator.hpp>
#include <boost/interprocess/containers/string.hpp>
#include <boost/interprocess/containers/vector.hpp>
#include <boost/interprocess/managed_shared_memory.hpp>
namespace bip = boost::interprocess;
struct BadSharedMemoryAccess final : std::runtime_error {
BadSharedMemoryAccess(std::string msg) : std::runtime_error{ std::move(msg) } {}
};
Դա նախերգանքն է: Հիմա եկեք հայտնենք մեր մտադրությունները.
using Segment = bip::managed_shared_memory;
template <typename U> using Alloc = bip::allocator<U, Segment::segment_manager>;
Սա հեշտացնում է սեգմենտին և դրա հատկացնողներին հղում կատարելը (և գուցե անջատել):
using Message = bip::string;
using Feed = bip::vector<Message>;
using SharedMessage = bip::basic_string<char, std::char_traits<char>, Alloc<char> >;
using SharedFeed = bip::vector<SharedMessage, Alloc<SharedMessage> >;
Պարզապես սահմանեք մեր տիրույթի սուբյեկտները: Օգտագործելով bip::string
/bip::vector
-ը կույտային և ընդհանուր տեղաբաշխման տարբերակների համար՝ մենք ստանում ենք լավագույն փոխազդեցությունը երկուսի միջև;
class MyCustomData final {
public:
using allocator_type = SharedFeed::allocator_type;
MyCustomData(std::size_t capacity, allocator_type alloc)
: messages_(capacity, SharedMessage(alloc), alloc) // don't brace initlaize
{ }
auto& messages() { return messages_; }
auto const& messages() const { return messages_; }
private:
uint_fast64_t init_add_index_ = 0;
int_fast64_t init_handle_index_ = -1;
SharedFeed messages_;
};
Առայժմ հանվել է virtual
կործանիչը և Message
կառուցվածքը, որը հարմարության համար պարզապես փաթաթել է bip::string
:
template <typename T> class SharedMemory final {
public:
template <typename... Args>
SharedMemory(std::string const& shm_segment_name,
std::size_t const segment_size,
std::string const& shm_object_name,
Args&&... args)
: shm_ { bip::open_or_create, shm_segment_name.c_str(), segment_size }
{
data_ = shm_.find_or_construct<T>
(shm_object_name.c_str())
(std::forward<Args>(args)...,
shm_.get_segment_manager())
;
if (!data_) throw BadSharedMemoryAccess {"cannot access " + shm_segment_name + "/" + shm_object_name};
}
T const& get() const { return *data_; }
T& get() { return *data_; }
auto free() const { return shm_.get_free_memory(); }
protected:
T* data_;
private:
Segment shm_;
};
Ինձ զարմացնում է, որ SharedMemory
-ը չափազանց շատ պարտականություններ ունի. մի կողմից այն փորձում է լինել խելացի հղում ընդհանուր օբյեկտների համար, իսկ մյուս կողմից՝ կառավարում է հատվածը: Սա հանգեցնում է խնդիրների, եթե դուք իրականում ցանկանում եք մի հատվածում ունենալ բազմաթիվ օբյեկտներ: Մտածեք բաժանվել Shared::Segment
-ի և Shared::Object<T>
-ի:
Feed generate_heap_feed(size_t n) {
Feed feed;
feed.reserve(n);
for (size_t i = 0; i < n ; ++i) {
feed.emplace_back(
"blablabla11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111"
+ std::to_string(i));
}
return feed;
}
Արդյունահանվել է փորձնական սնուցման գեներատորը main
-ից:
int main() {
static constexpr std::size_t capacity { 1000000 };
static constexpr auto estimate = 300ull << 20; // 300 MiB (<< 10 kilo, << 20 mebi, << 30 gibi)
Սխալ հաշվարկները³ փոխարինեց առատաձեռն գնահատականով: Չափումները տես ստորև:
using SharedData = SharedMemory<MyCustomData>;
SharedData shmem_data("SHM_SEGMENT", estimate, "SHM_CONTAINER", capacity);
std::cout << "Free: " << shmem_data.free() << "\n";
Գեղեցիկ և ընթեռնելի: Իմ համակարգում տպում է "Free: 282572448"
առաջին գործարկման ժամանակ:
Feed const feed = generate_heap_feed(capacity);
SharedFeed& shm_feed = shmem_data.get().messages();
Այժմ մենք ունենք մեր հոսքերը կողք կողքի, եկեք պատճենենք.
// copy feed from heap to shm
auto const n = std::min(feed.size(), shm_feed.size());
std::copy_n(feed.begin(), n, shm_feed.begin());
std::cout << "Copied: " << n << "\n";
std::cout << "Free: " << shmem_data.free() << "\n";
Այսքանը: Մենք չենք փորձում շարժվել, քանի որ գիտենք, որ դա չի կարող աշխատել: bip::basic_string
ճիշտ գիտի, թե ինչպես պատճենել անհամատեղելի բաշխիչների միջև: Ոչ մի քրտինք:
Լավ չափման համար եկեք տպենք որոշ ախտորոշիչ տեղեկատվություն.
{
// check some random samples
std::default_random_engine prng{std::random_device{}()};
auto pick = [&] { return std::uniform_int_distribution<>(0, n-1)(prng); };
for (auto index : {pick(), pick(), pick(), pick()}) {
std::string_view a = feed.at(index);
std::string_view b = shm_feed.at(index);
std::cout << "Message #" << index
<< (a == b? " OK":" FAIL")
<< " " << std::quoted(b) << std::endl;
}
}
}
Դիտեք այն Live on Coliru⁴
Տպումներ, օրինակ՝
Հատկապես նշեք ֆայլի չափերը (--apparent-size
ընդդեմ սկավառակի չափի): Սա հաստատում է իմ տեսակետը նոսր հատկացման մասին: Նույնիսկ եթե դուք վերապահել եք 100 ՏԲ, SHM_CONTAINER-ի արդյունավետ չափը դեռ կկազմի 182 ՄԲ:
ԲՈՆՈՒՍ ԲԱԺԻՆՆԵՐ
Scoped Allocator Adapters
Պարզապես մեկ տողի փոխարինում.
template <typename U> using Alloc = bip::allocator<U, Segment::segment_manager>;
հետ
template <typename U> using Alloc = boost::container::scoped_allocator_adaptor<
bip::allocator<U, Segment::segment_manager> >;
անում է հնարքը՝ բացելով կախարդական հատկացնողի տարածումը, օրինակ. վեկտորից տող իր տարրերը կառուցելիս (emplace
կամ assign
-ով): Այսպիսով, մենք կարող ենք ավելի պարզեցնել copy_n
-ը հետևյալից.
// copy feed from heap to shm
auto const n = std::min(feed.size(), shm_feed.size());
std::copy_n(feed.begin(), n, shm_feed.begin());
std::cout << "Copied: " << n << "\n";
պարզապես՝
shm_feed.assign(feed.begin(), feed.end());
std::cout << "Copied: " << shm_feed.size() << "\n";
Այն ունի ճիշտ նույն բաշխման վարքագիծը, ինչ նախկինում: Դիտեք այն նաև Ուղիղ եթեր Coliru-ում:
Պոլիմորֆ հատկացուցիչներ (c++17)
Սա սկզբունքորեն ոչինչ չի փոխի, բացառությամբ.
- դա կստիպի Feed/SharedFeed-ը և Message/SharedMessage-ը կիսել նույն ստատիկ տիպը
- այն կունենա scoped-allocator վարքագիծ, ինչպես նախկինում լռելյայն
Այնուամենայնիվ, քանի դեռ չենք ստացել համապատասխան աջակցություն ստանդարտի շքեղ ցուցիչների համար, սա երազանք է.
Նորից դարձնո՞ւմ եք Message
Struct:
Դե, Ավելի շատ նման է նորից պայքարին: Ես ընդունում եմ, որ ատում եմ գրել հատկացնող տվյալների տեսակները: Սա, անկասկած, օպտիմալ չէ, բայց դա նվազագույն բանն է, որը ես կարող էի անել, որպեսզի գործն աշխատի.
template <typename Alloc>
struct BasicMessage {
// pre-c++17:
// using allocator_type = typename Alloc::template rebind<char>::other;
using allocator_type = typename std::allocator_traits<Alloc>::template rebind_alloc<char>;
BasicMessage(std::allocator_arg_t, allocator_type alloc)
: _msg(alloc) { }
template <typename T1, typename... T,
typename = std::enable_if_t<
not std::is_same_v<std::allocator_arg_t, std::decay_t<T1> >
>
>
explicit BasicMessage(T1&& a, T&&... init)
: _msg(std::forward<T1>(a), std::forward<T>(init)...) { }
template <typename OtherAlloc>
BasicMessage(BasicMessage<OtherAlloc> const& other, allocator_type alloc)
: _msg(other.message().begin(), other.message().end(), alloc) { }
template <typename OtherAlloc, typename OM = BasicMessage<OtherAlloc> >
std::enable_if_t<
not std::is_same_v<allocator_type, typename OM::allocator_type>,
BasicMessage&>
operator=(BasicMessage<OtherAlloc> const& other) {
_msg.assign(other.message().begin(), other.message().end());
return *this;
}
template <typename OtherAlloc>
BasicMessage(std::allocator_arg_t, allocator_type alloc, BasicMessage<OtherAlloc> const& other)
: _msg(other.message().begin(), other.message().end(), alloc) { }
BasicMessage(BasicMessage const&) = default;
BasicMessage(BasicMessage&&) = default;
BasicMessage& operator=(BasicMessage const&) = default;
BasicMessage& operator=(BasicMessage&&) = default;
auto& message() const { return _msg; }
auto& message() { return _msg; }
private:
bip::basic_string<char, std::char_traits<char>, allocator_type> _msg;
};
using Message = BasicMessage<std::allocator<char> >;
using Feed = bip::vector<Message>;
using SharedMessage = BasicMessage<Alloc<char> >;
using SharedFeed = bip::vector<SharedMessage, Alloc<SharedMessage> >;
Լավ կողմն այն է, որ այն դեռ օգտագործում է կախարդական հանձնարարությունը վերը ներկայացված scoped_allocator_adaptor ուղղման շնորհիվ: Թերևս, եթե դա ցանկալի չլիներ, դուք կարող եք մի փոքր ավելի քիչ բարդությունից խուսափել:
Այլուր ինտերֆեյսի աննշան փոփոխություններով.
: messages_(capacity, SharedMessage(std::allocator_arg, alloc), alloc) // don't brace initlaize
և
std::string_view a = feed.at(index).message();
std::string_view b = shm_feed.at(index).message();
ամեն ինչ դեռ աշխատում է, տես Live On Coliru
¹ ոչ ստանդարտ, հետևաբար վախեցնելու մեջբերումները
² Ես կասկածում եմ, որ դուք կարող եք փորձել կիրառել Խափանողի օրինակը
³ տե՛ս Հիշողության քարտեզագրված ուժեղացման rttree-ի համար պահանջվող չափի գնահատում ա>
4-ը managed_shared_memory
-ը փոխարինեց manage_mapped_file
-ով և նվազեցրեց հզորությունները՝ Coliru-ի սահմանափակումների պատճառով
29.06.2020
bip::string
-ի, բայց ոչbip::vector
-ի մասին, որտեղ դուք նշել եք հատկացուցիչ: Ի՞նչ հատկացուցիչ կօգտագործերbip::string
-ը, եթե այն նախապես միացված լիներ: Ինչպիսի segment_manager-ով այն պետք է մուտքագրվի: 03.07.2020