ทุกคนสามารถให้คำอธิบายที่ดีเกี่ยวกับคำสำคัญในภาษา C # ได้หรือไม่? ปัญหาอะไรที่แก้ได้และมันไม่ได้? ในกรณีใดบ้างที่ฉันจะต้องใช้การล็อก?
ทุกคนสามารถให้คำอธิบายที่ดีเกี่ยวกับคำสำคัญในภาษา C # ได้หรือไม่? ปัญหาอะไรที่แก้ได้และมันไม่ได้? ในกรณีใดบ้างที่ฉันจะต้องใช้การล็อก?
คำตอบ:
ฉันไม่คิดว่าจะมีคนตอบคำถามที่ดีไปกว่าEric Lippert (เน้นในต้นฉบับ):
ใน C # "volatile" หมายถึงไม่เพียง แต่ "ตรวจสอบให้แน่ใจว่าคอมไพเลอร์และ jitter ไม่ได้ทำการจัดเรียงรหัสใหม่หรือลงทะเบียนการเพิ่มประสิทธิภาพการแคชกับตัวแปรนี้" นอกจากนี้ยังหมายถึง "บอกโปรเซสเซอร์ให้ทำสิ่งที่จำเป็นต้องทำเพื่อให้แน่ใจว่าฉันกำลังอ่านค่าล่าสุดแม้ว่าจะหมายถึงการหยุดโปรเซสเซอร์อื่นและทำให้หน่วยความจำหลักตรงกันกับแคช"
ที่จริงแล้วบิตสุดท้ายคือเรื่องโกหก ความหมายที่แท้จริงของการอ่านและเขียนระเหยง่ายมีความซับซ้อนมากกว่าที่ฉันเขียนไว้ที่นี่ ในความเป็นจริงพวกเขาไม่ได้รับประกันว่าโปรเซสเซอร์ทุกตัวจะหยุดสิ่งที่ทำอยู่และอัพเดตแคชไปยัง / จากหน่วยความจำหลัก แต่พวกเขาให้การค้ำประกันอ่อนแอเกี่ยวกับวิธีการเข้าถึงหน่วยความจำก่อนและหลังการอ่านและการเขียนอาจจะสังเกตเห็นที่จะสั่งซื้อด้วยความเคารพซึ่งกันและกัน การดำเนินการบางอย่างเช่นการสร้างเธรดใหม่การเข้าล็อคหรือการใช้หนึ่งในตระกูลของวิธีการแบบอินเตอร์ล็อกที่มีการรับรองที่ดีกว่าเกี่ยวกับการสังเกตการสั่งซื้อ หากคุณต้องการรายละเอียดเพิ่มเติมอ่านหัวข้อ 3.10 และ 10.5.3 ของข้อกำหนด C # 4.0
ตรงไปตรงมาผมกีดกันคุณจากที่เคยทำสนามระเหย เขตข้อมูลที่ระเหยได้เป็นสัญญาณว่าคุณกำลังทำสิ่งที่บ้าคลั่งอย่างจริงจัง: คุณกำลังพยายามอ่านและเขียนค่าเดียวกันในสองเธรดที่แตกต่างกันโดยไม่ต้องใส่กุญแจ ล็อครับประกันว่าหน่วยความจำที่อ่านหรือแก้ไขภายในล็อคถูกตรวจสอบว่าสอดคล้องกันล็อครับประกันว่ามีเพียงเธรดเดียวเท่านั้นที่เข้าถึงหน่วยความจำที่กำหนดในแต่ละครั้งและต่อ ๆ ไป จำนวนสถานการณ์ที่ล็อคช้าเกินไปมีขนาดเล็กมากและความน่าจะเป็นที่คุณจะได้รหัสผิดเพราะคุณไม่เข้าใจรูปแบบหน่วยความจำที่แน่นอนมีขนาดใหญ่มาก ฉันไม่ได้พยายามเขียนรหัส low-lock ใด ๆ ยกเว้นการใช้งานอินเตอร์ล็อคที่น่าสนใจที่สุด ฉันปล่อยให้การใช้งานของ "ระเหย" เพื่อผู้เชี่ยวชาญจริง
สำหรับการอ่านเพิ่มเติมดู:
volatile
จะมีคุณธรรมล็อค
หากคุณต้องการได้รับเทคนิคเพิ่มเติมเล็กน้อยเกี่ยวกับสิ่งที่คำสำคัญระเหยพิจารณาโปรแกรมต่อไปนี้ (ฉันใช้ DevStudio 2005):
#include <iostream>
void main()
{
int j = 0;
for (int i = 0 ; i < 100 ; ++i)
{
j += i;
}
for (volatile int i = 0 ; i < 100 ; ++i)
{
j += i;
}
std::cout << j;
}
การใช้การตั้งค่าคอมไพเลอร์มาตรฐานที่ดีที่สุด (ปล่อย) คอมไพเลอร์สร้างแอสเซมเบลอร์ต่อไปนี้ (IA32):
void main()
{
00401000 push ecx
int j = 0;
00401001 xor ecx,ecx
for (int i = 0 ; i < 100 ; ++i)
00401003 xor eax,eax
00401005 mov edx,1
0040100A lea ebx,[ebx]
{
j += i;
00401010 add ecx,eax
00401012 add eax,edx
00401014 cmp eax,64h
00401017 jl main+10h (401010h)
}
for (volatile int i = 0 ; i < 100 ; ++i)
00401019 mov dword ptr [esp],0
00401020 mov eax,dword ptr [esp]
00401023 cmp eax,64h
00401026 jge main+3Eh (40103Eh)
00401028 jmp main+30h (401030h)
0040102A lea ebx,[ebx]
{
j += i;
00401030 add ecx,dword ptr [esp]
00401033 add dword ptr [esp],edx
00401036 mov eax,dword ptr [esp]
00401039 cmp eax,64h
0040103C jl main+30h (401030h)
}
std::cout << j;
0040103E push ecx
0040103F mov ecx,dword ptr [__imp_std::cout (40203Ch)]
00401045 call dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (402038h)]
}
0040104B xor eax,eax
0040104D pop ecx
0040104E ret
เมื่อดูที่เอาต์พุตคอมไพเลอร์ได้ตัดสินใจใช้การลงทะเบียน ecx เพื่อเก็บค่าของตัวแปร j สำหรับการวนซ้ำแบบไม่ลบเลือน (ตอนแรก) คอมไพเลอร์ได้กำหนด i ให้กับ eax register ค่อนข้างตรงไปตรงมา มีบิตที่น่าสนใจอยู่สองสามอย่าง - คำสั่ง lea ebx, [ebx] เป็นคำสั่งมัลติไบต์ที่มีประสิทธิภาพเพื่อให้วงกระโดดไปที่แอดเดรสหน่วยความจำที่อยู่ในแนว 16 ไบต์ อีกข้อหนึ่งคือการใช้ edx เพื่อเพิ่มตัวนับลูปแทนการใช้คำสั่ง inc eax คำสั่งเพิ่ม reg, reg มีเวลาแฝงที่ต่ำกว่าใน IA32 คอร์บางตัวเมื่อเทียบกับคำสั่ง inc reg แต่ไม่เคยมีเวลาแฝงที่สูงกว่า
ทีนี้สำหรับลูปที่มีตัวนับลูประเหย ตัวนับถูกเก็บไว้ที่ [esp] และคำสำคัญระเหยบอกคอมไพเลอร์ค่าควรอ่านจาก / เขียนไปยังหน่วยความจำและไม่เคยกำหนดให้ลงทะเบียน คอมไพเลอร์ยังไปไกลถึงการไม่ทำโหลด / เพิ่ม / จัดเก็บเป็นสามขั้นตอนที่แตกต่างกัน (โหลด eax, inc eax, บันทึก eax) เมื่ออัปเดตค่าตัวนับแทนหน่วยความจำจะถูกปรับเปลี่ยนโดยตรงในคำสั่งเดียว เร็ก) วิธีการสร้างโค้ดทำให้มั่นใจได้ว่าค่าของตัวนับลูปนั้นทันสมัยอยู่เสมอในบริบทของซีพียูแกนเดียว ไม่มีการดำเนินการกับข้อมูลที่อาจส่งผลให้เกิดความเสียหายหรือการสูญเสียข้อมูล (ดังนั้นไม่ได้ใช้โหลด / inc / ร้านค้าเนื่องจากค่าสามารถเปลี่ยนแปลงได้ในระหว่างการ inc จึงจะหายไปในร้าน) เนื่องจากอินเตอร์รัปต์สามารถให้บริการได้เมื่อคำสั่งปัจจุบันเสร็จสิ้นแล้วเท่านั้น
เมื่อคุณแนะนำซีพียูตัวที่สองให้กับระบบคำสำคัญที่ไม่ลบเลือนจะไม่ป้องกันข้อมูลที่ถูกอัพเดตโดย CPU ตัวอื่นในเวลาเดียวกัน ในตัวอย่างข้างต้นคุณจะต้องใช้ข้อมูลที่ไม่ได้จัดแนวเพื่อรับความเสียหายที่อาจเกิดขึ้น คำสำคัญระเหยจะไม่ป้องกันความเสียหายที่อาจเกิดขึ้นหากข้อมูลไม่สามารถจัดการแบบอะตอมตัวอย่างเช่นถ้าตัวนับลูปเป็นประเภทยาวยาว (64 บิต) ดังนั้นมันจะต้องใช้สอง 32 บิตในการอัปเดตค่ากลาง ซึ่งการขัดจังหวะสามารถเกิดขึ้นและเปลี่ยนแปลงข้อมูลได้
ดังนั้นคำสำคัญระเหยง่ายสำหรับข้อมูลที่สอดคล้องซึ่งน้อยกว่าหรือเท่ากับขนาดของการลงทะเบียนพื้นเมืองเช่นการดำเนินการที่เป็นอะตอมเสมอ
คำสำคัญระเหยถูกคิดว่าจะใช้กับการดำเนินงาน IO ที่ IO จะเปลี่ยนแปลงตลอดเวลา แต่มีที่อยู่คงที่เช่นหน่วยความจำที่แมปอุปกรณ์ UART และคอมไพเลอร์ไม่ควรนำค่าแรกอ่านจากที่อยู่
หากคุณจัดการกับข้อมูลขนาดใหญ่หรือมี CPU หลายตัวคุณจะต้องใช้ระบบล็อคระดับสูงกว่า (OS) เพื่อจัดการการเข้าถึงข้อมูลอย่างถูกต้อง
หากคุณใช้. NET 1.1 จำเป็นต้องใช้คีย์เวิร์ดแบบระเหยเมื่อทำการล็อคที่ทำเครื่องหมายไว้สองครั้ง ทำไม? เนื่องจากก่อนหน้า. NET 2.0 สถานการณ์ต่อไปนี้อาจทำให้เกิดเธรดที่สองเพื่อเข้าถึงวัตถุที่ไม่เป็นโมฆะ
ก่อนหน้า. NET 2.0, this.foo สามารถกำหนดอินสแตนซ์ใหม่ของ Foo ได้ก่อนที่ Constructor จะทำงานเสร็จ ในกรณีนี้อาจมีเธรดที่สองเข้ามา (ในระหว่างการเรียกเธรด 1 ของตัวสร้าง Foo) และประสบการณ์ต่อไปนี้:
ก่อนที่จะ. NET 2.0 คุณสามารถประกาศสิ่งนี้ได้เช่นเดียวกับความผันผวนในการแก้ไขปัญหานี้ ตั้งแต่. NET 2.0 คุณไม่จำเป็นต้องใช้คำสำคัญระเหยเพื่อทำการล็อคการตรวจสอบสองครั้ง
วิกิพีเดียมีบทความที่ดีเกี่ยวกับการล็อคสองครั้งที่ผ่านมาและสัมผัสสั้น ๆ ในหัวข้อนี้: http://en.wikipedia.org/wiki/Double-checked_locking
foo
อย่างไร ไม่ล็อคเธรด 1 this.bar
และดังนั้นเฉพาะเธรด 1 เท่านั้นจึงจะสามารถเริ่มต้น foo ที่จุด givne ในเวลาหรือไม่ ฉันหมายความว่าคุณทำการตรวจสอบค่าหลังจากการปลดล็อคอีกครั้งเมื่อมันควรจะมีค่าใหม่จากเธรด 1
บางครั้งคอมไพเลอร์จะปรับฟิลด์ให้เหมาะสมและใช้รีจิสเตอร์เพื่อเก็บมัน หากเธรด 1 ทำการเขียนไปยังฟิลด์และเธรดอื่นเข้าถึงได้เนื่องจากการอัปเดตถูกเก็บไว้ในรีจิสเตอร์ (และไม่ใช่หน่วยความจำ) เธรดที่ 2 จะได้รับข้อมูลเก่า
คุณสามารถคิดถึงคำสำคัญที่ลบเลือนว่าเป็นการพูดกับคอมไพเลอร์ "ฉันต้องการให้คุณเก็บค่านี้ไว้ในหน่วยความจำ" สิ่งนี้รับประกันว่าเธรดที่ 2 ดึงค่าล่าสุด
จากMSDN : ตัวดัดแปลงแบบระเหยมักใช้สำหรับฟิลด์ที่เข้าถึงโดยเธรดจำนวนมากโดยไม่ต้องใช้คำสั่งล็อกเพื่อเข้าถึงแบบอนุกรม การใช้ตัวแก้ไขแบบระเหยทำให้แน่ใจได้ว่าเธรดหนึ่งดึงค่าล่าสุดที่เขียนโดยเธรดอื่น
CLR ชอบที่จะปรับคำแนะนำให้เหมาะสมดังนั้นเมื่อคุณเข้าถึงฟิลด์ในโค้ดอาจไม่สามารถเข้าถึงค่าปัจจุบันของฟิลด์ได้ตลอดเวลา (อาจมาจากสแต็ก ฯลฯ ) การทำเครื่องหมายฟิลด์volatile
เพื่อให้แน่ใจว่าค่าปัจจุบันของฟิลด์นั้นเข้าถึงได้โดยคำสั่ง สิ่งนี้มีประโยชน์เมื่อค่าสามารถแก้ไขได้ (ในสถานการณ์ที่ไม่ล็อค) โดยเธรดที่เกิดขึ้นพร้อมกันในโปรแกรมของคุณหรือโค้ดอื่น ๆ ที่รันในระบบปฏิบัติการ
คุณสูญเสียการเพิ่มประสิทธิภาพบางอย่างชัดเจน แต่จะทำให้รหัสง่ายขึ้น
ฉันพบบทความนี้โดยJoydip Kanjilalมีประโยชน์มาก!
When you mark an object or a variable as volatile, it becomes a candidate for volatile reads and writes. It should be noted that in C# all memory writes are volatile irrespective of whether you are writing data to a volatile or a non-volatile object. However, the ambiguity happens when you are reading data. When you are reading data that is non-volatile, the executing thread may or may not always get the latest value. If the object is volatile, the thread always gets the most up-to-date value
ฉันจะทิ้งไว้ที่นี่เพื่ออ้างอิง
คอมไพเลอร์บางครั้งเปลี่ยนลำดับของคำสั่งในรหัสเพื่อปรับให้เหมาะสม โดยปกตินี่ไม่ใช่ปัญหาในสภาพแวดล้อมแบบเธรดเดียว แต่อาจเป็นปัญหาในสภาพแวดล้อมแบบมัลติเธรด ดูตัวอย่างต่อไปนี้:
private static int _flag = 0;
private static int _value = 0;
var t1 = Task.Run(() =>
{
_value = 10; /* compiler could switch these lines */
_flag = 5;
});
var t2 = Task.Run(() =>
{
if (_flag == 5)
{
Console.WriteLine("Value: {0}", _value);
}
});
หากคุณรัน t1 และ t2 คุณคาดว่าจะไม่มีผลลัพธ์หรือ "Value: 10" เป็นผลลัพธ์ อาจเป็นได้ว่าคอมไพเลอร์สลับสายภายในฟังก์ชั่น t1 หาก t2 ดำเนินการอาจเป็นไปได้ว่า _flag มีค่าเท่ากับ 5 แต่ _value มีค่า 0 ดังนั้นตรรกะที่คาดไว้อาจใช้งานไม่ได้
ในการแก้ไขปัญหานี้คุณสามารถใช้คำหลักระเหยง่ายที่คุณสามารถนำไปใช้กับฟิลด์ คำสั่งนี้ปิดใช้งานการเพิ่มประสิทธิภาพของคอมไพเลอร์เพื่อให้คุณสามารถบังคับลำดับที่ถูกต้องในรหัสของคุณ
private static volatile int _flag = 0;
คุณควรใช้สารระเหยถ้าคุณต้องการจริงๆเพราะมันปิดการใช้งานการเพิ่มประสิทธิภาพคอมไพเลอร์บางอย่างก็จะส่งผลกระทบต่อประสิทธิภาพการทำงาน มันไม่รองรับกับทุกภาษา. NET (Visual Basic ไม่รองรับ) ดังนั้นจึงเป็นอุปสรรคต่อการทำงานร่วมกันของภาษา
ดังนั้นเพื่อสรุปทั้งหมดคำตอบที่ถูกต้องสำหรับคำถามคือ: หากรหัสของคุณกำลังทำงานในรันไทม์ 2.0 หรือใหม่กว่าคำสำคัญที่ระเหยง่ายนั้นแทบจะไม่จำเป็นเลยและจะเป็นอันตรายมากกว่าดีถ้าใช้โดยไม่จำเป็น IE ไม่เคยใช้มัน แต่ในรุ่นก่อนหน้าของรันไทม์มันเป็นสิ่งจำเป็นสำหรับการตรวจสอบล็อคที่เหมาะสมในฟิลด์คงที่ ฟิลด์สแตติกเฉพาะที่คลาสมีรหัสการกำหนดค่าเริ่มต้นคลาสสแตติก
หลายกระทู้สามารถเข้าถึงตัวแปร การปรับปรุงล่าสุดจะอยู่ในตัวแปร