विंडोज बनाम लिनक्स - सी ++ थ्रेड पूल मेमोरी उपयोग

Aug 17 2020

मैं विंडोज और लिनक्स (डेबियन) में कुछ सी ++ रीस्ट एपीआई फ्रेमवर्क के मेमोरी उपयोग को देख रहा हूं। विशेष रूप से, मैंने इन दो रूपरेखाओं को देखा है: cpprestsdk और cpp-roleplib । दोनों में, एक थ्रेड पूल बनाया जाता है और सेवा अनुरोधों के लिए उपयोग किया जाता है।

मैंने सीपीपी-कैंसिलिब से थ्रेड पूल कार्यान्वयन लिया और स्मृति उपयोग को दिखाने के लिए इसे नीचे एक न्यूनतम कार्यशील उदाहरण में रखा, जिसे मैं विंडोज और लिनक्स पर देख रहा हूं।

#include <cassert>
#include <condition_variable>
#include <functional>
#include <iostream>
#include <list>
#include <map>
#include <memory>
#include <mutex>
#include <string>
#include <thread>
#include <vector>

using namespace std;

// TaskQueue and ThreadPool taken from https://github.com/yhirose/cpp-httplib
class TaskQueue {
public:
    TaskQueue() = default;
    virtual ~TaskQueue() = default;

    virtual void enqueue(std::function<void()> fn) = 0;
    virtual void shutdown() = 0;

    virtual void on_idle() {};
};

class ThreadPool : public TaskQueue {
public:
    explicit ThreadPool(size_t n) : shutdown_(false) {
        while (n) {
            threads_.emplace_back(worker(*this));
            cout << "Thread number " << threads_.size() + 1 << " has ID " << threads_.back().get_id() << endl;
            n--;
        }
    }

    ThreadPool(const ThreadPool&) = delete;
    ~ThreadPool() override = default;

    void enqueue(std::function<void()> fn) override {
        std::unique_lock<std::mutex> lock(mutex_);
        jobs_.push_back(fn);
        cond_.notify_one();
    }

    void shutdown() override {
        // Stop all worker threads...
        {
            std::unique_lock<std::mutex> lock(mutex_);
            shutdown_ = true;
        }

        cond_.notify_all();

        // Join...
        for (auto& t : threads_) {
            t.join();
        }
    }

private:
    struct worker {
        explicit worker(ThreadPool& pool) : pool_(pool) {}

        void operator()() {
            for (;;) {
                std::function<void()> fn;
                {
                    std::unique_lock<std::mutex> lock(pool_.mutex_);

                    pool_.cond_.wait(
                        lock, [&] { return !pool_.jobs_.empty() || pool_.shutdown_; });

                    if (pool_.shutdown_ && pool_.jobs_.empty()) { break; }

                    fn = pool_.jobs_.front();
                    pool_.jobs_.pop_front();
                }

                assert(true == static_cast<bool>(fn));
                fn();
            }
        }

        ThreadPool& pool_;
    };
    friend struct worker;

    std::vector<std::thread> threads_;
    std::list<std::function<void()>> jobs_;

    bool shutdown_;

    std::condition_variable cond_;
    std::mutex mutex_;
};

// MWE
class ContainerWrapper {
public:
    ~ContainerWrapper() {
        cout << "Destructor: data map is of size " << data.size() << endl;
    }

    map<pair<string, string>, double> data;
};

void handle_post() {
    
    cout << "Start adding data, thread ID: " << std::this_thread::get_id() << endl;

    ContainerWrapper cw;
    for (size_t i = 0; i < 5000; ++i) {
        string date = "2020-08-11";
        string id = "xxxxx_" + std::to_string(i);
        double value = 1.5;
        cw.data[make_pair(date, id)] = value;
    }

    cout << "Data map is now of size " << cw.data.size() << endl;

    unsigned pause = 3;
    cout << "Sleep for " << pause << " seconds." << endl;
    std::this_thread::sleep_for(std::chrono::seconds(pause));
}

int main(int argc, char* argv[]) {

    cout << "ID of main thread: " << std::this_thread::get_id() << endl;

    std::unique_ptr<TaskQueue> task_queue(new ThreadPool(40));

    for (size_t i = 0; i < 50; ++i) {
        
        cout << "Add task number: " << i + 1 << endl;
        task_queue->enqueue([]() { handle_post(); });

        // Sleep enough time for the task to finish.
        std::this_thread::sleep_for(std::chrono::seconds(5));
    }

    task_queue->shutdown();

    return 0;
}

