เหตุการณ์ C # และความปลอดภัยของเธรด


237

UPDATE

ตั้งแต่ C # 6 คำตอบสำหรับคำถามนี้คือ:

SomeEvent?.Invoke(this, e);

ฉันมักจะได้ยิน / อ่านคำแนะนำต่อไปนี้:

ทำสำเนาของเหตุการณ์เสมอก่อนที่คุณจะตรวจสอบnullและดำเนินการ การทำเช่นนี้จะช่วยขจัดปัญหาที่อาจเกิดขึ้นกับเธรดที่เหตุการณ์เกิดขึ้นnullที่ตำแหน่งระหว่างที่คุณตรวจสอบว่ามีค่าว่างหรือไม่

// Copy the event delegate before checking/calling
EventHandler copy = TheEvent;

if (copy != null)
    copy(this, EventArgs.Empty); // Call any handlers on the copied list

อัปเดต : ฉันคิดว่าจากการอ่านเกี่ยวกับการปรับให้เหมาะสมซึ่งสิ่งนี้อาจต้องการให้สมาชิกเหตุการณ์ระเหยได้ แต่ Jon Skeet ระบุไว้ในคำตอบของเขาว่า CLR ไม่ได้เพิ่มประสิทธิภาพการคัดลอก

แต่ในขณะเดียวกันเพื่อให้ปัญหานี้เกิดขึ้นอีกเธรดอื่นต้องทำดังนี้:

// Better delist from event - don't want our handler called from now on:
otherObject.TheEvent -= OnTheEvent;
// Good, now we can be certain that OnTheEvent will not run...

ลำดับที่แท้จริงอาจเป็นส่วนผสมนี้:

// Copy the event delegate before checking/calling
EventHandler copy = TheEvent;

// Better delist from event - don't want our handler called from now on:
otherObject.TheEvent -= OnTheEvent;    
// Good, now we can be certain that OnTheEvent will not run...

if (copy != null)
    copy(this, EventArgs.Empty); // Call any handlers on the copied list

จุดที่OnTheEventทำงานหลังจากผู้เขียนได้ยกเลิกการสมัครและยังพวกเขาเพียงยกเลิกการสมัครสมาชิกโดยเฉพาะเพื่อหลีกเลี่ยงสิ่งที่เกิดขึ้น สิ่งที่จำเป็นจริงๆคือการปรับใช้เหตุการณ์ที่กำหนดเองด้วยการซิงโครไนซ์ที่เหมาะสมในaddและremoveอุปกรณ์ และนอกจากนี้ยังมีปัญหาการหยุดชะงักที่เป็นไปได้หากล็อคจะถูกจัดขึ้นในขณะที่เหตุการณ์ถูกไล่ออก

ดังนั้นการเขียนโปรแกรม Cargo ลัทธินี้คืออะไร? ดูเหมือนว่า - ผู้คนจำนวนมากต้องทำตามขั้นตอนนี้เพื่อปกป้องรหัสของพวกเขาจากหลายเธรดเมื่อในความเป็นจริงฉันคิดว่าเหตุการณ์ต้องใช้ความระมัดระวังมากกว่านี้ก่อนที่พวกเขาจะสามารถใช้เป็นส่วนหนึ่งของการออกแบบแบบมัลติเธรด . ดังนั้นผู้ที่ไม่ได้รับการดูแลเพิ่มเติมอาจเพิกเฉยต่อคำแนะนำนี้ - มันไม่ใช่ปัญหาสำหรับโปรแกรมแบบเธรดเดียวและในความเป็นจริงหากไม่มีvolatileรหัสตัวอย่างออนไลน์ส่วนใหญ่คำแนะนำอาจไม่มี ผลกระทบทั้งหมด

(และมันง่ายกว่าไหมถ้าเพียงแค่กำหนดค่าว่างdelegate { }ในการประกาศสมาชิกเพื่อให้คุณไม่จำเป็นต้องตรวจสอบnullตั้งแต่แรก)

Updated:ในกรณีที่ไม่ชัดเจนฉันเข้าใจถึงความตั้งใจของคำแนะนำ - เพื่อหลีกเลี่ยงข้อยกเว้นการอ้างอิงที่เป็นโมฆะในทุกสถานการณ์ ประเด็นของฉันคือข้อยกเว้นการอ้างอิง null นี้สามารถเกิดขึ้นได้หากเธรดอื่นถูกเพิกถอนจากเหตุการณ์และเหตุผลเดียวที่ทำเช่นนั้นคือเพื่อให้แน่ใจว่าจะไม่มีการรับสายอีกต่อไปผ่านเหตุการณ์นั้นซึ่งเทคนิคนี้ไม่สามารถทำได้อย่างชัดเจน . คุณต้องการปกปิดสภาพการแข่งขัน - มันจะเป็นการดีกว่าถ้าคุณเปิดเผยมัน! ข้อยกเว้น null นั้นช่วยในการตรวจสอบการละเมิดส่วนประกอบของคุณ หากคุณต้องการให้องค์ประกอบของคุณได้รับการปกป้องจากการละเมิดคุณสามารถทำตามตัวอย่างของ WPF - เก็บ ID เธรดในตัวสร้างของคุณแล้วโยนข้อยกเว้นหากเธรดอื่นพยายามโต้ตอบโดยตรงกับส่วนประกอบของคุณ หรือใช้ส่วนประกอบของเธรดที่ปลอดภัยอย่างแท้จริง (ไม่ใช่เรื่องง่าย)

ดังนั้นฉันขอยืนยันว่าการทำสำเนา / ตรวจสอบสำนวนนี้เป็นการเขียนโปรแกรมลัทธิสินค้าเท่านั้นเพิ่มความยุ่งเหยิงและเสียงรบกวนให้กับรหัสของคุณ เพื่อป้องกันเธรดอื่น ๆ จริง ๆ ต้องใช้งานมากขึ้น

อัปเดตเพื่อตอบสนองต่อบล็อกของ Eric Lippert:

ดังนั้นมีสิ่งสำคัญที่ฉันพลาดเกี่ยวกับตัวจัดการเหตุการณ์: "ตัวจัดการเหตุการณ์จำเป็นต้องแข็งแกร่งในหน้าของการถูกเรียกแม้ว่าจะยกเลิกการสมัครกิจกรรม" และแน่นอนดังนั้นเราจึงต้องดูแลเกี่ยวกับความเป็นไปได้ของเหตุการณ์ nullผู้รับมอบสิทธิ์ความเป็นอยู่ ข้อกำหนดเกี่ยวกับตัวจัดการเหตุการณ์นั้นมีการบันทึกไว้ทุกที่หรือไม่?

ดังนั้น: "มีวิธีอื่นในการแก้ปัญหานี้ตัวอย่างเช่นการเริ่มต้นตัวจัดการเพื่อให้มีการกระทำที่ว่างเปล่าที่ไม่เคยถูกลบออกไป แต่การตรวจสอบค่า null เป็นรูปแบบมาตรฐาน"

ดังนั้นคำถามที่เหลืออีกส่วนหนึ่งของฉันคือทำไมจึงชัดเจน - โมฆะ - ตรวจสอบ "รูปแบบมาตรฐาน"? อีกทางเลือกหนึ่งคือการมอบหมายผู้แทนที่ว่างเปล่าจะต้อง= delegate {}ถูกเพิ่มเข้าไปในการประกาศกิจกรรมเท่านั้นและนี่จะเป็นการขจัดพิธีเล็ก ๆ เหล่านั้นให้หมดไปจากทุกที่ที่มีการจัดงาน มันจะง่ายต่อการตรวจสอบให้แน่ใจว่าผู้รับมอบสิทธิ์ที่ว่างเปล่านั้นราคาถูกในการสร้างอินสแตนซ์ หรือฉันยังขาดอะไรอยู่

