কেন থ্রেডগুলি জুড়ে একটি ভাগ করে নেওয়া পরিবর্তনশীল কোডটি দৃশ্যত কোনও রেসের শর্তে ভুগছে না?


107

আমি সাইগউইন জিসিসি ব্যবহার করছি এবং এই কোডটি চালাচ্ছি:

#include <iostream>
#include <thread>
#include <vector>
using namespace std;

unsigned u = 0;

void foo()
{
    u++;
}

int main()
{
    vector<thread> threads;
    for(int i = 0; i < 1000; i++) {
        threads.push_back (thread (foo));
    }
    for (auto& t : threads) t.join();

    cout << u << endl;
    return 0;
}

লাইন দিয়ে সংকলিত: g++ -Wall -fexceptions -g -std=c++14 -c main.cpp -o main.o

এটি 1000 মুদ্রণ করে, যা সঠিক। যাইহোক, থ্রেডগুলির কারণে পূর্বের বর্ধিত মানকে ওভাররাইটিংয়ের কারণে আমি কম সংখ্যার প্রত্যাশা করেছি। এই কোডটি পারস্পরিক অ্যাক্সেসে ভুগছে না কেন?

আমার পরীক্ষার মেশিনটিতে 4 টি কোর রয়েছে এবং আমি যে প্রোগ্রামটি জানি তার উপরে আমি কোনও বিধিনিষেধ রাখি না।

fooআরও জটিল কিছু দিয়ে ভাগ করে নেওয়া সামগ্রীর প্রতিস্থাপন করার সময় সমস্যাটি থেকেই যায়

if (u % 3 == 0) {
    u += 4;
} else {
    u -= 1;
}

66
এসএমপি সিস্টেমে (ডুয়াল পেন্টিয়াম প্রো মেশিনের মতো) খুব ব্যবহৃত x86 সিপিইউগুলির সাথে সামঞ্জস্যতা রক্ষার জন্য ইন্টেল সিপিইউগুলির কিছু আশ্চর্যজনক অভ্যন্তরীণ "শট ডাউন" যুক্তি রয়েছে। আমাদের শিখানো হয় যে প্রচুর ব্যর্থতার শর্তগুলি x x মেশিনে আসলে কখনই ঘটে না। সুতরাং একটি কোর uমেমরি ফিরে লিখতে যান বলে। সিপিইউ আসলে আশ্চর্যজনক কাজগুলি করবে যে নোটিশের জন্য মেমরি লাইনটি uসিপিইউ'র ক্যাশে নেই এবং এটি বর্ধিত ক্রিয়াকলাপ পুনরায় চালু করবে। এই কারণেই x86 থেকে অন্যান্য আর্কিটেকচারে যাওয়া চোখের খোলার অভিজ্ঞতা হতে পারে!
ডেভিড শোয়ার্জ

1
সম্ভবত এখনও খুব দ্রুত। অন্যান্য থ্রেডগুলি সম্পূর্ণ হওয়ার আগে এটি আরম্ভ হবে তা নিশ্চিত করার জন্য থ্রেডটি কিছু করার আগে ফলন হয়েছে তা নিশ্চিত করার জন্য আপনাকে কোড যুক্ত করতে হবে।
রব কে