जब मैं इस MWE को चलाता हूं और विंडोज बनाम लिनक्स में मेमोरी की खपत को देखता हूं, तो मुझे नीचे दिया गया ग्राफ़ मिलता है। विंडोज के लिए, मैं निजी बाइट्स मूल्य perfmonप्राप्त करता था । लिनक्स में, मैं कंटेनर के मेमोरी उपयोग को लॉग करता था। यह कंटेनर के अंदर चलने से प्रक्रिया के अनुरूप था । यह ग्राफ़ से प्रकट होता है कि जब कोई फ़ंक्शन फ़ंक्शन में विंडोज में चर के लिए मेमोरी आवंटित करता है, तो वह मेमोरी वापस दी जाती हैdocker stats --no-stream --format "{{.MemUsage}}restopmaphandle_postजब फ़ंक्शन फ़ंक्शन के अगले कॉल से पहले बाहर निकलता है। यह उस प्रकार का व्यवहार था जिसकी मुझे भली प्रकार से अपेक्षा थी। मुझे इस बारे में कोई अनुभव नहीं है कि ओएस एक फ़ंक्शन द्वारा आवंटित मेमोरी के साथ कैसे व्यवहार करता है जिसे थ्रेड में जीवित किया जाता है जब थ्रेड जीवित रहता है, जैसे कि थ्रेड पूल में यहां। लिनक्स पर, ऐसा लगता है कि मेमोरी उपयोग बढ़ता रहता है और फ़ंक्शन से बाहर निकलने पर मेमोरी वापस नहीं दी जाती है। जब सभी 40 थ्रेड्स का उपयोग किया गया है, और प्रक्रिया करने के लिए 10 और कार्य हैं, तो मेमोरी का उपयोग बढ़ना बंद हो जाता है। क्या कोई लिनक्स में एक उच्च स्तरीय दृश्य दे सकता है जो कि मेमोरी प्रबंधन के दृष्टिकोण से लिनक्स में हो रहा है या यहां तक ​​कि कुछ बिंदुओं के बारे में जहां इस विशिष्ट विषय पर कुछ पृष्ठभूमि जानकारी देखने के लिए है?

संपादन 1 : मैंने नीचे दिए गए ग्राफ़ को लिनक्स कंटेनर में हर दूसरे rssसे चलने के आउटपुट मान को दिखाने के लिए संपादित किया है ps -p <pid> -h -o etimes,pid,rss,vszजहां <pid>प्रक्रिया की आईडी परीक्षण की जा रही है। के आउटपुट के साथ यह उचित समझौता है docker stats --no-stream --format "{{.MemUsage}}

संपादित 2 : एसटीएल आवंटनकर्ताओं के संबंध में नीचे एक टिप्पणी के आधार पर, मैंने handle_postनिम्नलिखित के साथ फ़ंक्शन को शामिल #include <cstdlib>करके और शामिल करने के साथ एमडब्ल्यूई से नक्शा हटा दिया #include <cstring>। अब, handle_postफ़ंक्शन केवल 500K ints के लिए मेमोरी आवंटित करता है और सेट करता है जो लगभग 2MiB है।

void handle_post() {
    
    size_t chunk = 500000 * sizeof(int);
    if (int* p = (int*)malloc(chunk)) {

        memset(p, 1, chunk);
        cout << "Allocated and used " << chunk << " bytes, thread ID: " << this_thread::get_id() << endl;
        cout << "Memory address: " << p << endl;

        unsigned pause = 3;
        cout << "Sleep for " << pause << " seconds." << endl;
        this_thread::sleep_for(chrono::seconds(pause));

        free(p);
    }
}

मुझे यहाँ भी वही व्यवहार मिलता है। मैंने उदाहरणों में थ्रेड्स की संख्या 8 और कार्यों की संख्या घटाकर 10 कर दी है। नीचे दिया गया ग्राफ़ परिणाम दिखाता है।

संपादन 3 : मैंने परिणामों को लिनक्स सेंटोस मशीन पर चलाने से जोड़ा है। यह मोटे तौर पर डेबियन डोकर छवि परिणाम के परिणामों से सहमत है।