แน่นอนว่าต้องเป็นอย่างนั้น (ตามที่ Jon Skeet แนะนำ) นี่เป็นเพียงคำแนะนำ. NET 1.x ที่ยังไม่ตายเหมือนที่ควรจะทำในปี 2005?


4
คำถามนี้เกิดขึ้นเมื่อมีการพูดคุยกันภายในอีกระยะหนึ่ง ฉันตั้งใจที่จะบล็อกสิ่งนี้มาระยะหนึ่งแล้ว โพสต์ของฉันในเรื่องอยู่ที่นี่: กิจกรรมและการแข่งขัน
Eric Lippert

3
Stephen Cleary มีบทความ CodeProjectซึ่งตรวจสอบปัญหานี้และเขาสรุปวัตถุประสงค์ทั่วไปไม่มีวิธีแก้ปัญหา "thread-safe" โดยทั่วไปจะขึ้นอยู่กับตัวเรียกเหตุการณ์เพื่อให้แน่ใจว่าผู้รับมอบสิทธิ์ไม่เป็นโมฆะและขึ้นอยู่กับตัวจัดการเหตุการณ์เพื่อให้สามารถจัดการกับการเรียกใช้หลังจากที่ถูกยกเลิกการสมัคร
rkagerer

3
@rkagerer - จริงๆแล้วปัญหาที่สองบางครั้งจะต้องได้รับการจัดการโดยตัวจัดการเหตุการณ์แม้ว่าเธรดจะไม่เกี่ยวข้อง สามารถเกิดขึ้นได้หากผู้จัดการเหตุการณ์รายหนึ่งแจ้งผู้จัดการรายอื่นให้ยกเลิกการสมัครจากกิจกรรมที่กำลังจัดการอยู่ แต่ผู้สมัครสมาชิกคนที่ 2 นั้นจะได้รับเหตุการณ์ต่อไป (เพราะยกเลิกการสมัครในขณะที่กำลังดำเนินการจัดการ)
Daniel Earwicker

3
การเพิ่มการสมัครสมาชิกไปยังเหตุการณ์ที่มีสมาชิกเป็นศูนย์ลบการสมัครสมาชิกเท่านั้นสำหรับเหตุการณ์การเรียกใช้เหตุการณ์ที่มีสมาชิกศูนย์และการเรียกใช้เหตุการณ์ที่มีสมาชิกหนึ่งรายนั้นเป็นการดำเนินการที่รวดเร็วกว่าการเพิ่ม / ลบ / เรียกใช้ สมาชิก การเพิ่มตัวแทนตัวแทนจำลองจะทำให้กรณีทั่วไปช้าลง ปัญหาที่แท้จริงของ C # คือผู้สร้างตัดสินใจที่จะEventName(arguments)เรียกตัวแทนของเหตุการณ์โดยไม่มีเงื่อนไขแทนที่จะให้มันเรียกเฉพาะผู้รับมอบสิทธิ์หากไม่ใช่ค่าว่าง (ไม่ทำอะไรเลยถ้าไม่มีค่า)
supercat

คำตอบ:


100

JIT ไม่ได้รับอนุญาตให้ดำเนินการเพิ่มประสิทธิภาพที่คุณกำลังพูดถึงในส่วนแรกเนื่องจากเงื่อนไข ฉันรู้ว่าเรื่องนี้ถูกยกฐานะเป็นปีศาจเมื่อไม่นานมานี้ แต่มันก็ไม่ถูกต้อง (ฉันตรวจสอบกับ Joe Duffy หรือ Vance Morrison เมื่อไม่นานมานี้ฉันจำไม่ได้ว่าอันไหน)

หากไม่มีตัวแก้ไขแบบระเหยอาจเป็นไปได้ว่าสำเนาที่ถ่ายในเครื่องนั้นจะล้าสมัย แต่นั่นคือทั้งหมด NullReferenceExceptionมันจะไม่ทำให้เกิดการ

และใช่มีเงื่อนไขการแข่งขันอย่างแน่นอน - แต่จะมีเสมอ สมมติว่าเราเพิ่งเปลี่ยนรหัสเป็น:

TheEvent(this, EventArgs.Empty);

ตอนนี้สมมติว่ารายการการเรียกร้องสำหรับผู้รับมอบสิทธิ์นั้นมี 1,000 รายการ เป็นไปได้อย่างสมบูรณ์แบบที่การกระทำในตอนต้นของรายการจะถูกดำเนินการก่อนที่เธรดอื่นจะยกเลิกการสมัครตัวจัดการใกล้กับส่วนท้ายของรายการ อย่างไรก็ตามตัวจัดการนั้นจะยังคงทำงานเนื่องจากจะเป็นรายการใหม่ (ผู้ได้รับมอบหมายไม่สามารถเปลี่ยนแปลงได้) เท่าที่ฉันเห็นได้ว่าสิ่งนี้หลีกเลี่ยงไม่ได้

การใช้ตัวแทนที่ว่างเปล่านั้นจะหลีกเลี่ยงการตรวจสอบความผิดพลาด แต่ไม่สามารถแก้ไขสภาพการแข่งขันได้ นอกจากนี้ยังไม่รับประกันว่าคุณจะ "เห็น" คุณค่าของตัวแปรล่าสุดเสมอ


4
Joe Duffy "การเขียนโปรแกรมพร้อมกันบน Windows" ครอบคลุมการเพิ่มประสิทธิภาพ JIT และรูปแบบหน่วยความจำของคำถาม; ดูcode.logos.com/blog/2008/11/events_and_threads_part_4.html
Bradley Grainger

2
ฉันยอมรับสิ่งนี้โดยอ้างอิงจากความคิดเห็นเกี่ยวกับคำแนะนำ "มาตรฐาน" ที่เป็น pre-C # 2 และฉันไม่ได้ยินใครก็ตามที่โต้แย้งว่า เว้นแต่จะมีราคาแพงมากในการสร้างอินสแตนซ์เหตุการณ์ของคุณเพียงแค่ใส่ '= ผู้รับมอบสิทธิ์ {}' ในตอนท้ายของการประกาศกิจกรรมของคุณแล้วโทรเหตุการณ์ของคุณโดยตรงราวกับว่าเป็นวิธีการ อย่ากำหนด null ให้กับพวกเขา (สิ่งอื่น ๆ ที่ฉันนำมาเกี่ยวกับการทำให้มั่นใจว่าตัวจัดการไม่ได้ถูกเรียกหลังจากการเพิกถอนซึ่งเป็นสิ่งที่ไม่เกี่ยวข้องและเป็นไปไม่ได้ที่จะมั่นใจได้แม้จะเป็นรหัสเธรดเดี่ยวเช่นหากตัวจัดการ 1 ขอให้ตัวจัดการ 2 เพิกถอนตัวจัดการ 2 ถัดไป)
Daniel Earwicker

2
กรณีปัญหาเดียว (เช่นเคย) คือโครงสร้างซึ่งคุณไม่สามารถมั่นใจได้ว่าพวกเขาจะถูกสร้างอินสแตนซ์กับสิ่งอื่นนอกเหนือจากค่า Null ในสมาชิกของพวกเขา แต่ structs ดูด
Daniel Earwicker

1
เกี่ยวกับผู้ร่วมประชุมที่ว่างเปล่ายังเห็นคำถามนี้: stackoverflow.com/questions/170907/...
Vladimir

2
@ โทนี่: ยังคงมีสภาพการแข่งขันระหว่างสิ่งที่สมัคร / ยกเลิกการเป็นสมาชิกและผู้รับมอบสิทธิ์ถูกดำเนินการ รหัสของคุณ (เพิ่งจะเรียกดูได้ในเวลาสั้น ๆ ) จะลดเงื่อนไขการแข่งขันโดยอนุญาตให้สมัคร / ยกเลิกการสมัครเพื่อให้มีผลในขณะที่กำลังถูกเลี้ยง แต่ฉันสงสัยว่าในกรณีส่วนใหญ่ที่พฤติกรรมปกติไม่ดีพอ
Jon Skeet