1
অন্য কোথাও উল্লেখ করা হয়েছে, থ্রেড কোডটি এত সংক্ষিপ্ত, পরবর্তী থ্রেডটি সারিবদ্ধ হওয়ার আগে এটি কার্যকর করা যেতে পারে। 100 টি কাউন্টের লুপে ইউ ++ স্থাপন করা প্রায় 10 টি থ্রেড। এবং লুপটি শুরুর আগে একটি সংক্ষিপ্ত বিলম্ব (বা
সমস্তগুলি

5
আসলে, প্রোগ্রামটি বারবার একটি লুপে ছড়িয়ে দেওয়া শেষ পর্যন্ত দেখায় যে এটি ভেঙে গেছে: while true; do res=$(./a.out); if [[ $res != 1000 ]]; then echo $res; break; fi; done;আমার সিস্টেমে 999 বা 998 প্রিন্ট করার মতো কিছু ।
ড্যানিয়েল কামিল কোজার 24'17

উত্তর:


266

foo()এত ছোট যে প্রতিটি থ্রেড সম্ভবত পরবর্তীটি তৈরি হওয়ার আগেই শেষ হয়। যদি আপনি এর foo()আগে এলোমেলো সময়ের জন্য একটি ঘুম জুড়েন তবে u++আপনি যা আশা করেন তা দেখতে শুরু করতে পারেন।


51
এটি প্রকৃতপক্ষে প্রত্যাশিত উপায়ে আউটপুট পরিবর্তন করেছে।
মাফু

49
আমি নোট করব যে এটি সাধারণভাবে জাতি শর্ত প্রদর্শনের জন্য একটি বরং ভাল কৌশল। আপনার যে কোনও দুটি ক্রিয়াকলাপের মধ্যে বিরতি দেওয়ার জন্য সক্ষম হওয়া উচিত; যদি না হয় তবে রেসের শর্ত আছে।
ম্যাথিউ এম।

আমরা সম্প্রতি সি # তে এই সমস্যাটি নিয়েছিলাম। কোড প্রায়শই ব্যর্থ হয় না, তবে সাম্প্রতিক কোনও এপিআই কলের সংযোজন এটিকে ধারাবাহিকভাবে পরিবর্তন করতে পর্যাপ্ত বিলম্ব প্রবর্তন করেছিল।
ওবসিডিয়ান ফিনিক্স

@MatthieuM। মাইক্রোসফ্টের কী এমন কোনও স্বয়ংক্রিয় সরঞ্জাম নেই যা সঠিকভাবে এটি করতে পারে, উভয়ই জাতিগুলির শর্তগুলি সনাক্তকরণ এবং এগুলি নির্ভরযোগ্যভাবে পুনরুত্পাদনযোগ্য করে তোলার একটি পদ্ধতি হিসাবে?
ম্যাসন হুইলারের

1
@ মেসনওহিলার: আমি লিনাক্সে একচেটিয়াভাবে কাজ করি তাই ... ডুনো :(
ম্যাথিউ এম।

59

জাতিটির শর্তটি বোঝা গুরুত্বপূর্ণ যে কোডটি ভুলভাবে চলবে কিনা তার গ্যারান্টি দেয় না, নিছক এটি কিছু করতে পারে, কারণ এটি একটি অপরিজ্ঞাত আচরণ। প্রত্যাশিতভাবে চালানো সহ।

বিশেষত এক্স 86 এবং এএমডি 64 মেশিনের রেসের শর্তগুলি কিছু ক্ষেত্রে খুব কমই সমস্যার কারণ হতে পারে কারণ অনেকগুলি নির্দেশাবলী পারমাণবিক এবং সহজাত গ্যারান্টি খুব বেশি। এই গ্যারান্টিগুলি মাল্টি প্রসেসর সিস্টেমে কিছুটা হ্রাস করা হয়েছে যেখানে লক উপসর্গটি বেশিরভাগ নির্দেশকে পারমাণবিক হওয়ার জন্য প্রয়োজন।

যদি আপনার মেশিনে বৃদ্ধি কোনও পারমাণবিক বিকল্প হয় তবে ভাষা মান অনুসারে এটি অপরিজ্ঞাত আচরণগত হলেও এটি সম্ভবত সঠিকভাবে চলবে।

বিশেষত আমি প্রত্যাশা করি এই ক্ষেত্রে কোডটি কোনও পারমাণবিক আনয়ন এবং নির্দেশনা যুক্ত করতে পারে (এক্স ৮ assembly এ্যাসেম্বলিতে এডিডি বা এক্সএডিডি) যা প্রকৃতপক্ষে একক প্রসেসর সিস্টেমে পারমাণবিক, তবে মাল্টিপ্রসেসর সিস্টেমে এটি পারমাণবিক এবং লক হওয়ার নিশ্চয়তা নেই এটি করা প্রয়োজন হবে। আপনি যদি কোনও মাল্টিপ্রসেসর সিস্টেমে চলেছেন তবে একটি উইন্ডো থাকবে যেখানে থ্রেডগুলি হস্তক্ষেপ করতে পারে এবং ভুল ফলাফল তৈরি করতে পারে।

বিশেষত আমি আপনার কোডটি https://godbolt.org/ ব্যবহার করে সমাবেশে সংকলিত করেছি এবং এতে foo()সংকলন করেছি:

foo():
        add     DWORD PTR u[rip], 1
        ret

এর অর্থ এটি সম্পূর্ণরূপে একটি অতিরিক্ত নির্দেশনা সম্পাদন করছে যা কোনও একক প্রসেসরের জন্য পারমাণবিক হবে (যদিও উপরে উল্লিখিত হিসাবে এটি মাল্টি প্রসেসর সিস্টেমের জন্য নয়)।


41
এটি মনে রাখা গুরুত্বপূর্ণ যে "উদ্দেশ্যে হিসাবে চালানো" অপরিজ্ঞাত আচরণের অনুমোদিত ফলাফল outcome
চিহ্নিত করুন

3
আপনি ইঙ্গিত হিসাবে, এই নির্দেশনা একটি এসএমপি মেশিনে (যা সব আধুনিক সিস্টেম হয়) এ পারমাণবিক নয় । এমনকি inc [u]পারমাণবিক নয়। LOCKউপসর্গ একটি নির্দেশ সত্যিই পারমাণবিক তৈরি করতে প্রয়োজন বোধ করা হয়। ওপি সহজভাবে ভাগ্যবান হয়ে উঠছে। মনে রাখবেন যে আপনি সিপিইউকে "এই ঠিকানায় 1 শব্দটি যুক্ত করুন" বলার পরেও, সিপিইউকে এখনও আনতে হবে, বৃদ্ধি করতে হবে, সেই মানটি সংরক্ষণ করতে হবে এবং অন্য সিপিইউ একই জিনিস একই সাথে করতে পারে, যার ফলে ফলাফলটি ভুল হয়েছিল।
জোনাথন রাইনহার্ট

2
আমি নীচে ভোট দিয়েছি, কিন্তু তারপরে আমি আপনার প্রশ্নটি আবার পড়েছি এবং বুঝতে পেরেছি যে আপনার পারমাণবিকতা সংক্রান্ত বিবৃতিগুলি একটি সিপিইউ ধরেছে। আপনি যদি এটিকে আরও স্পষ্ট করতে আপনার প্রশ্নটি সম্পাদনা করেন (যখন আপনি "পারমাণবিক" বলবেন, তখন পরিষ্কার হয়ে নিন যে এটি কেবলমাত্র একটি সিপিইউ-তে রয়েছে), তবে আমি আমার ডাউন-ভোট সরিয়ে ফেলতে সক্ষম হব।
জোনাথন রাইনহার্ট

3
নিম্নবিত্ত, আমি এই দাবিকে কিছুটা মেহ পেয়েছি "বিশেষত X86 এবং এএমডি 64 মেশিনের রেসের অবস্থার কারণে কিছু ক্ষেত্রে খুব কমই সমস্যা দেখা দেয় কারণ অনেকগুলি নির্দেশনা পারমাণবিক এবং সংহতির গ্যারান্টি খুব বেশি" " অনুচ্ছেদে আপনার একক মূলের দিকে ফোকাস করছেন এমন সুস্পষ্ট ধারণা তৈরি করা শুরু করা উচিত। তবুও, আজকাল ভোক্তা ডিভাইসে মাল্টি-কোর আর্কিটেকচারগুলি ডি-ফ্যাক্টো স্ট্যান্ডার্ড যা আমি প্রথমটির চেয়ে শেষেরটিকে ব্যাখ্যা করার জন্য এটি কর্নার কেস হিসাবে বিবেচনা করব।
প্যাট্রিক ট্রেনটিন

3
ওহ, অবশ্যই। x86 এর অনেকগুলি পিছনে-সামঞ্জস্য রয়েছে ... সঠিকভাবে ভুল লিখিত কোডটি সম্ভব পরিমাণে কাজ করে তা নিশ্চিত করার জন্য স্টফ রয়েছে। পেন্টিয়াম প্র-অফ-অর্ডার কার্যকর করার সময় এটি সত্যিই বড় ব্যাপার ছিল। ইন্টেল এটি নিশ্চিত করতে চেয়েছিল যে কোডের ইনস্টলড বেসটি তাদের নতুন চিপের জন্য বিশেষভাবে পুনরায় সংযুক্ত করার প্রয়োজন ছাড়াই কাজ করেছে। x86 একটি সিআইএসসি কোর হিসাবে শুরু হয়েছিল, তবে এটি অভ্যন্তরীণভাবে একটি আরআইএসসি কোর হিসাবে বিবর্তিত হয়েছে, যদিও এটি এখনও প্রোগ্রামারের দৃষ্টিকোণ থেকে সিআইএসসি হিসাবে অনেক উপায়ে উপস্থাপন করে এবং আচরণ করে। আরো জানার জন্য, পিটার Cordes এর উত্তর দেখার এখানে
কোডি গ্রে

20

আমি মনে করি আপনি এর আগে বা পরে একটি ঘুম রাখলে এটি এতোটুকু জিনিস নয় u++। এটি বরং যে অপারেশনটি কোডটিতে u++অনুবাদ করে - স্প্যানিং থ্রেডগুলির ওভারহেডের সাথে তুলনা করে যেগুলি কল করে foo- খুব দ্রুত এমন সঞ্চালিত হয়েছিল যে এতে বাধা পাওয়ার সম্ভাবনা নেই। তবে, যদি আপনি অপারেশনটি "দীর্ঘায়িত করেন" u++তবে রেসের শর্তটি অনেক বেশি সম্ভাবনা হয়ে উঠবে:

void foo()
{
    unsigned i = u;
    for (int s=0;s<10000;s++);
    u = i+1;
}

ফলাফল: 694


বিটিডাব্লু: আমিও চেষ্টা করেছি

if (u % 2) {
    u += 2;
} else {
    u -= 1;
}

এবং এটি আমাকে বেশিরভাগ সময় দিয়েছে 1997, তবে কখনও কখনও 1995


1
আমি যে কোনও অস্পষ্ট বুদ্ধিমান সংকলকটির কাছে আশা করব যে পুরো ফাংশনটি একই জিনিসটিতে অনুকূলিত হবে। আমি অবাক হয়েছি এটি ছিল না। আকর্ষণীয় ফলাফলের জন্য আপনাকে ধন্যবাদ।
মান

এটা ঠিক সঠিক। পরবর্তী থ্রেডে প্রশ্নযুক্ত ছোট্ট ফাংশনটি সম্পাদন শুরু করার আগে অনেক হাজার নির্দেশাবলীর চালানো দরকার। আপনি যখন ফাংশনে মৃত্যুর সময়টি থ্রেড তৈরির ওভারহেডের কাছাকাছি রাখেন, আপনি রেসের অবস্থার প্রভাব দেখতে পাবেন।
জোনাথন রাইনহার্ট

@ ভ্যালিটি: আমি এটি O3 অপ্টিমাইজেশনের অধীনে জালিয়াতিপূর্ণ লুপটি মুছে ফেলারও প্রত্যাশা করেছি। তাই না?
ব্যবহারকারী 21820

কিভাবে else u -= 1কখনও মৃত্যুদন্ড কার্যকর করা যেতে পারে? এমনকি সমান্তরাল পরিবেশে মানটি কখনই মাপসই করা উচিত নয় %2, তাই না?
মাফু

2
আউটপুট থেকে, দেখে মনে else u -= 1হয় একবার একবার মৃত্যুদন্ড কার্যকর করা হয়, প্রথম বার foo () বলা হয়, যখন আপনি ইউ == 0. বাকী 999 বার ইউ বিজোড় এবং u += 2মৃত্যুদন্ড কার্যকর হয় যার ফলস্বরূপ u = -1 + 999 * 2 = 1997; অর্থাৎ সঠিক আউটপুট। একটি race পরিস্থিতি কখনও কখনও একটি সমান্তরাল থ্রেড দ্বারা ওভাররাইট হতে + = 2 এক ঘটায় এবং আপনি 1995 পেতে
লুক

7

এটি একটি রেসের শর্তে ভুগছে। রাখুন usleep(1000);সামনে u++;fooএবং আমি বিভিন্ন আউটপুট (<1000) প্রতিটি সময় দেখুন।


6
  1. কেন জাতি শর্ত, আপনার জন্য স্পষ্ট করেননি যদিও এটি সম্ভবত উত্তর নেই অস্তিত্ব, যে foo()এত দ্রুত সময় এটি একটি থ্রেড শুরু করার জন্য লাগে তুলনায় হয়, প্রতিটি থ্রেড শেষ করার আগে পরবর্তী পারেন এমনকি শুরু। কিন্তু ...

  2. এমনকি আপনার আসল সংস্করণেও ফলাফলটি সিস্টেমের দ্বারা পরিবর্তিত হয়: আমি আপনার (কোয়াড-কোর) ম্যাকবুকটিতে এটি চেষ্টা করেছি এবং দশ রানের মধ্যে আমি 1000 বার তিনবার, 999 ছয়বার এবং একবার 998 পেয়েছি। সুতরাং প্রতিযোগিতা কিছুটা বিরল, তবে স্পষ্টভাবে উপস্থিত।

  3. আপনি এর সাথে সংকলন করেছেন '-g', এতে বাগগুলি অদৃশ্য করার একটি উপায় রয়েছে। আমি আপনার কোডটি পুনরায় সংশোধন করেছি, তবুও অপরিবর্তিত তবে ছাড়াই '-g'এবং রেসটি আরও স্পষ্ট হয়ে উঠেছে: আমি একবার 1000 পেয়েছি, 999 তিনবার, 998 দুবার, 997 দুবার, 996 একবার এবং 992 একবার।

  4. করছেন। একটি ঘুম যোগ করার পরামর্শ - যা সাহায্য করে, তবে (ক) একটি স্থির ঘুমের সময় সূচনার সময়গুলি এখনও থ্রেডগুলি ফেলে দেয় (টাইমার রেজোলিউশনের সাপেক্ষে), এবং (খ) যখন আমরা যা চাই তা এলোমেলো ঘুম এগুলিকে ছড়িয়ে দেয় we তাদের একসাথে কাছাকাছি টানুন। পরিবর্তে, আমি তাদের একটি প্রারম্ভিক সংকেতের জন্য অপেক্ষা করার কোড দিয়েছিলাম, যাতে তাদের কাজ করতে দেওয়ার আগে আমি সেগুলি তৈরি করতে পারি। এই সংস্করণটির সাথে (ছাড়া বা ছাড়া '-g'), আমি সর্বত্র স্থানগুলি পেয়েছি, 974 হিসাবে কম এবং 998 এর চেয়ে বেশি নয়:

    #include <iostream>
    #include <thread>
    #include <vector>
    using namespace std;
    
    unsigned u = 0;
    bool start = false;
    
    void foo()
    {
        while (!start) {
            std::this_thread::yield();
        }
        u++;
    }
    
    int main()
    {
        vector<thread> threads;
        for(int i = 0; i < 1000; i++) {
            threads.push_back (thread (foo));
        }
        start = true;
        for (auto& t : threads) t.join();
    
        cout << u << endl;
        return 0;
    }

শুধু একটি নোট. -gপতাকা কোন ভাবেই না "করে বাগ উধাও হয়ে যায়।" -gউভয় গনুহ এবং ঝনঝন কম্পাইলার থাকা ফ্ল্যাগ কেবল কম্পাইল বাইনারিতে ডিবাগ চিহ্ন যোগ করা হয়েছে। এটি আপনাকে কিছু মানব পাঠযোগ্য আউটপুট সহ আপনার প্রোগ্রামগুলিতে জিডিবি এবং মেমচেকের মতো ডায়াগনস্টিক সরঞ্জামগুলি চালানোর অনুমতি দেয়। উদাহরণস্বরূপ, যখন মেমচেক কোনও প্রোগ্রামের উপর মেমরি ফাঁস দিয়ে চালিত হয় এটি প্রোগ্রামটি -gপতাকা ব্যবহার করে নির্মিত না হলে লাইন নম্বরটি আপনাকে জানায় না ।
এমএস-ডিডোস

মঞ্জুর, ডিবাগার থেকে লুকানো বাগগুলি সাধারণত সংকলক অপ্টিমাইজেশনের বিষয়; আমি চেষ্টা করেছি উচিত, এবং বললেন, "ব্যবহার -O2 পরিবর্তে এর -g"। তবে এটি বলেছিল, যদি আপনি কখনও বাগটি শিকারের আনন্দ না পান যা কেবল বাইরে সংকলন করার সময় প্রকাশ পায় তবে -gনিজেকে ভাগ্যবান মনে করুন। এটা তোলে করতে সূক্ষ্ম এলিয়াসিং বাগ খুব nastiest মধ্যে কয়েকটির সাথে, ঘটে। আমি আছে এটা দেখা, যদিও সম্প্রতি না, এবং আমি হয়তো বিশ্বাস করতে পারে তা একটি পুরনো মালিকানা কম্পাইলার একটি ছল তাই আমি তোমাকে বিশ্বাস করি করব, আপাতত, গনুহ এবং ঝনঝন আধুনিক সংস্করণ সম্পর্কে।
dgould

-gআপনাকে অপ্টিমাইজেশন ব্যবহার করা থেকে বিরত রাখে না। উদাহরণস্বরূপ gcc -O3 -gএকই asm তৈরি করে gcc -O3তবে ডিবাগ মেটাডেটা দিয়ে। যদিও আপনি কিছু ভেরিয়েবল মুদ্রণ করার চেষ্টা করেন তবে জিডিবি "অপ্টিমাইজড আউট" বলবে। -gস্মৃতিতে কিছু জিনিসের আপেক্ষিক অবস্থানগুলি পরিবর্তন করতে পারে, যদি এটি যুক্ত করা কোনও উপাদান অংশটির .textঅংশ থাকে। এটি অবশ্যই অবজেক্ট ফাইলে স্থান নেবে, তবে আমি মনে করি এটি লিঙ্ক করার পরে সমস্তগুলি টেক্সট বিভাগের এক প্রান্তে শেষ হয় (বিভাগ নয়), বা কোনও বিভাগের অংশ নয়। গতিশীল লাইব্রেরির জন্য জিনিসগুলি ম্যাপ করা হয়েছে এমন জায়গায় প্রভাব ফেলতে পারে।
পিটার কর্ডেস
আমাদের সাইট ব্যবহার করে, আপনি স্বীকার করেছেন যে আপনি আমাদের কুকি নীতি এবং গোপনীয়তা নীতিটি পড়েছেন এবং বুঝতে পেরেছেন ।
Licensed under cc by-sa 3.0 with attribution required.