संपादित करें 4 : नीचे एक और टिप्पणी के आधार पर, मैं नीचे उदाहरण भाग गया valgrindके massifउपकरण। massifआदेश पंक्ति पैरामीटर नीचे दी गई छवियों में हैं। मैंने इसे --pages-as-heap=yesनीचे, दूसरी छवि के साथ , और इस ध्वज के बिना, नीचे पहली छवि के साथ चलाया । पहली छवि यह बताएगी कि ~ 2MiB मेमोरी को (साझा) हीप को आवंटित किया जाता है क्योंकि handle_postफ़ंक्शन को एक थ्रेड पर निष्पादित किया जाता है और फिर फ़ंक्शन से बाहर निकलने के रूप में मुक्त किया जाता है। यह वही है जो मैं उम्मीद करता हूं और जो मैं विंडोज पर देखता हूं। मुझे यकीन नहीं है कि ग्राफ की व्याख्या --pages-as-heap=yesअभी तक कैसे की जाएगी, अर्थात दूसरी छवि।

मैं ऊपर के ग्राफ़ में दिखाए गए कमांड से massifमूल्य के साथ पहली छवि में आउटपुट को समेट नहीं सकता । अगर मैं डॉकर छवि को चलाता हूं और कंटेनर मेमोरी को 12 एमबी का उपयोग करके सीमित करता है , तो कंटेनर 7 वें आवंटन पर मेमोरी से बाहर चला जाता है और ओएस द्वारा मार दिया जाता है। मैं आउटपुट में मिलता हूं और जब मैं देखता हूं, तो देखता हूं । इससे यह पता चलता है कि मूल्य वास्तव में प्रक्रिया द्वारा उपयोग की जा रही (हीप) मेमोरी को प्रतिबिंबित कर रहा है, जबकि उपकरण यह गणना कर रहा है कि यह / और / कॉल पर आधारित होना चाहिए । यह इस परीक्षण से सिर्फ मेरी बुनियादी धारणा है। मेरा प्रश्न अभी भी खड़ा होगा यानी ऐसा क्यों है, या ऐसा प्रतीत होता है कि, समारोह से बाहर निकलने पर हीप मेमोरी को मुक्त नहीं किया जा रहा है और न ही निपटाया जा रहा है?rsspsdocker run --rm -it --privileged --memory="12m" --memory-swap="12m" --name=mwe_test cpp_testing:1.0KilleddmesgKilled process 25709 (cpp_testing) total-vm:529960kB, anon-rss:10268kB, file-rss:2904kB, shmem-rss:0kBrsspsmassifmallocnewfreedeletehandle_post

संपादित करें 5 : मैंने मेमोरी उपयोग के एक ग्राफ के नीचे जोड़ा है क्योंकि आप थ्रेड पूल में थ्रेड्स की संख्या 1 से 4 तक बढ़ाते हैं। पैटर्न जारी रहता है क्योंकि आप थ्रेड्स की संख्या 10 तक बढ़ाते हैं इसलिए मैंने 5 से 10 को शामिल नहीं किया है ध्यान दें कि मैंने शुरुआत में 5 सेकंड का ठहराव जोड़ा है, mainजो पहले ~ 5secs के लिए ग्राफ में प्रारंभिक फ्लैट लाइन है। ऐसा प्रतीत होता है कि, थ्रेड काउंट की परवाह किए बिना, पहले कार्य को संसाधित करने के बाद मेमोरी रिलीज़ होती है, लेकिन उस मेमोरी को रिलीज़ नहीं किया जाता है (पुन: उपयोग के लिए?) कार्य 2 के बाद 10. के माध्यम से यह सुझाव हो सकता है कि कुछ मेमोरी आवंटन पैरामीटर के दौरान ट्यून किया गया है कार्य 1 निष्पादन (केवल ज़ोर से सोचकर!)।

संपादित करें 6 : नीचे दिए गए विस्तृत उत्तर के सुझाव के आधार पर , मैंने MALLOC_ARENA_MAXउदाहरण को चलाने से पहले पर्यावरण चर को 1 और 2 पर सेट किया । यह निम्न ग्राफ़ में आउटपुट देता है। यह उत्तर में दिए गए इस चर के प्रभाव की व्याख्या के आधार पर अपेक्षित है।

जवाब

2 BeeOnRope Aug 20 2020 at 03:07

कई आधुनिक एलोकेटर, जिसमें glibc 2.17 शामिल है, जिसका आप उपयोग कर रहे हैं, एक ही समय में आवंटित होने वाले थ्रेड्स के बीच विवाद से बचने के लिए कई एरेनास (एक संरचना जो मुक्त मेमोरी क्षेत्रों को ट्रैक करता है) का उपयोग करें।