52

ฉันเห็นผู้คนจำนวนมากกำลังเข้าสู่วิธีการขยายการทำสิ่งนี้ ...

public static class Extensions   
{   
  public static void Raise<T>(this EventHandler<T> handler, 
    object sender, T args) where T : EventArgs   
  {   
    if (handler != null) handler(sender, args);   
  }   
}

ที่ให้ไวยากรณ์ที่ดีขึ้นเพื่อยกระดับกิจกรรม ...

MyEvent.Raise( this, new MyEventArgs() );

และยังทำไปกับสำเนาโลคอลเนื่องจากมันถูกจับที่เวลาการเรียกใช้เมธอด


9
ฉันชอบไวยากรณ์ แต่ขอให้ชัดเจน ... มันไม่ได้แก้ปัญหาด้วยตัวจัดการค้างที่ถูกเรียกแม้หลังจากที่ไม่ได้ลงทะเบียน นี้เพียงแก้ปัญหา null dereference ในขณะที่ฉันชอบไวยากรณ์ฉันถามว่ามันดีกว่า: กิจกรรมสาธารณะ EventHandler <T> MyEvent = delete {}; ... MyEvent (นี่คือใหม่ MyEventArgs ()); นี่เป็นวิธีแก้ปัญหาแรงเสียดทานต่ำมากที่ฉันชอบสำหรับความเรียบง่าย
Simon Gillbee

@ Simon ฉันเห็นคนอื่นพูดถึงสิ่งที่แตกต่างกันในเรื่องนี้ ฉันได้ทดสอบแล้วและสิ่งที่ฉันทำบ่งชี้ว่าสิ่งนี้จัดการกับปัญหาตัวจัดการโมฆะได้ แม้ว่า Sink ดั้งเดิมจะไม่ถอนการลงทะเบียนจากกิจกรรมหลังจากการตรวจสอบ handler! = null แต่เหตุการณ์นั้นยังคงมีอยู่และจะไม่มีข้อยกเว้นเกิดขึ้น
JP Alioto

yup, cf คำถามนี้: stackoverflow.com/questions/192980/…
Benjol

1
+1 ฉันเพิ่งเขียนวิธีนี้ด้วยตัวเองเริ่มคิดเกี่ยวกับความปลอดภัยของเธรดได้ทำการวิจัยบางอย่างและสะดุดกับคำถามนี้
Niels van der Rest

สิ่งนี้จะถูกเรียกจาก VB.NET ได้อย่างไร? หรือ 'RaiseEvent' รองรับสถานการณ์แบบมัลติเธรดหรือไม่

35

"ทำไมชัดแจ้งเป็นโมฆะ - ตรวจสอบ 'รูปแบบมาตรฐาน' หรือไม่"

ฉันสงสัยว่าเหตุผลนี้อาจเป็นเพราะการตรวจสอบโมฆะนั้นมีประสิทธิภาพมากกว่า

หากคุณสมัครสมาชิกตัวแทนที่ว่างเปล่าให้กับกิจกรรมของคุณเมื่อพวกเขาถูกสร้างขึ้นจะมีค่าใช้จ่ายบางส่วน:

  • ค่าใช้จ่ายในการสร้างผู้แทนที่ว่างเปล่า
  • ค่าใช้จ่ายในการสร้างโซ่ผู้รับมอบสิทธิ์เพื่อเก็บไว้
  • ค่าใช้จ่ายในการอัญเชิญผู้แทนที่ไม่มีจุดหมายทุกครั้งที่มีการเพิ่มกิจกรรม

(โปรดทราบว่าการควบคุม UI มักจะมีเหตุการณ์จำนวนมากซึ่งส่วนใหญ่ไม่เคยสมัครเป็นสมาชิกโดยต้องสร้างผู้สมัครสมาชิกจำลองให้แต่ละเหตุการณ์

ฉันทำการทดสอบประสิทธิภาพคร่าวๆเพื่อดูผลกระทบของวิธีสมัครสมาชิกที่ว่างเปล่าและนี่คือผลลัพธ์ของฉัน:

Executing 50000000 iterations . . .
OnNonThreadSafeEvent took:      432ms
OnClassicNullCheckedEvent took: 490ms
OnPreInitializedEvent took:     614ms <--
Subscribing an empty delegate to each event . . .
Executing 50000000 iterations . . .
OnNonThreadSafeEvent took:      674ms
OnClassicNullCheckedEvent took: 674ms
OnPreInitializedEvent took:     2041ms <--
Subscribing another empty delegate to each event . . .
Executing 50000000 iterations . . .
OnNonThreadSafeEvent took:      2011ms
OnClassicNullCheckedEvent took: 2061ms
OnPreInitializedEvent took:     2246ms <--
Done

โปรดทราบว่าสำหรับกรณีของศูนย์หรือสมาชิกหนึ่งคน (โดยทั่วไปสำหรับการควบคุม UI ที่มีกิจกรรมมากมาย) เหตุการณ์ที่เริ่มต้นด้วยตัวแทนเปล่าจะช้าลงอย่างเห็นได้ชัด (มากกว่า 50 ล้านซ้ำ ... )

สำหรับข้อมูลเพิ่มเติมและรหัสที่มาเยี่ยมชมบล็อกโพสต์เกี่ยวกับเรื่องนี้.NET จัดกิจกรรมความปลอดภัยหัวข้อภาวนาว่าผมเผยแพร่เพียงวันก่อนคำถามนี้ถูกถาม (!)

(การตั้งค่าการทดสอบของฉันอาจมีข้อบกพร่องโปรดอย่าลังเลที่จะดาวน์โหลดซอร์สโค้ดและตรวจสอบด้วยตนเอง


8
ฉันคิดว่าคุณทำประเด็นสำคัญในการโพสต์บล็อกของคุณ: ไม่จำเป็นต้องกังวลเกี่ยวกับผลกระทบของประสิทธิภาพจนกว่าจะเป็นคอขวด ทำไมปล่อยให้วิธีที่น่าเกลียดเป็นวิธีที่แนะนำ? ถ้าเราต้องการปรับให้เหมาะสมก่อนกำหนดแทนความชัดเจนเราจะใช้แอสเซมเบลอร์ - ดังนั้นคำถามของฉันยังคงอยู่และฉันคิดว่าคำตอบที่น่าจะเป็นคือคำแนะนำนั้นง่ายต่อการมอบหมายตัวแทนที่ไม่ระบุชื่อและใช้เวลานาน ในเรื่อง "pot roast story" ที่มีชื่อเสียง
Daniel Earwicker

13
และตัวเลขของคุณพิสูจน์จุดดีมาก: ค่าโสหุ้ยลดลงเหลือแค่ NANOSECONDS เพียงสองและครึ่ง (!!!) ต่อเหตุการณ์ที่เกิดขึ้น (pre-init vs. classic-null) สิ่งนี้จะตรวจจับไม่ได้ในแอพเกือบทั้งหมดที่มีการทำงานจริง แต่เนื่องจากการใช้งานเหตุการณ์ส่วนใหญ่อยู่ในกรอบงาน GUI คุณจะต้องเปรียบเทียบสิ่งนี้กับต้นทุนของการทาสีส่วนของหน้าจอใน Winforms เป็นต้นดังนั้น มันจะกลายเป็นมองไม่เห็นมากขึ้นในการทำงานของ CPU จริงและรอทรัพยากร คุณได้รับ +1 จากฉันสำหรับการทำงานหนัก :)
Daniel Earwicker

1
@DanielEarwicker พูดถูกต้องคุณได้ทำให้ฉันเป็นผู้ศรัทธาในเหตุการณ์สาธารณะ WrapperDoneHandler OnWrapperDone = (x, y) => {}; แบบ
Mickey Perlstein

1
มันอาจจะดีต่อเวลาDelegate.Combine/ Delegate.Removeคู่ในกรณีที่เหตุการณ์มีศูนย์สมาชิกหนึ่งหรือสองคน หากมีการเพิ่มและลบอินสแตนซ์ของผู้ร่วมประชุมซ้ำ ๆ ซ้ำ ๆ กันความแตกต่างของค่าใช้จ่ายในแต่ละกรณีจะเด่นชัดCombineเป็นพิเศษเนื่องจากมีพฤติกรรมกรณีและปัญหาพิเศษที่รวดเร็วเมื่อมีข้อโต้แย้งอย่างใดอย่างหนึ่งnull(เพิ่งกลับมาอีกครั้ง) และRemoveรวดเร็วมาก เท่ากับ (เพิ่งกลับ null)
supercat

12

ฉันสนุกกับการอ่านนี้จริงๆ - ไม่ใช่! แม้ว่าฉันต้องการมันเพื่อทำงานกับคุณสมบัติ C # ที่เรียกว่า events!

ทำไมไม่แก้ไขในคอมไพเลอร์? ฉันรู้ว่ามีคน MS ที่อ่านโพสต์เหล่านี้ดังนั้นโปรดอย่าเปลวไฟนี้!

1 - ปัญหา Null ) ทำไมไม่สร้างเหตุการณ์ให้เป็น. แทนที่จะเป็นโมฆะในตอนแรก จะมีการบันทึกรหัสกี่บรรทัดสำหรับการตรวจสอบค่าว่างหรือต้องติด= delegate {}ประกาศ ให้คอมไพเลอร์จัดการกับกรณีที่ว่างเปล่า IE ไม่ทำอะไรเลย! หากทุกอย่างเกี่ยวข้องกับผู้สร้างเหตุการณ์พวกเขาสามารถตรวจสอบหา. เรียบเรียงและทำทุกอย่างที่พวกเขาสนใจ! มิฉะนั้นการตรวจสอบค่า null / การเพิ่มผู้รับมอบสิทธิ์จะถูกแฮ็คปัญหา!

