আমি পোর্টেবল কোড লিখতে চাই (ইন্টেল, এআরএম, পাওয়ারপিসি ...) যা ক্লাসিক সমস্যার বৈকল্পিক সমাধান করে:
Initially: X=Y=0
Thread A:
X=1
if(!Y){ do something }
Thread B:
Y=1
if(!X){ do something }
যার লক্ষ্য হ'ল উভয় থ্রেডের এমন পরিস্থিতি এড়ানোsomething
। (যদি কোনও জিনিস না চালায় তবে এটি ঠিক আছে; এটি একবারে চালানো ঠিক নয়) এই নীচে আমার যুক্তিতে কিছু ত্রুটি দেখলে দয়া করে আমাকে সংশোধন করুন।
আমি সচেতন, যে আমি memory_order_seq_cst
পরমাণু store
গুলি এবং load
গুলি দ্বারা নিম্নলিখিত হিসাবে লক্ষ্য অর্জন করতে পারেন :
std::atomic<int> x{0},y{0};
void thread_a(){
x.store(1);
if(!y.load()) foo();
}
void thread_b(){
y.store(1);
if(!x.load()) bar();
}
যা লক্ষ্য অর্জন করে, কারণ
{x.store(1), y.store(1), y.load(), x.load()}
ইভেন্টগুলিতে কিছু একক মোট অর্ডার থাকতে হবে, যা অবশ্যই প্রোগ্রামের আদেশ "প্রান্ত" এর সাথে একমত হতে হবে:
x.store(1)
"TO তে আগে"y.load()
y.store(1)
"TO তে আগে"x.load()
এবং যদি foo()
বলা হয়, তবে আমাদের অতিরিক্ত প্রান্ত রয়েছে:
y.load()
"আগে মান পড়ে"y.store(1)
এবং যদি bar()
বলা হয়, তবে আমাদের অতিরিক্ত প্রান্ত রয়েছে:
x.load()
"আগে মান পড়ে"x.store(1)
এবং এই সমস্ত প্রান্তগুলি একত্রিত হয়ে একটি চক্র গঠন করবে:
x.store(1)
"TO এর আগে" y.load()
" " এর আগে " " মান পড়ার আগে " y.store(1)
" " " TO "এর আগে" x.load()
"আগে মান পড়ার আগে" "হয়x.store(true)
যা আদেশের কোনও চক্র নেই তা লঙ্ঘন করে।
আমি ইচ্ছাকৃতভাবে স্ট্যান্ডার্ড শর্তগুলির বিপরীতে "TO এর আগে" এবং "পূর্বে মূল্য পড়ি" মানহীন শর্তাদি ব্যবহার করি happens-before
, কারণ এই ধারগুলি সত্যই happens-before
সম্পর্ককে বোঝায় যে আমার ধারনার যথার্থতা সম্পর্কে আমি মতামত জানাতে চাই , এককভাবে একত্রিত হতে পারে গ্রাফ এবং এই জাতীয় সংযুক্ত গ্রাফের চক্র নিষিদ্ধ। আমি এটি সম্পর্কে নিশ্চিত নই। আমি যা জানি তা এই কোডটি ইন্টেল জিসিসি এবং কলং এবং এআরএম জিসিসিতে সঠিক বাধা তৈরি করে
এখন, আমার আসল সমস্যাটি আরও জটিল, কারণ "এক্স" এর উপর আমার কোনও নিয়ন্ত্রণ নেই - এটি কিছু ম্যাক্রো, টেম্পলেট ইত্যাদির আড়ালে লুকিয়ে রয়েছে এবং এটি সম্ভবত দুর্বল হতে পারে might seq_cst
আমি জানি না "এক্স" একটি একক পরিবর্তনশীল, বা অন্য কোনও ধারণা (যেমন একটি হালকা ওজনের সেমোফোর বা মিটেক্স)। কেবলমাত্র আমি জানি যে আমার কাছে দুটি ম্যাক্রো রয়েছে set()
এবং check()
এমন একটি আর থ্রেড "" পরে " check()
ফিরে আসে । (এটা হয় যে পরিচিত এবং থ্রেড-নিরাপদ ও ডেটা-জাতি UB তৈরি করতে পারবেন না।)true
set()
set
check
সুতরাং ধারণাগতভাবে set()
কিছুটা "এক্স = 1" এর check()
মতো এবং এটি "এক্স" এর মতো, তবে আমার কোনও জড়িত পরমাণুর সাথে সরাসরি কোনও অ্যাক্সেস নেই।
void thread_a(){
set();
if(!y.load()) foo();
}
void thread_b(){
y.store(1);
if(!check()) bar();
}
আমি উদ্বিগ্ন, এটি set()
অভ্যন্তরীণভাবে x.store(1,std::memory_order_release)
এবং / অথবা check()
হতে পারে অভ্যন্তরীণভাবে প্রয়োগ করা হতে পারে x.load(std::memory_order_acquire)
। বা অনুমানকভাবে একটি std::mutex
যে থ্রেড আনলক করা হচ্ছে এবং অন্যটি try_lock
আইএন; আইএসও স্ট্যান্ডার্ডে std::mutex
কেবল অর্ডার অর্ডার এবং রিলিজ করার গ্যারান্টি রয়েছে, seq_cst নয়।
এই যদি হয় তাহলে, তারপর check()
's যদি শরীর করতে পারার আগে "পুনর্বিন্যস্তভাবে" করা y.store(true)
( দেখুন অ্যালেক্স এর উত্তর তারা কোথায় প্রকট যে এই পাওয়ারপিসি ঘটে )।
এটি সত্যিই খারাপ হবে, কারণ এখন ইভেন্টগুলির এই ক্রমটি সম্ভব:
thread_b()
প্রথমেx
(0
) এর পুরানো মান লোড করেthread_a()
সহ সবকিছু কার্যকর করেfoo()
thread_b()
সহ সবকিছু কার্যকর করেbar()
সুতরাং, উভয় foo()
এবং কল bar()
পেয়েছিলাম, যা আমি এড়ানো ছিল। এটি রোধ করার জন্য আমার বিকল্পগুলি কী কী?
বিকল্প ক
স্টোর-লোড বাধা জোর করার চেষ্টা করুন। এটি, বাস্তবে, এটি অর্জন করা যায় std::atomic_thread_fence(std::memory_order_seq_cst);
- অ্যালেক্সের ব্যাখ্যা অনুযায়ী আলাদাভাবে উত্তর দেওয়া সমস্ত পরীক্ষিত সংকলক একটি সম্পূর্ণ বেড়া নির্গত করে:
- x86_64: MFENCE CE
- পাওয়ারপিসি: hwsync
- ইটানুইম: এমএফ
- এআরএমভি 7 / এআরএমভি 8: ডিএমবি ইএস
- মিপস 64: সিঙ্ক
এই পদ্ধতির সাথে সমস্যাটি হ'ল, আমি সি ++ নিয়মে কোনও গ্যারান্টি খুঁজে পাইনি, এটি std::atomic_thread_fence(std::memory_order_seq_cst)
অবশ্যই সম্পূর্ণ মেমরির বাধাতে অনুবাদ করবে। প্রকৃতপক্ষে, atomic_thread_fence
সি ++ এর এস ধারণাটি মেমরি বাধাগুলির সমাবেশ ধারণার চেয়ে বিমূর্ততার ভিন্ন স্তরের বলে মনে হয় এবং "কী কী পারমাণবিক ক্রিয়াকলাপটি কীটির সাথে সুসংগত করে" এর মতো স্টাফের সাথে আরও বেশি ডিল করে। নীচে বাস্তবায়ন লক্ষ্য অর্জন করে এমন কোনও তাত্ত্বিক প্রমাণ রয়েছে কি?
void thread_a(){
set();
std::atomic_thread_fence(std::memory_order_seq_cst)
if(!y.load()) foo();
}
void thread_b(){
y.store(true);
std::atomic_thread_fence(std::memory_order_seq_cst)
if(!check()) bar();
}
বিকল্প বি
ওয়াইডে রিড-মডিফাই-রাইট মেমরি_অর্ডার_এককি_রেল অপারেশন ব্যবহার করে সিঙ্ক্রোনাইজেশন অর্জন করতে আমাদের ওয়াইয়ের ওপরে নিয়ন্ত্রণ ব্যবহার করুন:
void thread_a(){
set();
if(!y.fetch_add(0,std::memory_order_acq_rel)) foo();
}
void thread_b(){
y.exchange(1,std::memory_order_acq_rel);
if(!check()) bar();
}
এখানে ধারণাটি হ'ল একক পারমাণবিক ( y
) এ অ্যাক্সেসগুলি অবশ্যই একটি একক আদেশ হতে হবে যার উপর সমস্ত পর্যবেক্ষক সম্মত হন, তাই হয় fetch_add
হয় আগে exchange
বা বিপরীত।
যদি fetch_add
এর আগে হয় exchange
তবে "রিলিজ" অংশটি fetch_add
"অর্জন" অংশটির সাথে সিঙ্ক্রোনাইজ করে exchange
এবং এইভাবে এর সমস্ত পার্শ্ব প্রতিক্রিয়াগুলি set()
কোড এক্সিকিউটিভের কাছে দৃশ্যমান হতে হয় check()
, তাই bar()
বলা হবে না।
তা না হলে, exchange
আগে fetch_add
, তারপর fetch_add
দেখতে হবে 1
এবং কল না foo()
। সুতরাং, উভয়কেই কল করা অসম্ভব foo()
এবং bar()
। এই যুক্তি কি সঠিক?
বিকল্প সি
"প্রান্ত" প্রবর্তন করতে ডামি অ্যাটমিক্স ব্যবহার করুন যা দুর্যোগ রোধ করে। নিম্নলিখিত পদ্ধতির বিবেচনা করুন:
void thread_a(){
std::atomic<int> dummy1{};
set();
dummy1.store(13);
if(!y.load()) foo();
}
void thread_b(){
std::atomic<int> dummy2{};
y.store(1);
dummy2.load();
if(!check()) bar();
}
আপনি যদি মনে করেন যে সমস্যাটি এখানে atomic
স্থানীয়, তবে তাদেরকে বিশ্বব্যাপী সরিয়ে নিয়ে যাওয়ার কল্পনা করুন, নিম্নলিখিত যুক্তিতে এটি আমার কাছে তাত্পর্যপূর্ণ বলে মনে হয় না এবং আমি ইচ্ছাকৃতভাবে কোডটি এমনভাবে লিখেছি যাতে এটি কতটা মজাদার exp এবং ডামি 2 সম্পূর্ণ পৃথক।
কেন পৃথিবীতে এটি কাজ করতে পারে? ওয়েল, অবশ্যই কিছু একক সামগ্রিক অর্ডার থাকতে হবে {dummy1.store(13), y.load(), y.store(1), dummy2.load()}
যার ক্রমটি প্রোগ্রামের আদেশের সাথে "প্রান্ত" এর সাথে সামঞ্জস্যপূর্ণ থাকতে হবে:
dummy1.store(13)
"TO তে আগে"y.load()
y.store(1)
"TO তে আগে"dummy2.load()
(একটি seq_cst স্টোর + লোড আশা করি স্টোরলড সহ একটি সম্পূর্ণ মেমরি বাধা সমতুল্য সি ++ সমৃদ্ধ, যেমন তারা এমনকি আআআর্ক including৪ সহ সত্যিকারের আইএসএতে asm করে তবে যেখানে কোনও পৃথক বাধা নির্দেশের প্রয়োজন নেই))
এখন, আমাদের দুটি বিষয় বিবেচনা করতে হবে: হয় y.store(1)
হয় y.load()
মোট ক্রমের আগে বা পরে।
যদি y.store(1)
এর আগে হয় y.load()
তবে foo()
ডাকা হবে না এবং আমরা নিরাপদ।
যদি y.load()
আগে হয় y.store(1)
, তবে এটি ইতিমধ্যে প্রোগ্রাম ক্রমে আমাদের দুটি প্রান্তের সাথে একত্রিত করে, আমরা এটি হ্রাস করি:
dummy1.store(13)
"TO তে আগে"dummy2.load()
এখন, এটি dummy1.store(13)
একটি রিলিজ অপারেশন, যা এর প্রভাবগুলি প্রকাশ করে set()
এবং dummy2.load()
এটি একটি অধিগ্রহণ অপারেশন, সুতরাং check()
এর প্রভাবগুলি দেখতে হবে set()
এবং এভাবে bar()
ডাকা হবে না এবং আমরা নিরাপদ are
এটি কি এখানে ভাবতে সঠিক check()
হবে যে ফলাফলগুলি দেখতে পাবে set()
? আমি কি বিভিন্ন ধরণের "প্রান্তগুলি" সংযুক্ত করতে পারি ("প্রোগ্রাম অর্ডার" ওরফে সিকোয়েন্সড এর আগে, "সম্পূর্ণ আদেশ", "মুক্তির আগে", "অর্জনের পরে") এর মতো? আমার এ সম্পর্কে গুরুতর সন্দেহ রয়েছে: সি ++ বিধিগুলি একই স্থানে স্টোর এবং লোডের মধ্যে সম্পর্কের "সিনক্রোনাইজ-সাথে" সম্পর্কে কথা বলে মনে হয় - এখানে এমন কোনও পরিস্থিতি নেই।
মনে রাখবেন আমরা কেবল কেস যেখানে সম্পর্কে উদ্বিগ্ন হন dumm1.store
হয় পরিচিত (অন্যান্য যুক্তি মাধ্যমে) আগে হতে হবে dummy2.load
seq_cst মোট যাতে। সুতরাং যদি তারা একই ভেরিয়েবলটি অ্যাক্সেস করে থাকত তবে লোডটি সঞ্চিত মানটি দেখতে পেত এবং এর সাথে সিঙ্ক্রোনাইজ করত।
(বাস্তবায়নের জন্য মেমোরি-বাধা / পুনর্নির্ধারণের যুক্তি যেখানে পারমাণবিক লোড এবং স্টোরগুলি কমপক্ষে 1-উপায় মেমরি বাধা (এবং seq_cst ক্রিয়াকলাপগুলি পুনরায় অর্ডার করতে পারে না: যেমন একটি seq_cst স্টোর একটি seq_cst লোড পাস করতে পারে না) যে কোনও লোড / দোকানে পর dummy2.load
স্পষ্টভাবে অন্যান্য থ্রেড কাছে দৃশ্যমান হয়ে পরে y.store
। আর একইভাবে অন্যান্য থ্রেড জন্য, ... সামনে y.load
।)
আপনি https://godbolt.org/z/u3dTa8 এ আমার বিকল্প এ, বি, সি প্রয়োগের সাথে খেলতে পারেন
foo()
এবং bar()
উভয়কেই ডাকা হচ্ছে।
compare_exchange_*
কোনও পারমাণবিক স্তরে তার মান পরিবর্তন না করে কোনও আরএমডাব্লু অপারেশন করতে পারেন (কেবলমাত্র প্রত্যাশিত এবং একই মানটিতে নতুন সেট করুন)।
atomic<bool>
রয়েছে exchange
এবং আছে compare_exchange_weak
। পরবর্তীটি সিএএস (সত্য, সত্য) বা মিথ্যা, মিথ্যা দ্বারা একটি ডামি আরএমডাব্লু করতে ব্যবহার করা যেতে পারে। এটি হয় ব্যর্থ হয় বা পরমাণুর সাথে মানটি নিজের সাথে প্রতিস্থাপন করে। (X86-64 asm এ, সেই কৌশলটি lock cmpxchg16b
আপনি কীভাবে গ্যারান্টেড-পারমাণবিক 16-বাইট লোড করবেন; অকার্যকর তবে আলাদা লক নেওয়ার চেয়ে কম খারাপ bad)
foo()
এবং bar()
নাও ডাকা হবে না। আমি কোডের অনেকগুলি "রিয়েল ওয়ার্ল্ড" উপাদানগুলিতে আনতে চাইনি, "আপনার মনে হয় আপনার এক্স সমস্যা আছে তবে আপনার ওয়াই" সমস্যা রয়েছে বলে মনে করেন এ ধরণের প্রতিক্রিয়া। কিন্তু, যদি এক সত্যিই জানতে পটভূমি তলা কি দরকার: set()
সত্যিই some_mutex_exit()
, check()
হয় try_enter_some_mutex()
, y
হয় "কিছু ওয়েটার হয়", foo()
"প্রস্থান কেউ নিদ্রাভঙ্গ আপ ছাড়া" হয়, bar()
"wakup জন্য অপেক্ষা" হয় ... কিন্তু, আমি অস্বীকার এই নকশাটি এখানে আলোচনা করুন - আমি এটি সত্যিই পরিবর্তন করতে পারি না।
std::atomic_thread_fence(std::memory_order_seq_cst)
একটি সম্পূর্ণ বাধা সংকলন করে তবে পুরো ধারণাটি বাস্তবায়নের বিশদ হিসাবে আপনি পাবেন না স্ট্যান্ডার্ডে এটির কোনও উল্লেখ। (CPU- র মেমরি মডেল সাধারণত হয় কি reorerings অনুক্রমিক দৃঢ়তা আপেক্ষিক অনুমতি দেওয়া হয় পদ সংজ্ঞায়িত যেমন এক্স 86 হল SeQ-CST + একটি দোকান বাফার W / ফরওয়ার্ডিং।)