ในขณะที่ฉันทำงานผ่านวิดีโอสอนการดาวน์โหลดออนไลน์สำหรับการพัฒนากราฟิก 3D และ Game Engine ที่ทำงานร่วมกับ OpenGL ที่ทันสมัย เราใช้volatile
ในชั้นเรียนของเรา สามารถดูเว็บไซต์บทแนะนำได้ที่นี่และวิดีโอที่ใช้volatile
คีย์เวิร์ดพบได้ในShader Engine
วิดีโอชุด 98 งานเหล่านี้ไม่ใช่ของฉันเอง แต่ได้รับการรับรองMarek A. Krzeminski, MASc
และนี่เป็นข้อความที่ตัดตอนมาจากหน้าดาวน์โหลดวิดีโอ
และหากคุณสมัครรับข้อมูลเว็บไซต์ของเขาและสามารถเข้าถึงวิดีโอของเขาในวิดีโอนี้ได้เขาอ้างอิงบทความนี้เกี่ยวกับการใช้งานVolatile
กับmultithreading
การเขียนโปรแกรม
ระเหยได้: เพื่อนที่ดีที่สุดของโปรแกรมเมอร์มัลติเธรด
โดย Andrei Alexandrescu 1 กุมภาพันธ์ 2544
คีย์เวิร์ดระเหยถูกออกแบบมาเพื่อป้องกันการปรับแต่งคอมไพลเลอร์ที่อาจทำให้โค้ดไม่ถูกต้องเมื่อมีเหตุการณ์อะซิงโครนัสบางอย่าง
ฉันไม่อยากทำให้คุณเสียอารมณ์ แต่คอลัมน์นี้กล่าวถึงหัวข้อที่น่ากลัวของการเขียนโปรแกรมแบบมัลติเธรด ถ้า - ตามที่ Generic ภาคก่อนกล่าว - การเขียนโปรแกรมที่ปลอดภัยเป็นพิเศษนั้นยากการเล่นของเด็ก ๆ เมื่อเทียบกับการเขียนโปรแกรมแบบมัลติเธรด
โปรแกรมที่ใช้เธรดหลายเธรดเป็นเรื่องยากที่จะเขียนพิสูจน์ว่าถูกต้องดีบักบำรุงรักษาและทำให้เชื่องโดยทั่วไป โปรแกรมมัลติเธรดที่ไม่ถูกต้องอาจทำงานเป็นเวลาหลายปีโดยไม่มีความผิดพลาดเพียงเพื่อเรียกใช้ amok โดยไม่คาดคิดเนื่องจากเป็นไปตามเงื่อนไขเวลาที่สำคัญบางประการ
ไม่จำเป็นต้องพูดโปรแกรมเมอร์ที่เขียนโค้ดแบบมัลติเธรดต้องการความช่วยเหลือทั้งหมดที่เธอจะได้รับ คอลัมน์นี้มุ่งเน้นไปที่สภาพการแข่งขันซึ่งเป็นแหล่งที่มาของปัญหาทั่วไปในโปรแกรมมัลติเธรดและให้ข้อมูลเชิงลึกและเครื่องมือเกี่ยวกับวิธีหลีกเลี่ยงและที่น่าอัศจรรย์พอให้คอมไพเลอร์ทำงานอย่างหนักในการช่วยเหลือคุณ
คำหลักเพียงเล็กน้อย
แม้ว่าทั้งมาตรฐาน C และ C ++ จะเงียบอย่างเห็นได้ชัดเมื่อพูดถึงเธรด แต่พวกเขาก็ให้สัมปทานกับมัลติเธรดเพียงเล็กน้อยในรูปแบบของคีย์เวิร์ดที่ผันผวน
เช่นเดียวกับ const ที่เป็นที่รู้จักกันดีระเหยเป็นตัวปรับประเภท โดยมีวัตถุประสงค์เพื่อใช้ร่วมกับตัวแปรที่เข้าถึงและแก้ไขในเธรดต่างๆ โดยทั่วไปหากไม่มีความผันผวนการเขียนโปรแกรมแบบมัลติเธรดจะเป็นไปไม่ได้หรือคอมไพเลอร์เสียโอกาสในการปรับให้เหมาะสมอย่างมาก คำอธิบายเป็นไปตามลำดับ
พิจารณารหัสต่อไปนี้:
class Gadget {
public:
void Wait() {
while (!flag_) {
Sleep(1000);
}
}
void Wakeup() {
flag_ = true;
}
...
private:
bool flag_;
};
จุดประสงค์ของ Gadget :: รอด้านบนคือการตรวจสอบตัวแปร flag_ member ทุกวินาทีและส่งคืนเมื่อตัวแปรนั้นถูกตั้งค่าเป็น true โดยเธรดอื่น อย่างน้อยนั่นคือสิ่งที่โปรแกรมเมอร์ตั้งใจไว้ แต่อนิจจาการรอไม่ถูกต้อง
สมมติว่าคอมไพลเลอร์ระบุว่า Sleep (1000) เป็นการเรียกเข้าสู่ไลบรารีภายนอกที่ไม่สามารถแก้ไขตัวแปรสมาชิก flag_ ได้ จากนั้นคอมไพลเลอร์สรุปว่าสามารถแคช flag_ ในรีจิสเตอร์และใช้รีจิสเตอร์นั้นแทนการเข้าถึงหน่วยความจำออนบอร์ดที่ช้าลง นี่เป็นการเพิ่มประสิทธิภาพที่ยอดเยี่ยมสำหรับโค้ดเธรดเดียว แต่ในกรณีนี้จะส่งผลเสียต่อความถูกต้อง: หลังจากที่คุณเรียกรอวัตถุ Gadget บางตัวแม้ว่าเธรดอื่นจะเรียก Wakeup แต่ Wait จะวนซ้ำตลอดไป เนื่องจากการเปลี่ยนแปลงของ flag_ จะไม่ปรากฏในรีจิสเตอร์ที่แคช flag_ การมองโลกในแง่ดีเกินไป ...
การแคชตัวแปรในรีจิสเตอร์เป็นการเพิ่มประสิทธิภาพที่มีค่ามากซึ่งใช้เวลาส่วนใหญ่ดังนั้นจึงน่าเสียดายที่จะเสียมันไป C และ C ++ ให้โอกาสคุณในการปิดใช้งานการแคชดังกล่าวอย่างชัดเจน หากคุณใช้ตัวปรับเปลี่ยนการระเหยกับตัวแปรคอมไพลเลอร์จะไม่แคชตัวแปรนั้นในรีจิสเตอร์ - การเข้าถึงแต่ละครั้งจะเข้าสู่ตำแหน่งหน่วยความจำจริงของตัวแปรนั้น ดังนั้นสิ่งที่คุณต้องทำเพื่อให้คำสั่งผสม Wait / Wakeup ของ Gadget ทำงานได้คือการมีคุณสมบัติ flag_ อย่างเหมาะสม:
class Gadget {
public:
... as above ...
private:
volatile bool flag_;
};
คำอธิบายส่วนใหญ่เกี่ยวกับเหตุผลและการใช้งานของ volatile stop ที่นี่และแนะนำให้คุณระบุชนิดพื้นฐานที่ระเหยได้ที่คุณใช้ในหลายเธรด อย่างไรก็ตามมีอะไรอีกมากมายที่คุณสามารถทำได้ด้วยการระเหยเนื่องจากเป็นส่วนหนึ่งของระบบประเภทที่ยอดเยี่ยมของ C ++
การใช้สารระเหยกับประเภทที่ผู้ใช้กำหนด
คุณสามารถระเหย - คุณสมบัติไม่เพียง แต่ประเภทดั้งเดิมเท่านั้น แต่ยังรวมถึงประเภทที่ผู้ใช้กำหนดด้วย ในกรณีนั้น volatile ปรับเปลี่ยนประเภทในลักษณะที่คล้ายกับ const (คุณสามารถใช้ const และ volatile กับประเภทเดียวกันได้พร้อมกัน)
แตกต่างจาก const การแยกแยะแบบระเหยระหว่างประเภทดั้งเดิมและประเภทที่ผู้ใช้กำหนดเอง ประเภทดั้งเดิมยังคงสนับสนุนการดำเนินการทั้งหมด (การบวกการคูณการกำหนด ฯลฯ ) ซึ่งแตกต่างจากคลาสคลาส ตัวอย่างเช่นคุณสามารถกำหนด int ที่ไม่ลบเลือนให้กับ int ที่ระเหยได้ แต่คุณไม่สามารถกำหนดวัตถุที่ไม่ลบเลือนให้กับวัตถุที่ระเหยได้
ลองแสดงให้เห็นว่าการระเหยทำงานอย่างไรกับประเภทที่ผู้ใช้กำหนดเองในตัวอย่าง
class Gadget {
public:
void Foo() volatile;
void Bar();
...
private:
String name_;
int state_;
};
...
Gadget regularGadget;
volatile Gadget volatileGadget;
หากคุณคิดว่าวัตถุระเหยไม่เป็นประโยชน์กับวัตถุให้เตรียมรับมือกับความประหลาดใจ
volatileGadget.Foo();
regularGadget.Foo();
volatileGadget.Bar();
การแปลงจากประเภทที่ไม่ผ่านการรับรองไปเป็นแบบระเหยเป็นเรื่องเล็กน้อย อย่างไรก็ตามเช่นเดียวกับ const คุณไม่สามารถทำให้การเดินทางกลับจากความผันผวนเป็นไม่ผ่านการรับรองได้ คุณต้องใช้นักแสดง:
Gadget& ref = const_cast<Gadget&>(volatileGadget);
ref.Bar();
คลาสที่มีคุณสมบัติระเหยได้ให้การเข้าถึงเฉพาะส่วนย่อยของอินเทอร์เฟซซึ่งเป็นส่วนย่อยที่อยู่ภายใต้การควบคุมของคลาสที่ใช้งาน ผู้ใช้สามารถเข้าถึงอินเทอร์เฟซประเภทนั้นได้โดยใช้ const_cast เท่านั้น นอกจากนี้เช่นเดียวกับ constness ความผันผวนจะแพร่กระจายจากคลาสไปยังสมาชิก (ตัวอย่างเช่น volatileGadget.name_ และ volatileGadget.state_ เป็นตัวแปรที่ระเหยได้)
ส่วนที่ผันผวนส่วนที่สำคัญและเงื่อนไขการแข่งขัน
อุปกรณ์ซิงโครไนซ์ที่ง่ายที่สุดและใช้บ่อยที่สุดในโปรแกรมมัลติเธรดคือ mutex mutex เปิดโปงการได้มาและปล่อยดั้งเดิม เมื่อคุณเรียก Acquire ในบางเธรดเธรดอื่น ๆ ที่เรียก Acquire จะบล็อก ต่อมาเมื่อเธรดนั้นเรียกรีลีสเธรดหนึ่งเธรดที่ถูกบล็อกอย่างแม่นยำในการรับการโทรจะถูกปลด กล่าวอีกนัยหนึ่งสำหรับ mutex ที่กำหนดเธรดเดียวเท่านั้นที่สามารถรับเวลาประมวลผลระหว่างการโทรไปยัง Acquire และการเรียกร้องให้ Release รหัสการดำเนินการระหว่างการเรียกร้องให้ได้มาและการเรียกร้องให้ปล่อยเรียกว่าส่วนสำคัญ (คำศัพท์ของ Windows ค่อนข้างสับสนเพราะมันเรียก mutex เองว่าเป็นส่วนที่สำคัญในขณะที่ "mutex" เป็น mutex ระหว่างกระบวนการมันจะดีถ้าพวกเขาถูกเรียกว่า thread mutex และ process mutex)
Mutexes ใช้เพื่อป้องกันข้อมูลจากสภาพการแข่งขัน ตามคำนิยามเงื่อนไขการแย่งชิงจะเกิดขึ้นเมื่อผลของเธรดมากขึ้นในข้อมูลขึ้นอยู่กับวิธีการจัดกำหนดการเธรด เงื่อนไขการแข่งขันจะปรากฏขึ้นเมื่อเธรดสองเธรดขึ้นไปแข่งขันกันโดยใช้ข้อมูลเดียวกัน เนื่องจากเธรดสามารถขัดจังหวะซึ่งกันและกันในช่วงเวลาใดเวลาหนึ่งข้อมูลจึงอาจเสียหายหรือตีความผิดได้ ดังนั้นการเปลี่ยนแปลงและบางครั้งการเข้าถึงข้อมูลจะต้องได้รับการปกป้องอย่างรอบคอบด้วยส่วนที่สำคัญ ในการเขียนโปรแกรมเชิงวัตถุโดยทั่วไปหมายความว่าคุณเก็บ mutex ไว้ในคลาสเป็นตัวแปรสมาชิกและใช้เมื่อใดก็ตามที่คุณเข้าถึงสถานะของคลาสนั้น
โปรแกรมเมอร์มัลติเธรดที่มีประสบการณ์อาจหาวอ่านสองย่อหน้าข้างต้น แต่จุดประสงค์ของพวกเขาคือเพื่อให้การออกกำลังกายทางปัญญาเพราะตอนนี้เราจะเชื่อมโยงกับการเชื่อมต่อที่ผันผวน เราทำได้โดยการวาดเส้นขนานระหว่างโลกของประเภท C ++ กับโลกแห่งความหมายของเธรด
- นอกส่วนที่สำคัญเธรดใด ๆ อาจขัดจังหวะอื่น ๆ ได้ตลอดเวลา ไม่มีการควบคุมดังนั้นตัวแปรที่เข้าถึงได้จากหลายเธรดจึงมีความผันผวน สิ่งนี้สอดคล้องกับเจตนาดั้งเดิมของการระเหยนั่นคือการป้องกันไม่ให้คอมไพเลอร์แคชค่าที่ใช้โดยเธรดหลายเธรดโดยไม่เจตนา
- ภายในส่วนสำคัญที่กำหนดโดย mutex มีเพียงเธรดเดียวเท่านั้นที่มีสิทธิ์เข้าถึง ดังนั้นในส่วนที่สำคัญโค้ดการดำเนินการจึงมีความหมายแบบเธรดเดียว ตัวแปรที่ควบคุมจะไม่ระเหยอีกต่อไป - คุณสามารถลบคุณสมบัติระเหยได้
ในระยะสั้นข้อมูลที่ใช้ร่วมกันระหว่างเธรดมีความผันผวนตามแนวคิดภายนอกส่วนที่สำคัญและไม่ระเหยภายในส่วนที่สำคัญ
คุณเข้าสู่ส่วนที่สำคัญโดยล็อค mutex คุณลบคุณสมบัติระเหยออกจากประเภทโดยใช้ const_cast หากเราจัดการเพื่อรวมการดำเนินการทั้งสองนี้เข้าด้วยกันเราจะสร้างการเชื่อมต่อระหว่างระบบชนิดของ C ++ และความหมายของเธรดของแอปพลิเคชัน เราสามารถทำให้คอมไพเลอร์ตรวจสอบเงื่อนไขการแข่งขันให้เราได้
ล็อค
เราต้องการเครื่องมือที่รวบรวมการได้มาของ mutex และ const_cast มาพัฒนาเทมเพลตคลาส LockingPtr ที่คุณเริ่มต้นด้วย obj วัตถุระเหยและ mutex mtx ตลอดอายุการใช้งาน LockingPtr จะเก็บ mtx ที่ได้มา นอกจากนี้ LockingPtr ยังมีการเข้าถึง obj ที่ลบเลือน การเข้าถึงมีให้ในรูปแบบตัวชี้อัจฉริยะผ่านตัวดำเนินการ -> และตัวดำเนินการ * const_cast จะดำเนินการภายใน LockingPtr การแคสต์นั้นถูกต้องตามความหมายเนื่องจาก LockingPtr เก็บ mutex ที่ได้มาตลอดอายุการใช้งาน
ก่อนอื่นให้กำหนดโครงกระดูกของคลาส Mutex ซึ่ง LockingPtr จะทำงาน:
class Mutex {
public:
void Acquire();
void Release();
...
};
ในการใช้ LockingPtr คุณสามารถใช้ Mutex โดยใช้โครงสร้างข้อมูลดั้งเดิมและฟังก์ชันดั้งเดิมของระบบปฏิบัติการของคุณ
LockingPtr ถูกสร้างเทมเพลตด้วยชนิดของตัวแปรที่ควบคุม ตัวอย่างเช่นหากคุณต้องการควบคุมวิดเจ็ตคุณใช้ LockingPtr ที่คุณเริ่มต้นด้วยตัวแปรประเภทวิดเจ็ตระเหย
คำจำกัดความของ LockingPtr นั้นง่ายมาก LockingPtr ใช้ตัวชี้อัจฉริยะที่ไม่ซับซ้อน โดยมุ่งเน้นไปที่การรวบรวม const_cast และส่วนที่สำคัญเท่านั้น
template <typename T>
class LockingPtr {
public:
LockingPtr(volatile T& obj, Mutex& mtx)
: pObj_(const_cast<T*>(&obj)), pMtx_(&mtx) {
mtx.Lock();
}
~LockingPtr() {
pMtx_->Unlock();
}
T& operator*() {
return *pObj_;
}
T* operator->() {
return pObj_;
}
private:
T* pObj_;
Mutex* pMtx_;
LockingPtr(const LockingPtr&);
LockingPtr& operator=(const LockingPtr&);
};
แม้จะมีความเรียบง่าย แต่ LockingPtr เป็นตัวช่วยที่มีประโยชน์มากในการเขียนโค้ดมัลติเธรดที่ถูกต้อง คุณควรกำหนดออบเจ็กต์ที่ใช้ร่วมกันระหว่างเธรดเป็นแบบระเหยและห้ามใช้ const_cast ร่วมกับอ็อบเจ็กต์โดยอัตโนมัติ ลองอธิบายสิ่งนี้ด้วยตัวอย่าง
สมมติว่าคุณมีสองเธรดที่แชร์วัตถุเวกเตอร์:
class SyncBuf {
public:
void Thread1();
void Thread2();
private:
typedef vector<char> BufT;
volatile BufT buffer_;
Mutex mtx_;
};
ภายในฟังก์ชันเธรดคุณเพียงแค่ใช้ LockingPtr เพื่อควบคุมการเข้าถึงตัวแปร buffer_ member:
void SyncBuf::Thread1() {
LockingPtr<BufT> lpBuf(buffer_, mtx_);
BufT::iterator i = lpBuf->begin();
for (; i != lpBuf->end(); ++i) {
... use *i ...
}
}
โค้ดเขียนและเข้าใจง่ายมาก - เมื่อใดก็ตามที่คุณต้องการใช้ buffer_ คุณต้องสร้าง LockingPtr ที่ชี้ไปที่มัน เมื่อคุณทำเช่นนั้นคุณจะสามารถเข้าถึงอินเทอร์เฟซทั้งหมดของเวกเตอร์ได้
ส่วนที่ดีคือถ้าคุณทำผิดคอมไพเลอร์จะชี้ให้เห็น:
void SyncBuf::Thread2() {
BufT::iterator i = buffer_.begin();
for ( ; i != lpBuf->end(); ++i ) {
... use *i ...
}
}
คุณไม่สามารถเข้าถึงฟังก์ชันของ buffer_ ได้จนกว่าคุณจะใช้ const_cast หรือใช้ LockingPtr ความแตกต่างคือ LockingPtr เสนอวิธีการสั่งใช้ const_cast กับตัวแปรที่ผันผวน
LockingPtr แสดงออกได้อย่างน่าทึ่ง หากคุณต้องการเรียกใช้เพียงฟังก์ชันเดียวคุณสามารถสร้างอ็อบเจ็กต์ LockingPtr ชั่วคราวที่ไม่มีชื่อและใช้งานได้โดยตรง:
unsigned int SyncBuf::Size() {
return LockingPtr<BufT>(buffer_, mtx_)->size();
}
กลับไปที่ประเภทดั้งเดิม
เราได้เห็นว่าการระเหยได้ดีเพียงใดช่วยปกป้องวัตถุจากการเข้าถึงที่ไม่มีการควบคุมและวิธีที่ LockingPtr มอบวิธีการเขียนโค้ดที่ปลอดภัยสำหรับเธรดที่ง่ายและมีประสิทธิภาพ ตอนนี้กลับไปที่ประเภทดั้งเดิมซึ่งได้รับการปฏิบัติที่แตกต่างกันโดยระเหย
ลองพิจารณาตัวอย่างที่หลายเธรดแชร์ตัวแปรประเภท int
class Counter {
public:
...
void Increment() { ++ctr_; }
void Decrement() { —ctr_; }
private:
int ctr_;
};
หากจะเรียกการเพิ่มและการลดจากเธรดที่แตกต่างกันส่วนด้านบนจะเป็นบั๊กกี้ ขั้นแรก ctr_ ต้องมีความผันผวน ประการที่สองแม้แต่การดำเนินการที่ดูเหมือนอะตอมเช่น ++ ctr_ ก็เป็นการดำเนินการสามขั้นตอน หน่วยความจำเองไม่มีความสามารถทางคณิตศาสตร์ เมื่อเพิ่มตัวแปรโปรเซสเซอร์:
- อ่านตัวแปรนั้นในรีจิสเตอร์
- เพิ่มมูลค่าในการลงทะเบียน
- เขียนผลลัพธ์กลับไปยังหน่วยความจำ
การดำเนินการสามขั้นตอนนี้เรียกว่า RMW (Read-Modify-Write) ในระหว่างการปรับเปลี่ยนส่วนของการดำเนินการ RMW โปรเซสเซอร์ส่วนใหญ่จะปล่อยบัสหน่วยความจำเพื่อให้โปรเซสเซอร์อื่นเข้าถึงหน่วยความจำได้
หากในขณะนั้นโปรเซสเซอร์อื่นดำเนินการ RMW กับตัวแปรเดียวกันเรามีเงื่อนไขการแย่งชิง: การเขียนครั้งที่สองจะเขียนทับเอฟเฟกต์ของตัวแรก
เพื่อหลีกเลี่ยงสิ่งนั้นคุณสามารถพึ่งพา LockingPtr ได้อีกครั้ง:
class Counter {
public:
...
void Increment() { ++*LockingPtr<int>(ctr_, mtx_); }
void Decrement() { —*LockingPtr<int>(ctr_, mtx_); }
private:
volatile int ctr_;
Mutex mtx_;
};
ตอนนี้รหัสถูกต้อง แต่คุณภาพต่ำกว่าเมื่อเทียบกับรหัสของ SyncBuf ทำไม? เนื่องจากด้วย Counter คอมไพลเลอร์จะไม่เตือนคุณหากคุณเข้าถึง ctr_ โดยตรงโดยไม่ได้ตั้งใจ (โดยไม่ต้องล็อก) คอมไพเลอร์คอมไพล์ ++ ctr_ ถ้า ctr_ เป็นค่าความผันผวนแม้ว่าโค้ดที่สร้างขึ้นจะไม่ถูกต้อง คอมไพเลอร์ไม่ใช่พันธมิตรของคุณอีกต่อไปและมีเพียงความสนใจของคุณเท่านั้นที่จะช่วยให้คุณหลีกเลี่ยงสภาวะการแข่งขันได้
คุณควรทำอย่างไร เพียงห่อหุ้มข้อมูลดั้งเดิมที่คุณใช้ในโครงสร้างระดับที่สูงขึ้นและใช้การระเหยกับโครงสร้างเหล่านั้น ในทางตรงกันข้ามมันแย่กว่าที่จะใช้สารระเหยโดยตรงกับบิวท์อินทั้งๆที่ในตอนแรกนี่คือเจตนาในการใช้สารระเหย!
ฟังก์ชั่นสมาชิกที่ผันผวน
จนถึงตอนนี้เรามีคลาสที่รวบรวมสมาชิกข้อมูลที่มีความผันผวน ทีนี้ลองนึกถึงการออกแบบคลาสที่จะเป็นส่วนหนึ่งของอ็อบเจ็กต์ขนาดใหญ่และแชร์ระหว่างเธรด นี่คือที่ซึ่งฟังก์ชันสมาชิกที่ไม่เปลี่ยนแปลงสามารถช่วยได้มาก
เมื่อออกแบบชั้นเรียนของคุณคุณจะมีคุณสมบัติในการระเหยได้เฉพาะฟังก์ชันสมาชิกที่ปลอดภัยต่อเธรดเท่านั้น คุณต้องถือว่ารหัสจากภายนอกจะเรียกใช้ฟังก์ชันระเหยจากรหัสเมื่อใดก็ได้ อย่าลืม: การระเหยเท่ากับรหัสมัลติเธรดฟรีและไม่มีส่วนที่สำคัญ ไม่ลบเลือนเท่ากับสถานการณ์เธรดเดียวหรือภายในส่วนวิกฤต
ตัวอย่างเช่นคุณกำหนดคลาสวิดเจ็ตที่ใช้การดำเนินการในสองรูปแบบ - แบบที่ปลอดภัยสำหรับเธรดและแบบที่รวดเร็วและไม่มีการป้องกัน
class Widget {
public:
void Operation() volatile;
void Operation();
...
private:
Mutex mtx_;
};
สังเกตการใช้งานเกินกำลัง ตอนนี้ผู้ใช้ Widget สามารถเรียกใช้ Operation โดยใช้ไวยากรณ์ที่เหมือนกันสำหรับวัตถุที่ระเหยได้และได้รับความปลอดภัยของเธรดหรือสำหรับวัตถุทั่วไปและรับความเร็ว ผู้ใช้ต้องระมัดระวังในการกำหนดวัตถุวิดเจ็ตที่ใช้ร่วมกันว่าระเหยได้
เมื่อใช้ฟังก์ชันสมาชิกที่ระเหยได้การดำเนินการแรกมักจะล็อคสิ่งนี้ด้วย LockingPtr จากนั้นงานจะทำโดยใช้พี่น้องที่ไม่ลบเลือน:
void Widget::Operation() volatile {
LockingPtr<Widget> lpThis(*this, mtx_);
lpThis->Operation();
}
สรุป
เมื่อเขียนโปรแกรมหลายเธรดคุณสามารถใช้ volatile เพื่อประโยชน์ของคุณ คุณต้องปฏิบัติตามกฎต่อไปนี้:
- กำหนดวัตถุที่ใช้ร่วมกันทั้งหมดว่าระเหยได้
- อย่าใช้สารระเหยโดยตรงกับประเภทดั้งเดิม
- เมื่อกำหนดคลาสที่ใช้ร่วมกันให้ใช้ฟังก์ชันสมาชิกแบบระเหยเพื่อแสดงความปลอดภัยของเธรด
หากคุณทำเช่นนี้และหากคุณใช้ LockingPtr คอมโพเนนต์ทั่วไปแบบธรรมดาคุณสามารถเขียนโค้ดที่ปลอดภัยสำหรับเธรดและกังวลน้อยลงเกี่ยวกับเงื่อนไขการแข่งขันเนื่องจากคอมไพเลอร์จะกังวลสำหรับคุณและจะชี้จุดที่คุณผิด
สองโครงการที่ฉันมีส่วนเกี่ยวข้องกับการใช้ volatile และ LockingPtr เพื่อให้ได้ผลดี รหัสมีความสะอาดและเข้าใจได้ ฉันจำการหยุดชะงักได้สองสามครั้ง แต่ฉันชอบการหยุดชะงักมากกว่าเงื่อนไขการแข่งขันเพราะมันง่ายกว่ามากในการดีบั๊ก แทบไม่มีปัญหาใด ๆ ที่เกี่ยวข้องกับสภาพการแข่งขัน แต่แล้วคุณไม่เคยรู้
กิตติกรรมประกาศ
ขอบคุณมากสำหรับ James Kanze และ Sorin Jianu ที่ช่วยให้มีความคิดที่ลึกซึ้ง
Andrei Alexandrescu เป็นผู้จัดการฝ่ายพัฒนาที่ RealNetworks Inc. (www.realnetworks.com) ซึ่งตั้งอยู่ใน Seattle, WA และเป็นผู้เขียนหนังสือ Modern C ++ Design ที่ได้รับการยกย่อง เขาอาจได้รับการติดต่อที่ www.moderncppdesign.com Andrei ยังเป็นหนึ่งในผู้สอนที่โดดเด่นของ The C ++ Seminar (www.gotw.ca/cpp_seminar)
บทความนี้อาจจะล้าสมัยเล็กน้อย แต่ให้ข้อมูลเชิงลึกที่ดีเกี่ยวกับการใช้ตัวปรับความผันผวนที่ดีเยี่ยมในการใช้โปรแกรมมัลติเธรดเพื่อช่วยให้เหตุการณ์ไม่ตรงกันในขณะที่มีคอมไพเลอร์ตรวจสอบเงื่อนไขการแข่งขันสำหรับเรา สิ่งนี้อาจไม่ได้ตอบคำถามดั้งเดิมของ OPs โดยตรงเกี่ยวกับการสร้างรั้วหน่วยความจำ แต่ฉันเลือกที่จะโพสต์สิ่งนี้เพื่อเป็นคำตอบสำหรับคนอื่น ๆ เพื่อเป็นข้อมูลอ้างอิงที่ดีเยี่ยมสำหรับการใช้งานที่มีความผันผวนเมื่อทำงานกับแอปพลิเคชันมัลติเธรด