สุจริตฉันเบื่อที่จะต้องทำเช่นนี้กับทุกเหตุการณ์ - aka รหัสสำเร็จรูป!

public event Action<thisClass, string> Some;
protected virtual void DoSomeEvent(string someValue)
{
  var e = Some; // avoid race condition here! 
  if(null != e) // avoid null condition here! 
     e(this, someValue);
}

2 - ปัญหาสภาพการแข่งขัน ) ฉันอ่านโพสต์บล็อกของ Eric ฉันยอมรับว่า H (ตัวจัดการ) ควรจัดการเมื่อมีการแก้ไขตัวเอง แต่เหตุการณ์ไม่สามารถทำให้ไม่เปลี่ยนรูป / เธรดปลอดภัยหรือไม่ IE ตั้งค่าการล็อคธงในการสร้างของมันดังนั้นเมื่อใดก็ตามที่มันถูกเรียกมันจะล็อคการสมัครสมาชิกทั้งหมดและยกเลิกการสมัครสมาชิกมันในขณะที่กำลังดำเนินการ?

สรุป ,

ภาษาสมัยใหม่ไม่ควรแก้ปัญหาเช่นนี้ให้เรา?


เห็นด้วยควรมีการสนับสนุนที่ดีกว่าสำหรับคอมไพเลอร์ จนแล้วฉันสร้างด้าน PostSharp ซึ่งไม่นี้ในขั้นตอนการโพสต์คอมไพล์ :)
Steven Jeuris

4
Heaving สมัครสมาชิกด้าย / คำขอยกเลิกป้องกันในขณะที่รอรหัสนอกที่กำหนดเองที่สมบูรณ์ไกลยิ่งกว่าที่มีสมาชิกได้รับเหตุการณ์หลังจากการสมัครสมาชิกจะถูกยกเลิกโดยเฉพาะอย่างยิ่งนับตั้งแต่ "ปัญหา" หลังจะสามารถแก้ไขได้อย่างง่ายดายโดยเพียงแค่มีการจัดการเหตุการณ์ตรวจสอบธงเพื่อดูว่า พวกเขายังคงสนใจที่จะรับงานของพวกเขา แต่การหยุดชะงักซึ่งเป็นผลมาจากการออกแบบในอดีตอาจเป็นเรื่องยาก
supercat

@supercat Imo ความคิดเห็น "ไกลยิ่งแย่" ขึ้นอยู่กับแอพพลิเคชั่นที่ค่อนข้างสวย ใครจะไม่ต้องการการล็อคที่เข้มงวดมากโดยไม่มีการตั้งค่าสถานะเพิ่มเติมเมื่อเป็นตัวเลือก การหยุดชะงักควรเกิดขึ้นเฉพาะในกรณีที่เธรดการจัดการเหตุการณ์กำลังรอเธรดอื่น (นั่นคือการ subcribing / unsubscribeing) เนื่องจากการล็อกนั้นเป็นการเข้าร่วมของเธรดเดียวกันและการสมัคร / ยกเลิกการสมัครภายในตัวจัดการเหตุการณ์ดั้งเดิมจะไม่ถูกบล็อก หากมีการรอเธรดข้ามเป็นส่วนหนึ่งของตัวจัดการเหตุการณ์ที่จะเป็นส่วนหนึ่งของการออกแบบที่ฉันต้องการทำงานซ้ำ ฉันมาจากมุมแอพฝั่งเซิร์ฟเวอร์ที่มีรูปแบบที่สามารถคาดเดาได้
crokusek

2
@crokusek: การวิเคราะห์ที่จำเป็นในการพิสูจน์ว่าระบบไม่มีการหยุดชะงักเป็นเรื่องง่ายหากไม่มีกราฟในการกำกับการเชื่อมต่อแต่ละล็อคกับล็อคทั้งหมดที่อาจจำเป็นในขณะที่มันถือ [ขาดรอบพิสูจน์ระบบ การหยุดชะงักฟรี] การอนุญาตให้โค้ดโดยพลการถูกเรียกใช้ในขณะที่ล็อคถูกเก็บไว้จะสร้างขอบในกราฟ "อาจจะเป็นที่ต้องการ" จากการล็อคนั้นไปยังการล็อกใด ๆ ที่รหัสโดยพลการอาจได้รับ (ไม่ใช่การล็อคในระบบทุกอย่าง ) การมีอยู่ของวัฏจักรนั้นไม่ได้หมายความว่าการหยุดชะงักจะเกิดขึ้น แต่ ...
supercat

1
... จะเพิ่มระดับการวิเคราะห์ที่จำเป็นเพื่อพิสูจน์ว่าไม่สามารถทำได้
supercat

8

ด้วยC # 6ขึ้นไปโค้ดสามารถลดความซับซ้อนโดยใช้?.โอเปอเรเตอร์ใหม่ดังต่อไปนี้:

TheEvent?.Invoke(this, EventArgs.Empty);

นี่คือเอกสาร MSDN


5

ตามที่ Jeffrey Richter ในหนังสือCLR ผ่าน C #วิธีการที่ถูกต้องคือ:

// Copy a reference to the delegate field now into a temporary field for thread safety
EventHandler<EventArgs> temp =
Interlocked.CompareExchange(ref NewMail, null, null);
// If any methods registered interest with our event, notify them
if (temp != null) temp(this, e);

เพราะมันเป็นแรงสำเนาอ้างอิง สำหรับข้อมูลเพิ่มเติมโปรดดูที่ส่วนกิจกรรมของเขาในหนังสือ


อาจจะเป็นฉันหายไป smth แต่ InterlockedCompareExchange พ่น NullReferenceException ถ้าอาร์กิวเมนต์แรกเป็นโมฆะซึ่งเป็นสิ่งที่เราต้องการหลีกเลี่ยง msdn.microsoft.com/en-us/library/bb297966.aspx
Kniganapolke

