เหตุใดและวิธีการหลีกเลี่ยงการรั่วไหลของหน่วยความจำตัวจัดการเหตุการณ์?


154

ฉันเพิ่งรู้เมื่ออ่านคำถามและคำตอบใน StackOverflow ว่าการเพิ่มตัวจัดการเหตุการณ์ที่ใช้+=ใน C # (หรือฉันเดาภาษา. net อื่น ๆ ) อาจทำให้หน่วยความจำรั่วทั่วไป ...

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

ทำงานอย่างไร (หมายถึงเหตุใดจึงทำให้หน่วยความจำรั่ว)
ฉันจะแก้ไขปัญหานี้ได้อย่างไร ใช้-=ตัวจัดการเหตุการณ์เดียวกันเพียงพอหรือไม่
มีรูปแบบการออกแบบทั่วไปหรือแนวทางปฏิบัติที่ดีที่สุดสำหรับการจัดการสถานการณ์เช่นนี้หรือไม่?
ตัวอย่าง: ฉันควรจัดการแอปพลิเคชันที่มีหลายเธรดต่างกันอย่างไรโดยใช้ตัวจัดการเหตุการณ์ต่าง ๆ เพื่อเพิ่มหลายเหตุการณ์บน UI

มีวิธีที่ดีและเรียบง่ายในการตรวจสอบนี้อย่างมีประสิทธิภาพในแอปพลิเคชันขนาดใหญ่ที่สร้างขึ้นแล้วหรือไม่?

คำตอบ:


188

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

หากผู้เผยแพร่มีอายุการใช้งานนานกว่าผู้สมัครสมาชิกจะทำให้ผู้สมัครใช้งานยังคงอยู่แม้ว่าจะไม่มีการอ้างอิงอื่น ๆ ไปยังผู้สมัครสมาชิกก็ตาม

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

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


... ฉันเคยเห็นบางคนเขียนถึงคำตอบของคำถามเช่น "หน่วยความจำรั่วใน. net" คืออะไร
gillyb

32
วิธีที่จะหลีกเลี่ยงปัญหานี้จากทางผู้จัดพิมพ์คือการตั้งค่ากิจกรรมให้เป็นโมฆะเมื่อคุณแน่ใจว่าคุณจะไม่ดำเนินการใด ๆ อีกต่อไป สิ่งนี้จะลบสมาชิกทั้งหมดโดยนัยและอาจเป็นประโยชน์เมื่อเหตุการณ์บางอย่างเกิดขึ้นในช่วงระยะเวลาหนึ่งของอายุการใช้งานของวัตถุเท่านั้น
JSB ձոգչ

2
วิธี Dipose จะเป็นช่วงเวลาที่ดีสำหรับการตั้งค่าเหตุการณ์ให้เป็นโมฆะ
Davi Fiamenghi

6
@DaviFiamenghi: ถ้ามีบางอย่างที่ถูกกำจัดนั่นเป็นข้อบ่งชี้อย่างน้อยว่ามันจะมีสิทธิ์ได้รับการเก็บขยะเร็ว ๆ นี้ ณ จุดนี้มันไม่สำคัญว่าสมาชิกจะเป็นใคร
Jon Skeet

1
@ BrainSlugs83: "และรูปแบบการจัดกิจกรรมโดยทั่วไปรวมถึงผู้ส่งอยู่แล้ว" - แต่ก็ใช่ว่าเป็นกรณีที่ผู้ผลิต โดยทั่วไปแล้วอินสแตนซ์สมาชิกของเหตุการณ์นั้นมีความเกี่ยวข้องและผู้ส่งไม่ได้ ใช่ถ้าคุณสามารถสมัครสมาชิกโดยใช้วิธีการคงที่นี่ไม่ใช่ปัญหา - แต่นั่นไม่ค่อยเป็นตัวเลือกในประสบการณ์ของฉัน
Jon Skeet

13

ใช่-=เพียงพอ แต่อาจเป็นเรื่องยากที่จะติดตามทุกเหตุการณ์ที่ได้รับมอบหมาย (สำหรับรายละเอียดดูโพสต์ของ Jon) เกี่ยวกับรูปแบบการออกแบบที่มีลักษณะที่รูปแบบการจัดกิจกรรมที่อ่อนแอ


1
msdn.microsoft.com/en-us/library/aa970850(v=vs.100).aspxเวอร์ชั่น 4.0 ยังมีอยู่
Femaref

หากฉันรู้ว่าผู้เผยแพร่จะมีอายุยืนกว่าผู้สมัครสมาชิกฉันจะทำให้ผู้สมัครเป็นสมาชิกIDisposableและยกเลิกการสมัครรับข่าวสารจากเหตุการณ์
Shimmy Weitzhandler

10

ฉันได้อธิบายความสับสนนี้ในบล็อกที่https://www.spicelogic.com/Blog/net-event-handler-memory-leak-16 ฉันจะพยายามสรุปที่นี่เพื่อให้คุณสามารถมีความคิดที่ชัดเจน

หมายถึงการอ้างอิง "ต้องการ":

ก่อนอื่นคุณต้องเข้าใจว่าถ้าวัตถุ A เก็บการอ้างอิงไปยังวัตถุ B ดังนั้นมันจะหมายถึงวัตถุ A ต้องการให้วัตถุ B ทำงานใช่ไหม? ดังนั้นตัวรวบรวมขยะจะไม่รวบรวมวัตถุ B ตราบใดที่วัตถุ A ยังมีชีวิตอยู่ในหน่วยความจำ

ฉันคิดว่าส่วนนี้ควรชัดเจนสำหรับนักพัฒนา

