1. กำหนดไว้อย่างปลอดภัยอย่างไร
ความหมาย ในกรณีนี้นี่ไม่ใช่คำที่กำหนดยาก มันหมายถึง "คุณสามารถทำได้โดยไม่มีความเสี่ยง"
2. หากโปรแกรมสามารถดำเนินการได้อย่างปลอดภัยพร้อมกันหมายความว่าเป็น reentrant เสมอหรือไม่
เลขที่
ตัวอย่างเช่นลองมีฟังก์ชั่น C ++ ที่รับทั้งการล็อกและการเรียกกลับเป็นพารามิเตอร์:
#include <mutex>
typedef void (*callback)();
std::mutex m;
void foo(callback f)
{
m.lock();
// use the resource protected by the mutex
if (f) {
f();
}
// use the resource protected by the mutex
m.unlock();
}
ฟังก์ชั่นอื่นอาจจำเป็นต้องล็อค mutex เดียวกัน:
void bar()
{
foo(nullptr);
}
ตั้งแต่แรกเห็นทุกอย่างดูเหมือนจะโอเค… แต่เดี๋ยวก่อน:
int main()
{
foo(bar);
return 0;
}
หากการล็อก mutex ไม่เกิดขึ้นซ้ำนี่คือสิ่งที่จะเกิดขึ้นในเธรดหลัก:
main
foo
จะเรียก
foo
จะได้รับการล็อค
foo
จะเรียกซึ่งจะเรียกbar
foo
- ที่ 2
foo
จะพยายามที่จะได้รับล็อคล้มเหลวและรอให้มันถูกปล่อยออกมา
- การหยุดชะงัก
- โอ๊ะโอ ...
ตกลงฉันโกงโดยใช้สิ่งที่โทรกลับ แต่มันง่ายที่จะจินตนาการว่าโค้ดที่ซับซ้อนกว่านี้มีเอฟเฟกต์ที่คล้ายกัน
3. อะไรคือประเด็นร่วมกันระหว่างจุดทั้งหกที่กล่าวถึงว่าฉันควรจำไว้ในขณะที่ตรวจสอบรหัสของฉันสำหรับความสามารถ reentrant?
คุณสามารถดมกลิ่นปัญหาหากฟังก์ชั่นของคุณมี / ให้การเข้าถึงทรัพยากรถาวรที่แก้ไขได้หรือมี / ให้การเข้าถึงฟังก์ชั่นที่มีกลิ่นมีกลิ่น
( ตกลง 99% ของรหัสเราควรได้กลิ่นแล้ว ... ดูส่วนสุดท้ายเพื่อจัดการกับ ... )
ดังนั้นการศึกษารหัสของคุณหนึ่งในจุดเหล่านั้นควรเตือนคุณ:
- ฟังก์ชั่นมีสถานะ (เช่นการเข้าถึงตัวแปรทั่วโลกหรือแม้กระทั่งตัวแปรสมาชิกระดับ)
- ฟังก์ชั่นนี้สามารถเรียกใช้โดยหลายเธรดหรืออาจปรากฏสองครั้งในสแต็กในขณะที่กระบวนการกำลังดำเนินการ (เช่นฟังก์ชั่นสามารถเรียกตัวเองโดยตรงหรือโดยอ้อม) ฟังก์ชั่นการโทรกลับเป็นพารามิเตอร์ได้กลิ่นมาก
โปรดทราบว่า non-reentrancy เป็นไวรัส: ฟังก์ชันที่สามารถเรียกใช้ฟังก์ชันที่ไม่ใช่ reentrant ที่เป็นไปได้ไม่สามารถพิจารณา reentrant
โปรดทราบว่าวิธีการ C ++ นั้นมีกลิ่นเพราะสามารถเข้าถึงthis
ดังนั้นคุณควรศึกษารหัสเพื่อให้แน่ใจว่าพวกเขาไม่มีปฏิสัมพันธ์ที่ตลก
4.1 ฟังก์ชั่นการเรียกซ้ำทั้งหมดเป็น reentrant หรือไม่
เลขที่
ในกรณีแบบมัลติเธรดฟังก์ชันเรียกซ้ำที่เข้าถึงทรัพยากรที่ใช้ร่วมกันสามารถเรียกใช้โดยหลายเธรดในเวลาเดียวกันส่งผลให้ข้อมูลไม่ถูกต้อง / เสียหาย
ในกรณีที่มีเธรดเดียวฟังก์ชันแบบเรียกซ้ำอาจใช้ฟังก์ชันที่ไม่ใช่ reentrant (เช่นชื่อเสียstrtok
) หรือใช้ข้อมูลทั่วโลกโดยไม่ต้องจัดการกับความจริงที่ว่ามีการใช้ข้อมูลอยู่แล้ว ดังนั้นฟังก์ชั่นของคุณจะเรียกซ้ำได้เพราะมันเรียกตัวเองโดยตรงหรือโดยอ้อม แต่มันก็ยังสามารถเรียกซ้ำได้ - ไม่ปลอดภัย recursive-ที่ไม่ปลอดภัย
4.2 ฟังก์ชั่น thread-safe ทั้งหมดถูก reentrant หรือไม่
ในตัวอย่างข้างต้นฉันแสดงให้เห็นว่าฟังก์ชั่น threadsafe ไม่ได้เป็น reentrant ตกลงฉันโกงเพราะพารามิเตอร์โทรกลับ แต่จากนั้นมีหลายวิธีในการปิดกั้นเธรดโดยให้มันล็อคสองครั้งแบบไม่เรียกซ้ำ
4.3 ฟังก์ชันการเรียกซ้ำทั้งหมดและแบบปลอดภัยต่อการส่งข้อความซ้ำทั้งหมด
ฉันจะพูดว่า "ใช่" ถ้า "เรียกซ้ำ" คุณหมายถึง "ปลอดภัยแบบเรียกซ้ำ"
หากคุณสามารถรับประกันได้ว่าฟังก์ชั่นสามารถเรียกใช้พร้อมกันโดยหลายกระทู้และสามารถเรียกตัวเองไม่ว่าโดยตรงหรือโดยอ้อมโดยไม่มีปัญหาแล้วมันจะ reentrant
ปัญหากำลังประเมินการรับประกันนี้… ^ _ ^
5. เงื่อนไขเช่น reentrance และความปลอดภัยของด้ายนั้นแน่นอนเช่นพวกเขามีคำจำกัดความที่แน่นอนคอนกรีตหรือไม่
ฉันเชื่อว่าพวกเขาทำ แต่จากนั้นการประเมินฟังก์ชั่นเป็น thread-safe หรือ reentrant อาจเป็นเรื่องยาก นี่คือเหตุผลที่ฉันใช้คำว่ากลิ่นด้านบน: คุณสามารถค้นหาฟังก์ชั่นไม่ได้เป็น reentrant แต่มันอาจเป็นเรื่องยากที่จะตรวจสอบให้แน่ใจว่าโค้ดที่ซับซ้อนนั้นเป็น reentrant
6. ตัวอย่าง
สมมติว่าคุณมีวัตถุด้วยวิธีหนึ่งที่ต้องใช้ทรัพยากร:
struct MyStruct
{
P * p;
void foo()
{
if (this->p == nullptr)
{
this->p = new P();
}
// lots of code, some using this->p
if (this->p != nullptr)
{
delete this->p;
this->p = nullptr;
}
}
};
ปัญหาแรกคือถ้าฟังก์ชั่นนี้ถูกเรียกซ้ำ (เช่นฟังก์ชั่นนี้เรียกตัวเองไม่ว่าโดยตรงหรือโดยอ้อม) รหัสอาจจะผิดพลาดเพราะ this->p
จะถูกลบในตอนท้ายของการโทรครั้งสุดท้ายและยังคงใช้ก่อนสิ้น ของสายแรก
ดังนั้นรหัสนี้ไม่ปลอดภัยซ้ำ
เราสามารถใช้ตัวนับอ้างอิงเพื่อแก้ไขสิ่งนี้:
struct MyStruct
{
size_t c;
P * p;
void foo()
{
if (c == 0)
{
this->p = new P();
}
++c;
// lots of code, some using this->p
--c;
if (c == 0)
{
delete this->p;
this->p = nullptr;
}
}
};
วิธีนี้รหัสจะกลายเป็นแบบเรียกซ้ำปลอดภัย ... แต่ก็ยังไม่ได้ reentrant เนื่องจากปัญหาแบบมัลติเธรด: เราต้องแน่ใจว่าการแก้ไขc
และp
จะทำแบบอะตอมโดยใช้mutex แบบเรียกซ้ำ (ไม่ใช่ mutex ทั้งหมดที่เรียกซ้ำ):
#include <mutex>
struct MyStruct
{
std::recursive_mutex m;
size_t c;
P * p;
void foo()
{
m.lock();
if (c == 0)
{
this->p = new P();
}
++c;
m.unlock();
// lots of code, some using this->p
m.lock();
--c;
if (c == 0)
{
delete this->p;
this->p = nullptr;
}
m.unlock();
}
};
และแน่นอนทั้งหมดนี้ถือว่าlots of code
เป็นตัวเอง reentrant p
รวมถึงการใช้งานของ
และรหัสข้างต้นไม่ได้ยกเว้นความปลอดภัยจากระยะไกลแต่นี่เป็นอีกเรื่องหนึ่ง… ^ _ ^
7. เฮ้ 99% ของรหัสของเราไม่ใช่ reentrant!
มันค่อนข้างจริงสำหรับรหัสสปาเก็ตตี้ แต่ถ้าคุณแบ่งพาร์ติชันรหัสของคุณอย่างถูกต้องคุณจะหลีกเลี่ยงปัญหา reentrancy
7.1 ตรวจสอบให้แน่ใจว่าฟังก์ชั่นทั้งหมดไม่มีสถานะ
พวกเขาจะต้องใช้พารามิเตอร์ตัวแปรท้องถิ่นของตนเองฟังก์ชั่นอื่น ๆ ที่ไม่มีรัฐและส่งคืนสำเนาของข้อมูลหากพวกเขากลับมาเลย
7.2 ตรวจสอบให้แน่ใจว่าวัตถุของคุณ "ปลอดภัยแบบเรียกซ้ำ"
วิธีวัตถุมีการเข้าถึงthis
ดังนั้นจึงใช้สถานะร่วมกับวิธีการทั้งหมดของอินสแตนซ์เดียวกันของวัตถุ
ดังนั้นตรวจสอบให้แน่ใจว่าสามารถใช้วัตถุได้ที่จุดหนึ่งในสแต็ก (เช่นวิธีการเรียก A) จากนั้นไปที่อีกจุดหนึ่ง (เช่นการเรียกวิธีการ B) โดยไม่ทำให้วัตถุทั้งหมดเสียหาย ออกแบบวัตถุของคุณเพื่อให้แน่ใจว่าเมื่อออกจากวิธีการวัตถุนั้นมีเสถียรภาพและถูกต้อง (ไม่มีตัวชี้ห้อยไม่มีตัวแปรที่ขัดแย้งกับสมาชิก ฯลฯ )
7.3 ตรวจสอบให้แน่ใจว่าวัตถุทั้งหมดของคุณถูกห่อหุ้มอย่างถูกต้อง
ไม่มีใครควรเข้าถึงข้อมูลภายในได้:
// bad
int & MyObject::getCounter()
{
return this->counter;
}
// good
int MyObject::getCounter()
{
return this->counter;
}
// good, too
void MyObject::getCounter(int & p_counter)
{
p_counter = this->counter;
}
แม้การส่งคืนการอ้างอิง const อาจเป็นอันตรายหากผู้ใช้ดึงข้อมูลที่อยู่ของข้อมูลเนื่องจากบางส่วนของรหัสสามารถแก้ไขได้โดยไม่ต้องมีรหัสที่อ้างอิงการอ้างอิง const
7.4 ตรวจสอบให้แน่ใจว่าผู้ใช้รู้ว่าวัตถุของคุณไม่ปลอดภัยสำหรับเธรด
ดังนั้นผู้ใช้มีหน้าที่รับผิดชอบในการใช้ mutexes เพื่อใช้วัตถุที่ใช้ร่วมกันระหว่างกระทู้
วัตถุจาก STL ได้รับการออกแบบให้ไม่ปลอดภัยต่อเธรด (เนื่องจากปัญหาด้านประสิทธิภาพ) และดังนั้นหากผู้ใช้ต้องการแชร์ std::string
ระหว่างสองเธรดผู้ใช้จะต้องปกป้องการเข้าถึงด้วยการทำงานพร้อมกัน
7.5 ตรวจสอบให้แน่ใจว่ารหัส thread-safe ของคุณปลอดภัยซ้ำ
ซึ่งหมายความว่าการใช้ mutexes แบบเรียกซ้ำหากคุณเชื่อว่าทรัพยากรเดียวกันสามารถใช้สองครั้งโดยเธรดเดียวกัน