1
Interlocked.CompareExchangeจะล้มเหลวหากผ่านไปอย่างใดอย่างหนึ่งเป็นโมฆะrefแต่นั่นไม่ใช่สิ่งเดียวกับที่ถูกส่งผ่านrefไปยังที่เก็บข้อมูล (เช่นNewMail) ซึ่งมีอยู่และที่เก็บข้อมูลอ้างอิงโมฆะในตอนแรก
supercat

4

ฉันใช้รูปแบบการออกแบบนี้เพื่อให้แน่ใจว่าตัวจัดการเหตุการณ์จะไม่ถูกเรียกใช้หลังจากที่ยกเลิกการสมัคร มันใช้งานได้ดีจนถึงแม้ว่าฉันจะไม่ได้พยายามทำโปรไฟล์ใด ๆ

private readonly object eventMutex = new object();

private event EventHandler _onEvent = null;

public event EventHandler OnEvent
{
  add
  {
    lock(eventMutex)
    {
      _onEvent += value;
    }
  }

  remove
  {
    lock(eventMutex)
    {
      _onEvent -= value;
    }
  }

}

private void HandleEvent(EventArgs args)
{
  lock(eventMutex)
  {
    if (_onEvent != null)
      _onEvent(args);
  }
}

ฉันทำงานกับ Mono สำหรับ Android เป็นส่วนใหญ่ในปัจจุบันและ Android ดูเหมือนจะไม่ชอบเมื่อคุณพยายามอัปเดตมุมมองหลังจากกิจกรรมถูกส่งไปที่พื้นหลัง


อันที่จริงฉันเห็นคนอื่นกำลังใช้รูปแบบที่คล้ายกันมากที่นี่: stackoverflow.com/questions/3668953/…
Ash

2

การปฏิบัตินี้ไม่ได้เกี่ยวกับการบังคับใช้ลำดับของการดำเนินการบางอย่าง จริงๆแล้วมันเกี่ยวกับการหลีกเลี่ยงข้อยกเว้นการอ้างอิงว่าง

เหตุผลเบื้องหลังที่ผู้คนใส่ใจเกี่ยวกับข้อยกเว้นการอ้างอิงว่างเปล่าและไม่ใช่สภาพการแข่งขันจะต้องมีการวิจัยทางจิตวิทยาลึก ๆ ฉันคิดว่ามันมีบางอย่างเกี่ยวกับความจริงที่ว่าการแก้ไขปัญหาการอ้างอิง Null นั้นง่ายกว่ามาก เมื่อแก้ไขแล้วพวกเขาจะแขวนแบนเนอร์ "Mission Accomplished" ขนาดใหญ่ไว้ในรหัสของพวกเขาและเปิดเครื่องรูดชุดนักบินของพวกเขา

หมายเหตุ: การแก้ไขเงื่อนไขการแย่งชิงอาจเกี่ยวข้องกับการใช้แทร็กแบบซิงโครนัสแฟล็กว่าตัวจัดการควรรันหรือไม่


ฉันไม่ได้ขอวิธีแก้ไขปัญหานั้น ฉันสงสัยว่าเหตุใดจึงมีคำแนะนำอย่างกว้างขวางในการพ่นโค้ดเพิ่มเติมรอบ ๆ เหตุการณ์การยิงเมื่อมันหลีกเลี่ยงข้อยกเว้นว่างเปล่าเมื่อมีสภาพการแข่งขันที่ยากต่อการตรวจจับซึ่งจะยังคงมีอยู่
Daniel Earwicker

1
นั่นคือจุดของฉัน พวกเขาไม่สนใจสภาพการแข่งขัน พวกเขาสนใจเฉพาะข้อยกเว้นการอ้างอิง Null เท่านั้น ฉันจะแก้ไขมันเป็นคำตอบของฉัน
dss539

และประเด็นของฉันคือ: เหตุใดจึงสมเหตุสมผลที่จะใส่ใจเกี่ยวกับข้อยกเว้นการอ้างอิงว่างเปล่าและยังไม่สนใจสภาพการแข่งขัน?
Daniel Earwicker

4
ผู้จัดการเหตุการณ์ที่เขียนอย่างถูกต้องควรเตรียมรับมือกับความจริงที่ว่าคำขอใด ๆ เพื่อยกระดับเหตุการณ์ที่การประมวลผลอาจทับซ้อนคำขอเพื่อเพิ่มหรือลบเหตุการณ์อาจจบลงด้วยการเพิ่มหรือลบเหตุการณ์ โปรแกรมเมอร์เหตุผลไม่สนใจเกี่ยวกับสภาพการแข่งขันที่อยู่ในรหัสถูกต้องเขียนมันจะไม่ว่าใครจะชนะ
supercat

2
@ dss539: ในขณะที่หนึ่งสามารถออกแบบกรอบงานเหตุการณ์ซึ่งจะบล็อกการร้องขอยกเลิกการสมัครจนกว่าจะเสร็จสิ้นการร้องขอเหตุการณ์ที่รอดำเนินการเสร็จสิ้นการออกแบบดังกล่าวจะทำให้เป็นไปไม่ได้สำหรับเหตุการณ์ใด ๆ (แม้แต่บางอย่างเช่นUnloadเหตุการณ์) น่ารังเกียจ ดีกว่าคือการบอกว่าคำขอยกเลิกการสมัครสมาชิกกิจกรรมจะทำให้เหตุการณ์ถูกยกเลิก "ที่สุด" และสมาชิกเหตุการณ์นั้นควรตรวจสอบเมื่อมีการเรียกใช้ว่ามีสิ่งใดที่เป็นประโยชน์สำหรับพวกเขาหรือไม่
supercat

1

ดังนั้นฉันมาช้าไปงานเลี้ยงที่นี่ :)

สำหรับการใช้ null แทนรูปแบบวัตถุ null เพื่อแสดงเหตุการณ์ที่ไม่มีสมาชิกให้พิจารณาสถานการณ์สมมตินี้ คุณต้องเรียกใช้เหตุการณ์ แต่การสร้างวัตถุ (EventArgs) นั้นไม่สำคัญและในกรณีทั่วไปเหตุการณ์ของคุณไม่มีสมาชิก มันจะเป็นประโยชน์กับคุณถ้าคุณสามารถเพิ่มประสิทธิภาพรหัสของคุณเพื่อตรวจสอบเพื่อดูว่าคุณมีสมาชิกใด ๆ ก่อนที่คุณจะมุ่งมั่นในการประมวลผลเพื่อสร้างข้อโต้แย้งและเรียกใช้เหตุการณ์

เมื่อคำนึงถึงสิ่งนี้โซลูชันจะพูดว่า "ดีสมาชิกศูนย์จะถูกแทนด้วย null" จากนั้นเพียงแค่ทำการตรวจสอบโมฆะก่อนที่จะดำเนินการที่มีราคาแพง ฉันคิดว่าวิธีการอื่นในการทำเช่นนี้จะต้องมีคุณสมบัติ Count ในประเภท Delegate ดังนั้นคุณจะต้องดำเนินการที่มีราคาแพงหาก myDelegate.Count> 0 การใช้คุณสมบัติ Count เป็นรูปแบบที่ค่อนข้างดีที่ช่วยแก้ปัญหาเดิม การอนุญาตการปรับให้เหมาะสมและยังมีคุณสมบัติที่ดีในการเรียกใช้โดยไม่ทำให้ NullReferenceException

โปรดจำไว้ว่าเนื่องจากผู้ได้รับมอบหมายเป็นประเภทอ้างอิงพวกเขาจะได้รับอนุญาตให้เป็นโมฆะ อาจไม่มีวิธีที่ดีในการซ่อนความจริงนี้ภายใต้หน้าปกและสนับสนุนเฉพาะรูปแบบวัตถุ null สำหรับเหตุการณ์ดังนั้นทางเลือกอาจบังคับให้นักพัฒนาตรวจสอบทั้งแบบ null และแบบสมาชิกศูนย์ นั่นอาจจะน่าเกลียดกว่าในสถานการณ์ปัจจุบัน