+ = หมายถึงการฉีดการอ้างอิงของวัตถุด้านขวาไปยังวัตถุด้านซ้าย:

แต่ความสับสนนั้นมาจากโอเปอร์เรเตอร์ C # + = โอเปอเรเตอร์นี้ไม่ได้บอกผู้พัฒนาอย่างชัดเจนว่าทางด้านขวาของโอเปอเรเตอร์นี้กำลังทำการฉีดการอ้างอิงไปยังวัตถุทางซ้ายมือ

ป้อนคำอธิบายรูปภาพที่นี่

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

ป้อนคำอธิบายรูปภาพที่นี่

คุณสามารถหลีกเลี่ยงการรั่วไหลดังกล่าวได้โดยถอดตัวจัดการเหตุการณ์

จะตัดสินใจได้อย่างไร?

แต่มีเหตุการณ์และตัวจัดการเหตุการณ์จำนวนมากในฐานรหัสทั้งหมดของคุณ หมายความว่าคุณต้องทำการแยกตัวจัดการเหตุการณ์ออกทุกที่หรือไม่ คำตอบคือไม่ถ้าคุณต้องทำเช่นนั้น codebase ของคุณจะน่าเกลียดด้วย verbose

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

ป้อนคำอธิบายรูปภาพที่นี่

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

ตัวอย่างของสถานการณ์ที่คุณไม่ต้องกังวล

ตัวอย่างเช่นเหตุการณ์คลิกปุ่มของหน้าต่าง

ป้อนคำอธิบายรูปภาพที่นี่

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

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

ฉันจะให้ตัวอย่างหนึ่งที่วัตถุสมาชิกควรจะตายก่อนวัตถุผู้เผยแพร่ สมมติว่า MainWindow ของคุณเผยแพร่กิจกรรมชื่อ "SomethingHappened" และคุณแสดงหน้าต่างลูกจากหน้าต่างหลักด้วยการคลิกปุ่ม หน้าต่างลูกสมัครสมาชิกเหตุการณ์นั้นของหน้าต่างหลัก

ป้อนคำอธิบายรูปภาพที่นี่

และหน้าต่างลูกสมัครรับข้อมูลเหตุการณ์ของหน้าต่างหลัก

ป้อนคำอธิบายรูปภาพที่นี่

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

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

กฎง่ายๆ:ถ้ามุมมองของคุณ (เช่น WPF, WinForm, UWP, แบบฟอร์ม Xamarin เป็นต้น) สมัครรับข้อมูลเหตุการณ์ของ ViewModel อย่าลืมถอดตัวจัดการเหตุการณ์ออก เพราะโดยปกติแล้ว ViewModel จะใช้งานได้ยาวนานกว่ามุมมอง ดังนั้นหาก ViewModel ไม่ถูกทำลายมุมมองใด ๆ ที่เหตุการณ์ที่สมัครเป็นสมาชิกของ ViewModel นั้นจะยังคงอยู่ในหน่วยความจำซึ่งไม่ดี

พิสูจน์แนวคิดโดยใช้หน่วยความจำ profiler

มันจะไม่สนุกถ้าเราไม่สามารถตรวจสอบแนวคิดกับ profiler หน่วยความจำ ฉันใช้ JetBrain dotMemory profiler ในการทดลองนี้

ก่อนอื่นฉันเรียกใช้ MainWindow ซึ่งแสดงดังนี้:

ป้อนคำอธิบายรูปภาพที่นี่

จากนั้นฉันก็ถ่ายภาพหน่วยความจำ แล้วฉันคลิกปุ่ม3 ครั้ง หน้าต่างลูกสามลูกปรากฏขึ้น ฉันปิดหน้าต่างลูกเหล่านั้นทั้งหมดแล้วคลิกปุ่ม Force GC ในตัวสร้างโปรไฟล์ dotMemory เพื่อให้แน่ใจว่ามีการเรียก Garbage Collector จากนั้นฉันใช้สแนปชอตหน่วยความจำอื่นแล้วเปรียบเทียบ ดูเถิด! ความกลัวของเราเป็นจริง หน้าต่างลูกไม่ได้ถูกรวบรวมโดยตัวรวบรวมข้อมูลขยะแม้ว่าจะปิดไปแล้วก็ตาม ไม่เพียงแค่นั้น แต่จำนวนวัตถุที่รั่วไหลออกมาสำหรับวัตถุ ChildWindow ก็แสดงเป็น " 3 " ด้วย (ฉันคลิกปุ่ม 3 ครั้งเพื่อแสดงหน้าต่างลูก 3 ลูก)

ป้อนคำอธิบายรูปภาพที่นี่

ตกลงจากนั้นฉันแยกตัวจัดการเหตุการณ์ตามที่แสดงด้านล่าง

ป้อนคำอธิบายรูปภาพที่นี่

จากนั้นฉันได้ทำตามขั้นตอนเดียวกันและตรวจสอบตัวสร้างหน่วยความจำ คราวนี้ว้าว! ไม่มีการรั่วไหลของหน่วยความจำเพิ่มเติม

ป้อนคำอธิบายรูปภาพที่นี่


3

เหตุการณ์เป็นรายการที่เชื่อมโยงกันของตัวจัดการเหตุการณ์

เมื่อคุณ + + EventHandler ใหม่ในเหตุการณ์มันไม่สำคัญว่าถ้าฟังก์ชั่นนี้ได้รับการเพิ่มเป็นผู้ฟังมาก่อนมันจะได้รับการเพิ่มหนึ่งครั้งต่อ + =

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

ดูที่นี่

และMSDN ที่นี่


โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.