एक अखाड़े से मुक्त हुई स्मृति दूसरे अखाड़े द्वारा आवंटित किए जाने के लिए उपलब्ध नहीं है (जब तक कि कुछ प्रकार के क्रॉस-अखाड़ा हस्तांतरण को ट्रिगर नहीं किया जाता है)।

डिफ़ॉल्ट रूप से, ग्लिबक हर बार एक नया धागा आवंटित करेगा जब तक कि एक नया धागा आवंटन नहीं करता है, जब तक कि एक पूर्वनिर्धारित सीमा हिट नहीं होती है (जो कि सीपीयू की 8 * संख्या में चूक होती है) जैसा कि आप कोड की जांच करके देख सकते हैं ।

इसका एक परिणाम यह है कि एक थ्रेड पर मुक्त की गई मेमोरी अन्य थ्रेड्स के लिए उपलब्ध नहीं हो सकती है क्योंकि वे अलग-अलग क्षेत्रों का उपयोग कर रहे हैं, भले ही वह थ्रेड कोई उपयोगी काम नहीं कर रहा हो।

आप सभी थ्रेड्स को एक ही अखाड़े के लिए मजबूर करने के लिए ग्लिबेक मॉलोक ट्यून glibc.malloc.arena_max करने की कोशिश कर सकते 1हैं और देखें कि क्या यह आपके द्वारा देखे जा रहे व्यवहार को बदल देता है।

ध्यान दें कि यह सब कुछ उपयोगकर्ता के आवंटन आवंटन (libc में) के साथ करना है और स्मृति के OS आवंटन के साथ कुछ नहीं करना है: OS को कभी सूचित नहीं किया जाता है कि मेमोरी को मुक्त कर दिया गया है। यहां तक ​​कि अगर आप किसी एकल क्षेत्र को मजबूर करते हैं, तो इसका मतलब यह नहीं है कि उपयोगकर्ता स्थान आवंटित करने वाला ओएस को सूचित करने का फैसला करेगा: यह बस भविष्य के अनुरोध को संतुष्ट करने के लिए स्मृति को बनाए रख सकता है (इस व्यवहार को भी समायोजित करने के लिए ट्यूबल हैं)।

हालांकि, आपके परीक्षण में एकल अखाड़े का उपयोग करते हुए लगातार बढ़ रहे मेमोरी फ़ुटप्रिंट को रोकने के लिए पर्याप्त होना चाहिए क्योंकि मेमोरी अगले फ़्रेग शुरू होने से पहले मुक्त हो जाती है, और इसलिए हम इसे अगले कार्य द्वारा पुन: उपयोग करने की अपेक्षा करते हैं, जो एक अलग थ्रेड पर शुरू होता है।

अंत में, यह इंगित करने योग्य है कि क्या होता है यह अत्यधिक निर्भर करता है कि थ्रेड्स को स्थिति चर द्वारा कैसे अधिसूचित किया जाता है: संभवतः लिनक्स एक फीफो व्यवहार का उपयोग करता है, जहां सबसे हाल ही में कतारबद्ध (प्रतीक्षा) धागा अंतिम रूप से अधिसूचित किया जाएगा। यह आपको सभी थ्रेड्स के माध्यम से चक्र करने के लिए प्रेरित करता है क्योंकि आप कार्य जोड़ते हैं, जिससे कई एरेना बनते हैं। एक अधिक कुशल पैटर्न (विभिन्न कारणों के लिए) एक एलआईएफओ नीति है: अगले काम के लिए सबसे हाल ही में संलग्न धागे का उपयोग करें। इससे आपके परीक्षण में एक ही धागे का बार-बार उपयोग किया जाएगा और समस्या को "हल" किया जाएगा।

अंतिम नोट: कई आवंटनकर्ता, लेकिन आपके द्वारा उपयोग किए जा रहे ग्लिबक के पुराने संस्करण में नहीं, एक प्रति-थ्रेड कैश को भी लागू करें जो आवंटन को बिना किसी परमाणु संचालन के आगे बढ़ने की अनुमति देता है । यह कई एरेनास के उपयोग के समान प्रभाव पैदा कर सकता है, और जो थ्रेड्स की संख्या के साथ स्केलिंग करता रहता है।