หมายเหตุ: นี่คือการเก็งกำไรที่บริสุทธิ์ ฉันไม่ได้เกี่ยวข้องกับ. NET languages ​​หรือ CLR


ฉันถือว่าคุณหมายถึง "การใช้ผู้แทนว่างเปล่ามากกว่า ... " คุณสามารถทำสิ่งที่คุณแนะนำได้แล้วโดยมีเหตุการณ์เริ่มต้นไปที่ผู้แทนที่ว่างเปล่า การทดสอบ (MyEvent.GetInvocationList (). Length == 1) จะเป็นจริงหากผู้รับมอบสิทธิ์เริ่มต้นว่างเปล่าเป็นสิ่งเดียวในรายการ ยังคงไม่จำเป็นต้องทำสำเนาก่อน แม้ว่าฉันคิดว่ากรณีที่คุณอธิบายจะหายากมากอยู่แล้ว
Daniel Earwicker

ฉันคิดว่าเรากำลังพูดถึงความคิดของผู้ได้รับมอบหมายและกิจกรรมที่นี่ ถ้าฉันมีเหตุการณ์ Foo ในชั้นเรียนของฉันเมื่อผู้ใช้ภายนอกโทร MyType.Foo + = / - = พวกเขากำลังเรียกเมธอด add_Foo () และ remove_Foo () จริง ๆ อย่างไรก็ตามเมื่อฉันอ้างอิง Foo จากภายในคลาสที่กำหนดไว้ฉันจะอ้างอิงผู้ได้รับมอบหมายพื้นฐานโดยตรงไม่ใช่วิธี add_Foo () และ remove_Foo () และด้วยการมีอยู่ของประเภทเช่น EventHandlerList ไม่มีอะไรที่บังคับว่าผู้ได้รับมอบหมายและเหตุการณ์จะต้องอยู่ในที่เดียวกัน นี่คือสิ่งที่ฉันหมายถึงในวรรค "จำไว้" ในการตอบสนองของฉัน
Levi

(ต่อ) ฉันยอมรับว่านี่คือการออกแบบที่สับสน แต่ทางเลือกอาจแย่ลงกว่าเดิม เนื่องจากในท้ายที่สุดสิ่งที่คุณมีคือผู้แทน - คุณอาจอ้างอิงผู้แทนพื้นฐานโดยตรงคุณอาจได้รับมันจากคอลเลกชันคุณอาจยกตัวอย่างมันทันที - มันอาจเป็นไปไม่ได้ในทางเทคนิคที่จะสนับสนุนสิ่งอื่นนอกเหนือจาก รูปแบบเป็นโมฆะ
Levi

ขณะที่เรากำลังพูดถึงการยิงเหตุการณ์ฉันไม่เห็นว่าทำไมการเพิ่ม / ลบ accessors จึงมีความสำคัญที่นี่
Daniel Earwicker

@Levi: ฉันไม่ชอบวิธีที่ C # จัดการกับเหตุการณ์ หากฉันมี druthers ของฉันผู้รับมอบสิทธิ์จะได้รับชื่อที่แตกต่างจากเหตุการณ์ จากนอกชั้นเรียน, การดำเนินงานเท่านั้นที่ได้รับอนุญาตในชื่อเหตุการณ์จะเป็นและ+= -=ภายในชั้นเรียน, การดำเนินงานที่ได้รับอนุญาตนอกจากนี้ยังจะรวมถึงการภาวนา (ที่มีในตัวตรวจสอบ null) การทดสอบกับหรือการตั้งค่าnull nullสำหรับสิ่งอื่นใดเราจะต้องใช้ผู้รับมอบสิทธิ์ที่มีชื่อจะเป็นชื่อเหตุการณ์ที่มีคำนำหน้าหรือคำต่อท้ายเฉพาะ
supercat

0

สำหรับแอปพลิเคชันแบบเธรดเดี่ยวคุณจะได้รับการยืนยันว่าไม่ใช่ปัญหา

อย่างไรก็ตามหากคุณกำลังสร้างองค์ประกอบที่เปิดเผยเหตุการณ์ไม่มีการรับประกันว่าผู้บริโภคของส่วนประกอบของคุณจะไม่ไปมัลติเธรดซึ่งในกรณีนี้คุณต้องเตรียมตัวสำหรับสถานการณ์ที่เลวร้ายที่สุด

การใช้ผู้รับมอบสิทธิ์ที่ว่างจะช่วยแก้ปัญหา แต่ยังทำให้ประสิทธิภาพการทำงานในทุกการเรียกไปยังเหตุการณ์และอาจมีผลกระทบ GC

คุณถูกต้องที่ผู้บริโภคมีสิทธิ์ยกเลิกการสมัครเพื่อให้สิ่งนี้เกิดขึ้น แต่ถ้าพวกเขาทำมันผ่านสำเนาชั่วคราวแล้วพิจารณาข้อความที่อยู่ระหว่างการขนส่ง

หากคุณไม่ได้ใช้ตัวแปรชั่วคราวและไม่ใช้ผู้รับมอบสิทธิ์ที่ว่างเปล่าและมีคนยกเลิกการเป็นสมาชิกคุณจะได้รับข้อยกเว้นการอ้างอิงว่างซึ่งเป็นอันตรายถึงชีวิตดังนั้นฉันคิดว่าต้นทุนคุ้มค่า


0

ฉันไม่เคยคิดว่านี่จะเป็นปัญหามากนักเพราะโดยทั่วไปแล้วฉันจะป้องกันความผิดพลาดที่อาจเกิดขึ้นจากการทำเกลียวในวิธีคงที่ (ฯลฯ ) บนส่วนประกอบที่สามารถนำกลับมาใช้ใหม่ได้และฉันจะไม่ทำเหตุการณ์คงที่

ฉันทำผิดหรือเปล่า?


หากคุณจัดสรรอินสแตนซ์ของคลาสที่มีสถานะไม่แน่นอน (เขตข้อมูลที่เปลี่ยนค่า) จากนั้นให้หลายเธรดเข้าถึงอินสแตนซ์เดียวกันพร้อมกันโดยไม่ต้องใช้การล็อกเพื่อป้องกันฟิลด์เหล่านั้นจากการแก้ไขโดยสองเธรดในเวลาเดียวกัน อาจทำผิด หากเธรดทั้งหมดของคุณมีอินสแตนซ์แยกต่างหากของตนเอง (ไม่แชร์อะไรเลย) หรือวัตถุทั้งหมดของคุณไม่เปลี่ยนรูป (เมื่อจัดสรรแล้วค่าของฟิลด์ของพวกเขาจะไม่เปลี่ยนแปลง) ดังนั้นคุณอาจไม่เป็นไร
Daniel Earwicker

วิธีการทั่วไปของฉันคือการซิงโครไนซ์กับผู้โทรยกเว้นวิธีการคงที่ หากฉันเป็นผู้โทรฉันจะซิงโครไนซ์ในระดับที่สูงกว่านั้น (ยกเว้นของวัตถุที่มีวัตถุประสงค์คือการจัดการการเข้าถึงข้อมูลให้ตรงกันของหลักสูตรนั้น :).)
เกร็ก D

@ GregD ที่ขึ้นอยู่กับความซับซ้อนของวิธีการและข้อมูลที่ใช้ ถ้ามันส่งผลกระทบต่อสมาชิกภายในและคุณตัดสินใจที่จะทำงานในสถานะเธรด / มอบหมายคุณจะได้รับบาดเจ็บมากมาย
Mickey Perlstein

0

เดินสายกิจกรรมทั้งหมดของคุณในการก่อสร้างและปล่อยให้พวกเขาอยู่คนเดียว การออกแบบคลาส Delegate ไม่สามารถจัดการการใช้งานอื่น ๆ ได้อย่างถูกต้องเนื่องจากฉันจะอธิบายในย่อหน้าสุดท้ายของโพสต์นี้

ก่อนอื่นไม่มีประเด็นใดในการพยายามที่จะสกัดกั้นการแจ้งเตือนเหตุการณ์เมื่อตัวจัดการเหตุการณ์ของคุณต้องทำการตัดสินใจให้ตรงกันว่าจะตอบสนองต่อการแจ้งเตือนหรือไม่อย่างไร

สิ่งที่อาจได้รับแจ้งควรได้รับการแจ้งเตือน หากตัวจัดการเหตุการณ์ของคุณจัดการกับการแจ้งเตือนอย่างถูกต้อง (เช่นพวกเขาสามารถเข้าถึงสถานะแอปพลิเคชันที่มีสิทธิ์และตอบสนองเมื่อเหมาะสมเท่านั้น) จากนั้นจะเป็นการดีที่จะแจ้งให้พวกเขาทราบเมื่อใดก็ได้

ครั้งเดียวที่ผู้ดำเนินการไม่ควรได้รับแจ้งว่ามีเหตุการณ์เกิดขึ้นคือถ้าเหตุการณ์จริงไม่ได้เกิดขึ้น! ดังนั้นหากคุณไม่ต้องการให้ตัวจัดการแจ้งให้หยุดสร้างกิจกรรม (เช่นปิดการใช้งานการควบคุมหรืออะไรก็ตามที่รับผิดชอบในการตรวจจับและนำเหตุการณ์เข้าสู่การดำรงอยู่ตั้งแต่แรก)

สุจริตฉันคิดว่าชั้นผู้แทนจะไม่สามารถกู้คืนได้ การควบรวม / การเปลี่ยนเป็น MulticastDelegate นั้นเป็นความผิดพลาดครั้งใหญ่เนื่องจากมันเปลี่ยนนิยาม (ประโยชน์) ของเหตุการณ์จากสิ่งที่เกิดขึ้นในช่วงเวลาเดียวทันทีเป็นสิ่งที่เกิดขึ้นในช่วงเวลาหนึ่ง การเปลี่ยนแปลงดังกล่าวจำเป็นต้องมีกลไกการซิงโครไนซ์ที่สามารถยุบลงในตรรกะในทันทีทันใด แต่ MulticastDelegate ขาดกลไกดังกล่าว การซิงโครไนซ์ควรรวมช่วงเวลาทั้งหมดหรือเหตุการณ์ที่เกิดขึ้นทันทีดังนั้นเมื่อแอปพลิเคชันทำการตัดสินใจที่ซิงโครไนซ์เพื่อเริ่มจัดการเหตุการณ์มันจะเสร็จสิ้นการจัดการโดยสมบูรณ์ (ธุรกรรม) ด้วยกล่องดำที่เป็นคลาสไฮบริด MulticastDelegate / Delegate นี่เป็นไปไม่ได้ดังนั้นยึดมั่นในการใช้-สมาชิกเพียงครั้งเดียวและ / หรือใช้ชนิดของคุณเอง MulticastDelegate ที่มีที่จับประสานที่สามารถนำมาออกในขณะที่ห่วงโซ่จัดการจะถูกใช้ ฉันแนะนำสิ่งนี้เพราะทางเลือกอื่นคือการใช้การซิงโครไนซ์ / การทำธุรกรรมความสมบูรณ์แบบซ้ำซ้อนในตัวจัดการทั้งหมดของคุณซึ่งจะยุ่งยาก / ซับซ้อนโดยไม่จำเป็น


[1] ไม่มีตัวจัดการเหตุการณ์ที่เป็นประโยชน์ที่เกิดขึ้นที่ "ทันเวลาทันที" การดำเนินการทั้งหมดมีช่วงเวลา ตัวจัดการเดี่ยวใด ๆ สามารถมีลำดับขั้นตอนที่ไม่ยุ่งยากในการดำเนินการ การสนับสนุนรายการตัวจัดการไม่มีอะไรเปลี่ยนแปลง
Daniel Earwicker

[2] การล็อคกุญแจขณะที่เหตุการณ์ถูกยิงก็คือความบ้าคลั่งทั้งหมด มันนำไปสู่การหยุดชะงักอย่างหลีกเลี่ยงไม่ แหล่งที่มาจะออกล็อค A, เหตุการณ์ไฟอ่างล้างจานจะออกล็อค B ตอนนี้ล็อคสองจะถูกจัดขึ้น เกิดอะไรขึ้นถ้าการดำเนินการบางอย่างในเธรดอื่นส่งผลให้เกิดการล็อกที่ดำเนินการในลำดับย้อนกลับ ชุดค่าผสมที่ร้ายแรงดังกล่าวจะถูกตัดออกได้อย่างไรเมื่อความรับผิดชอบในการล็อคถูกแบ่งระหว่างส่วนประกอบที่ออกแบบ / ทดสอบแยกต่างหาก (ซึ่งเป็นจุดรวมของเหตุการณ์)
Daniel Earwicker

[3] ไม่มีปัญหาใด ๆ เหล่านี้ในทางใดทางหนึ่งเลยที่จะลดความได้เปรียบที่แพร่หลายของผู้รับมอบหมายมัลติคาสต์ / เหตุการณ์ในองค์ประกอบของเธรดเดียวโดยเฉพาะในกรอบ GUI กรณีการใช้งานนี้ครอบคลุมการใช้งานส่วนใหญ่ของเหตุการณ์ การใช้เหตุการณ์ในลักษณะที่ไม่มีเธรดนั้นมีค่าที่น่าสงสัย สิ่งนี้ไม่ได้ทำให้การออกแบบของพวกเขาเป็นโมฆะหรือมีประโยชน์ชัดเจนในบริบทที่พวกเขาเข้าท่า
Daniel Earwicker

[4] เธรด + เหตุการณ์แบบซิงโครนัสนั้นเป็นปลาเฮอริ่งแดง การสื่อสารแบบอะซิงโครนัสที่อยู่ในคิวเป็นวิธีที่จะไป
Daniel Earwicker

[1] ฉันไม่ได้หมายถึงเวลาที่วัดได้ ... ฉันกำลังพูดถึงการปฏิบัติการปรมาณูซึ่งเกิดขึ้นอย่างเป็นเหตุเป็นผลทันที ... และโดยที่ฉันหมายถึงไม่มีอะไรเกี่ยวข้องกับทรัพยากรเดียวกันที่พวกเขากำลังใช้อยู่สามารถเปลี่ยนแปลงได้ในขณะที่เหตุการณ์กำลังเกิดขึ้น เพราะมันต่อเนื่องกับล็อค
Triynko

0

โปรดดูที่นี่: http://www.danielfortunov.com/software/%24daniel_uckyovs_adventures_in_software_development/2009/04/23/net_event_invocation_thread_safety นี่เป็นวิธีแก้ไขที่ถูกต้องและควรใช้แทนการแก้ไขปัญหาอื่น ๆ ทั้งหมด

“ คุณสามารถตรวจสอบให้แน่ใจว่ารายการการเรียกใช้ภายในมีสมาชิกอย่างน้อยหนึ่งคนเสมอโดยการเริ่มต้นด้วยวิธีที่ไม่ระบุชื่อ เนื่องจากไม่มีบุคคลภายนอกสามารถอ้างอิงถึงวิธีการที่ไม่ระบุชื่อจึงไม่มีบุคคลภายนอกใดสามารถลบวิธีได้ดังนั้นผู้รับมอบสิทธิ์จะไม่เป็นโมฆะ” - Programming .NET Components, 2nd Edition โดย Juval Löwy

public static event EventHandler<EventArgs> PreInitializedEvent = delegate { };  

public static void OnPreInitializedEvent(EventArgs e)  
{  
    // No check required - event will never be null because  
    // we have subscribed an empty anonymous delegate which  
    // can never be unsubscribed. (But causes some overhead.)  
    PreInitializedEvent(null, e);  
}  

0

ฉันไม่เชื่อว่าคำถามนี้ถูก จำกัด ประเภท c # "event" เอาข้อ จำกัด นั้นออกทำไมไม่ลองคิดค้นวงล้อใหม่ซักหน่อยแล้วทำบางอย่างตามเส้นเหล่านี้?

เพิ่มหัวข้อกิจกรรมอย่างปลอดภัย - แนวทางปฏิบัติที่ดีที่สุด

  • ความสามารถในการย่อย / ยกเลิกการสมัครจากกระทู้ใด ๆ ในขณะที่อยู่ในช่วงเพิ่ม (ลบเงื่อนไขการแข่งขัน)
  • โอเปอเรเตอร์โอเวอร์โหลดสำหรับ + ​​= และ - = ที่ระดับคลาส
  • ผู้รับมอบสิทธิ์ที่กำหนดโดยผู้โทรทั่วไป

0

ขอบคุณสำหรับการอภิปรายที่มีประโยชน์ ฉันทำงานเกี่ยวกับปัญหานี้เมื่อเร็ว ๆ นี้และทำให้คลาสต่อไปนี้ซึ่งช้าลงเล็กน้อย แต่อนุญาตให้หลีกเลี่ยงการเรียกไปยังวัตถุที่ถูกกำจัด

จุดหลักที่นี่คือรายการที่สามารถแก้ไขได้แม้เหตุการณ์จะเพิ่มขึ้น

/// <summary>
/// Thread safe event invoker
/// </summary>
public sealed class ThreadSafeEventInvoker
{
    /// <summary>
    /// Dictionary of delegates
    /// </summary>
    readonly ConcurrentDictionary<Delegate, DelegateHolder> delegates = new ConcurrentDictionary<Delegate, DelegateHolder>();

    /// <summary>
    /// List of delegates to be called, we need it because it is relatevely easy to implement a loop with list
    /// modification inside of it
    /// </summary>
    readonly LinkedList<DelegateHolder> delegatesList = new LinkedList<DelegateHolder>();

    /// <summary>
    /// locker for delegates list
    /// </summary>
    private readonly ReaderWriterLockSlim listLocker = new ReaderWriterLockSlim();

    /// <summary>
    /// Add delegate to list
    /// </summary>
    /// <param name="value"></param>
    public void Add(Delegate value)
    {
        var holder = new DelegateHolder(value);
        if (!delegates.TryAdd(value, holder)) return;

        listLocker.EnterWriteLock();
        delegatesList.AddLast(holder);
        listLocker.ExitWriteLock();
    }

    /// <summary>
    /// Remove delegate from list
    /// </summary>
    /// <param name="value"></param>
    public void Remove(Delegate value)
    {
        DelegateHolder holder;
        if (!delegates.TryRemove(value, out holder)) return;

        Monitor.Enter(holder);
        holder.IsDeleted = true;
        Monitor.Exit(holder);
    }

    /// <summary>
    /// Raise an event
    /// </summary>
    /// <param name="args"></param>
    public void Raise(params object[] args)
    {
        DelegateHolder holder = null;

        try
        {
            // get root element
            listLocker.EnterReadLock();
            var cursor = delegatesList.First;
            listLocker.ExitReadLock();

            while (cursor != null)
            {
                // get its value and a next node
                listLocker.EnterReadLock();
                holder = cursor.Value;
                var next = cursor.Next;
                listLocker.ExitReadLock();

                // lock holder and invoke if it is not removed
                Monitor.Enter(holder);
                if (!holder.IsDeleted)
                    holder.Action.DynamicInvoke(args);
                else if (!holder.IsDeletedFromList)
                {
                    listLocker.EnterWriteLock();
                    delegatesList.Remove(cursor);
                    holder.IsDeletedFromList = true;
                    listLocker.ExitWriteLock();
                }
                Monitor.Exit(holder);

                cursor = next;
            }
        }
        catch
        {
            // clean up
            if (listLocker.IsReadLockHeld)
                listLocker.ExitReadLock();
            if (listLocker.IsWriteLockHeld)
                listLocker.ExitWriteLock();
            if (holder != null && Monitor.IsEntered(holder))
                Monitor.Exit(holder);

            throw;
        }
    }

    /// <summary>
    /// helper class
    /// </summary>
    class DelegateHolder
    {
        /// <summary>
        /// delegate to call
        /// </summary>
        public Delegate Action { get; private set; }

        /// <summary>
        /// flag shows if this delegate removed from list of calls
        /// </summary>
        public bool IsDeleted { get; set; }

        /// <summary>
        /// flag shows if this instance was removed from all lists
        /// </summary>
        public bool IsDeletedFromList { get; set; }

        /// <summary>
        /// Constuctor
        /// </summary>
        /// <param name="d"></param>
        public DelegateHolder(Delegate d)
        {
            Action = d;
        }
    }
}

และการใช้งานคือ:

    private readonly ThreadSafeEventInvoker someEventWrapper = new ThreadSafeEventInvoker();
    public event Action SomeEvent
    {
        add { someEventWrapper.Add(value); }
        remove { someEventWrapper.Remove(value); }
    }

    public void RaiseSomeEvent()
    {
        someEventWrapper.Raise();
    }

ทดสอบ

ฉันทดสอบในลักษณะดังต่อไปนี้ ฉันมีเธรดที่สร้างและทำลายวัตถุเช่นนี้:

var objects = Enumerable.Range(0, 1000).Select(x => new Bar(foo)).ToList();
Thread.Sleep(10);
objects.ForEach(x => x.Dispose());

ในตัวสร้างBar(วัตถุผู้ฟัง) ที่ฉันสมัครSomeEvent(ซึ่งมีการใช้งานตามที่แสดงด้านบน) และยกเลิกการสมัครในDispose:

    public Bar(Foo foo)
    {
        this.foo = foo;
        foo.SomeEvent += Handler;
    }

    public void Handler()
    {
        if (disposed)
            Console.WriteLine("Handler is called after object was disposed!");
    }

    public void Dispose()
    {
        foo.SomeEvent -= Handler;
        disposed = true;
    }

นอกจากนี้ฉันยังมีเธรดที่เพิ่มเหตุการณ์ในลูป

การกระทำทั้งหมดเหล่านี้จะเกิดขึ้นพร้อมกัน: ผู้ฟังจำนวนมากถูกสร้างและทำลายและเหตุการณ์ถูกยิงในเวลาเดียวกัน

หากมีสภาพการแข่งขันฉันควรเห็นข้อความในคอนโซล แต่มันว่างเปล่า แต่ถ้าฉันใช้เหตุการณ์ clr ตามปกติฉันเห็นว่าเต็มไปด้วยข้อความเตือน ดังนั้นฉันสามารถสรุปได้ว่าเป็นไปได้ที่จะใช้เธรดเหตุการณ์ที่ปลอดภัยใน c #

คุณคิดอย่างไร?


ดูดีพอสำหรับฉัน แม้ว่าฉันคิดว่ามันอาจเป็นไปได้ (ในทางทฤษฎี) ที่disposed = trueจะเกิดขึ้นก่อนหน้านี้foo.SomeEvent -= Handlerในใบสมัครทดสอบของคุณ แต่สร้างผลบวกที่ผิดพลาด แต่นอกเหนือจากนั้นมีบางสิ่งที่คุณอาจต้องการเปลี่ยนแปลง คุณต้องการใช้try ... finallyสำหรับการล็อกจริงๆ- สิ่งนี้จะช่วยให้คุณทำสิ่งนี้ไม่เพียง แต่ปลอดภัยต่อเธรด แต่ยังยกเลิกการใช้งานด้วย ไม่ต้องพูดถึงว่าคุณสามารถกำจัดความงี่เง่าtry catchนั้นได้ และคุณไม่ได้ตรวจสอบผู้รับมอบสิทธิ์ผ่านAdd/ Remove- อาจเป็นได้null(คุณควรทิ้งในAdd/ Remove)
Luaan
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.