การ“ ใช้” กับทรัพยากรมากกว่าหนึ่งรายการทำให้ทรัพยากรรั่วไหลได้หรือไม่


106

C # ให้ฉันทำสิ่งต่อไปนี้ (ตัวอย่างจาก MSDN):

using (Font font3 = new Font("Arial", 10.0f),
            font4 = new Font("Arial", 10.0f))
{
    // Use font3 and font4.
}

จะเกิดอะไรขึ้นถ้าfont4 = new Fontพ่น? จากสิ่งที่ฉันเข้าใจว่า font3 จะทำให้ทรัพยากรรั่วไหลและจะไม่ถูกกำจัดทิ้ง

  • นี่คือเรื่องจริง? (font4 จะไม่ถูกกำจัดทิ้ง)
  • หมายความว่าusing(... , ...)ควรหลีกเลี่ยงโดยสิ้นเชิงเพื่อสนับสนุนการใช้ซ้อนกันหรือไม่?

7
หน่วยความจำจะไม่รั่วไหล ในกรณีที่เลวร้ายที่สุดก็ยังคงได้รับ GC
SLaks

3
ฉันจะไม่แปลกใจถ้าusing(... , ...)คอมไพล์เป็นบล็อกโดยไม่คำนึงถึง แต่ฉันไม่รู้อย่างแน่นอน
แดน J

1
นั่นไม่ใช่สิ่งที่ฉันหมายถึง แม้ว่าคุณจะไม่ได้ใช้งานusingเลย แต่ในที่สุด GC ก็ยังคงรวบรวมมัน
SLaks

1
@zneak: หากคอมไพล์เป็นfinallyบล็อกเดียวมันจะไม่เข้าสู่บล็อกจนกว่าทรัพยากรทั้งหมดจะถูกสร้างขึ้น
SLaks

2
@zneak: เนื่องจากในการแปลง a usingเป็นtry- finallyนิพจน์การเริ่มต้นจะถูกประเมินภายนอกไฟล์try. ดังนั้นจึงเป็นคำถามที่สมเหตุสมผล
Ben Voigt

คำตอบ:


158

ไม่

คอมไพเลอร์จะสร้างfinallyบล็อกแยกต่างหากสำหรับตัวแปรแต่ละตัว

ข้อมูลจำเพาะ (§8.13) พูดว่า:

เมื่อการได้มาซึ่งทรัพยากรอยู่ในรูปแบบของการประกาศตัวแปรท้องถิ่นคุณสามารถได้รับทรัพยากรหลายประเภทที่กำหนด usingคำสั่งของแบบฟอร์ม

using (ResourceType r1 = e1, r2 = e2, ..., rN = eN) statement 

แม่นยำเทียบเท่ากับลำดับของการซ้อนโดยใช้คำสั่ง:

using (ResourceType r1 = e1)
   using (ResourceType r2 = e2)
      ...
         using (ResourceType rN = eN)
            statement

4
นั่นคือ 8.13 ใน C # Specification เวอร์ชัน 5.0, btw
Ben Voigt

11
@WeylandYutani: ถามอะไร?
SLaks

9
@WeylandYutani: นี่คือไซต์ถาม - ตอบ หากคุณมีคำถามโปรดเริ่มคำถามใหม่!
Eric Lippert

5
@ user1306322 ทำไม? ถ้าฉันอยากรู้จริงๆล่ะ?
Oxymoron

2
@Oxymoron คุณควรแสดงหลักฐานของความพยายามก่อนที่จะโพสต์คำถามในรูปแบบของการวิจัยและการคาดเดาหรือมิฉะนั้นคุณจะถูกบอกแบบเดียวกันสูญเสียความสนใจและมิฉะนั้นจะสูญเสียมากขึ้น เพียงคำแนะนำจากประสบการณ์ส่วนตัว
user1306322

67

UPDATE : ผมใช้คำถามนี้เป็นพื้นฐานสำหรับบทความซึ่งสามารถพบได้นั้นที่นี่ ; ดูการอภิปรายเพิ่มเติมเกี่ยวกับปัญหานี้ ขอบคุณสำหรับคำถามดีๆ!


แม้ว่าคำตอบของ Schabseจะถูกต้องและตอบคำถามที่ถาม แต่ก็มีตัวแปรสำคัญสำหรับคำถามของคุณที่คุณไม่ได้ถาม:

เกิดอะไรขึ้นถ้าfont4 = new Font()พ่นหลังจากทรัพยากรที่ไม่มีการจัดการได้รับการจัดสรรโดยสร้าง แต่ก่อนที่จะส่งกลับ ctor และเติมในfont4ที่มีการอ้างอิงหรือไม่

ขอฉันทำให้ชัดเจนขึ้นอีกนิด สมมติว่าเรามี:

public sealed class Foo : IDisposable
{
    private int handle = 0;
    private bool disposed = false;
    public Foo()
    {
        Blah1();
        int x = AllocateResource();
        Blah2();
        this.handle = x;
        Blah3();
    }
    ~Foo()
    {
        Dispose(false);
    }
    public void Dispose() 
    { 
        Dispose(true); 
        GC.SuppressFinalize(this);
    }
    private void Dispose(bool disposing)
    {
        if (!this.disposed)
        {
            if (this.handle != 0) 
                DeallocateResource(this.handle);
            this.handle = 0;
            this.disposed = true;
        }
    }
}

ตอนนี้เรามี

using(Foo foo = new Foo())
    Whatever(foo);

เช่นเดียวกับ

{
    Foo foo = new Foo();
    try
    {
        Whatever(foo);
    }
    finally
    {
        IDisposable d = foo as IDisposable;
        if (d != null) 
            d.Dispose();
    }
}

ตกลง. สมมติว่าWhateverพ่น จากนั้นfinallyบล็อกจะทำงานและทรัพยากรจะถูกยกเลิกการจัดสรร ไม่มีปัญหา.

สมมติว่าBlah1()พ่น จากนั้นการโยนจะเกิดขึ้นก่อนที่ทรัพยากรจะถูกจัดสรร ออบเจ็กต์ได้รับการจัดสรรแล้ว แต่ ctor ไม่เคยส่งกลับดังนั้นจึงfooไม่ถูกกรอกเราไม่เคยป้อนtryดังนั้นเราจึงไม่เคยป้อนไฟล์finally. การอ้างอิงอ็อบเจ็กต์ถูกละเลย ในที่สุด GC จะค้นพบและวางไว้ในคิว Finalizer handleยังคงเป็นศูนย์ดังนั้น Finalizer จึงไม่ทำอะไรเลย ขอให้สังเกตว่า finalizer จะต้องมีประสิทธิภาพในใบหน้าของวัตถุที่มีการสรุปที่มีตัวสร้างไม่เสร็จ คุณจะต้องเขียน Finalizers ที่มีความแข็งแกร่งนี้ นี่เป็นอีกเหตุผลหนึ่งที่คุณควรเขียนบทสรุปให้กับผู้เชี่ยวชาญและอย่าพยายามทำด้วยตัวเอง

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

ตอนนี้สมมติว่าBlah2()พ่น การโยนเกิดขึ้นหลังจากจัดสรรทรัพยากร แต่ก่อน handleจะเต็ม! อีกครั้ง Finalizer จะทำงาน แต่ตอนนี้handleยังคงเป็นศูนย์และเรารั่วที่จับ!

คุณต้องเขียนโค้ดที่ฉลาดมากเพื่อป้องกันการรั่วไหลนี้ ตอนนี้ในกรณีของFontทรัพยากรของคุณใครสนใจ? เรารั่วที่จับแบบอักษรเรื่องใหญ่ แต่ถ้าคุณต้องการให้ทรัพยากรที่ไม่มีการจัดการทุกอย่างสะอาดหมดจดไม่ว่าช่วงเวลาของข้อยกเว้นจะเป็นอย่างไรคุณก็มีปัญหาที่ยากมากในมือของคุณ

CLR ต้องแก้ปัญหานี้ด้วยการล็อก ตั้งแต่ C # 4 การล็อกที่ใช้lockคำสั่งได้รับการดำเนินการดังนี้:

bool lockEntered = false;
object lockObject = whatever;
try
{
    Monitor.Enter(lockObject, ref lockEntered);
    lock body here
}
finally
{
    if (lockEntered) Monitor.Exit(lockObject);
}

Enterได้รับการเขียนอย่างระมัดระวังเพื่อที่ว่าไม่ว่าสิ่งที่เป็นข้อยกเว้นจะโยน , lockEnteredกำหนดเป็นจริงถ้าหากล็อคถูกนำตัวจริง หากคุณมีข้อกำหนดที่คล้ายกันสิ่งที่คุณต้องเขียนคือ:

    public Foo()
    {
        Blah1();
        AllocateResource(ref handle);
        Blah2();
        Blah3();
    }

และเขียนAllocateResourceอย่างชาญฉลาดMonitor.Enterเพื่อที่ว่าไม่ว่าจะเกิดอะไรขึ้นภายในAllocateResourceสิ่งhandleนั้นจะถูกเติมในกรณีที่จำเป็นต้องถูกจัดสรรเท่านั้น

การอธิบายเทคนิคในการทำเช่นนั้นอยู่นอกเหนือขอบเขตของคำตอบนี้ ปรึกษาผู้เชี่ยวชาญหากคุณมีข้อกำหนดนี้


6
@gnat: คำตอบที่ยอมรับ S นั้นต้องยืนหยัดเพื่ออะไรบางอย่าง :-)
Eric Lippert

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

5
@ คริส: สมมติว่าไม่มีงานที่ทำระหว่างการจัดสรรและผลตอบแทนและระหว่างการส่งคืนและงานที่มอบหมาย เราลบBlahการเรียกใช้เมธอดเหล่านั้นทั้งหมด อะไรที่หยุด ThreadAbortException ไม่ให้เกิดขึ้นที่จุดใดจุดหนึ่ง
Eric Lippert

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

7
@GilesRoberts: วิธีนี้ช่วยแก้ปัญหาได้อย่างไร? สมมติว่าข้อยกเว้นที่เกิดขึ้นหลังจากการเรียกร้องให้AllocateResourceแต่ก่อนที่จะxมอบหมายให้ A ThreadAbortExceptionสามารถเกิดขึ้นได้ที่จุดนั้น ทุกคนที่นี่ดูเหมือนจะหายไปจุดของฉันซึ่งเป็นการสร้างทรัพยากรและการโอนการอ้างอิงไปยังตัวแปรไม่ได้เป็นงานอะตอม เพื่อแก้ปัญหาฉันได้ระบุว่าคุณต้องทำให้มันเป็นการดำเนินการของอะตอม
Eric Lippert

32

เพื่อเป็นส่วนเสริมของคำตอบ @SLaks นี่คือ IL สำหรับรหัสของคุณ:

.method private hidebysig static 
    void Main (
        string[] args
    ) cil managed 
{
    // Method begins at RVA 0x2050
    // Code size 74 (0x4a)
    .maxstack 2
    .entrypoint
    .locals init (
        [0] class [System.Drawing]System.Drawing.Font font3,
        [1] class [System.Drawing]System.Drawing.Font font4,
        [2] bool CS$4$0000
    )

    IL_0000: nop
    IL_0001: ldstr "Arial"
    IL_0006: ldc.r4 10
    IL_000b: newobj instance void [System.Drawing]System.Drawing.Font::.ctor(string, float32)
    IL_0010: stloc.0
    .try
    {
        IL_0011: ldstr "Arial"
        IL_0016: ldc.r4 10
        IL_001b: newobj instance void [System.Drawing]System.Drawing.Font::.ctor(string, float32)
        IL_0020: stloc.1
        .try
        {
            IL_0021: nop
            IL_0022: nop
            IL_0023: leave.s IL_0035
        } // end .try
        finally
        {
            IL_0025: ldloc.1
            IL_0026: ldnull
            IL_0027: ceq
            IL_0029: stloc.2
            IL_002a: ldloc.2
            IL_002b: brtrue.s IL_0034

            IL_002d: ldloc.1
            IL_002e: callvirt instance void [mscorlib]System.IDisposable::Dispose()
            IL_0033: nop

            IL_0034: endfinally
        } // end handler

        IL_0035: nop
        IL_0036: leave.s IL_0048
    } // end .try
    finally
    {
        IL_0038: ldloc.0
        IL_0039: ldnull
        IL_003a: ceq
        IL_003c: stloc.2
        IL_003d: ldloc.2
        IL_003e: brtrue.s IL_0047

        IL_0040: ldloc.0
        IL_0041: callvirt instance void [mscorlib]System.IDisposable::Dispose()
        IL_0046: nop

        IL_0047: endfinally
    } // end handler

    IL_0048: nop
    IL_0049: ret
} // end of method Program::Main

สังเกตการลอง / บล็อกที่ซ้อนกันในที่สุด


17

รหัสนี้ (ตามตัวอย่างต้นฉบับ):

using System.Drawing;

public class Class1
{
    public Class1()
    {
        using (Font font3 = new Font("Arial", 10.0f),
                    font4 = new Font("Arial", 10.0f))
        {
            // Use font3 and font4.
        }
    }
}

สร้างCILต่อไปนี้(ในVisual Studio 2013กำหนดเป้าหมาย. NET 4.5.1):

.method public hidebysig specialname rtspecialname
        instance void  .ctor() cil managed
{
    // Code size       82 (0x52)
    .maxstack  2
    .locals init ([0] class [System.Drawing]System.Drawing.Font font3,
                  [1] class [System.Drawing]System.Drawing.Font font4,
                  [2] bool CS$4$0000)
    IL_0000:  ldarg.0
    IL_0001:  call       instance void [mscorlib]System.Object::.ctor()
    IL_0006:  nop
    IL_0007:  nop
    IL_0008:  ldstr      "Arial"
    IL_000d:  ldc.r4     10.
    IL_0012:  newobj     instance void [System.Drawing]System.Drawing.Font::.ctor(string,
                                                                                  float32)
    IL_0017:  stloc.0
    .try
    {
        IL_0018:  ldstr      "Arial"
        IL_001d:  ldc.r4     10.
        IL_0022:  newobj     instance void [System.Drawing]System.Drawing.Font::.ctor(string,
                                                                                      float32)
        IL_0027:  stloc.1
        .try
        {
            IL_0028:  nop
            IL_0029:  nop
            IL_002a:  leave.s    IL_003c
        }  // end .try
        finally
        {
            IL_002c:  ldloc.1
            IL_002d:  ldnull
            IL_002e:  ceq
            IL_0030:  stloc.2
            IL_0031:  ldloc.2
            IL_0032:  brtrue.s   IL_003b
            IL_0034:  ldloc.1
            IL_0035:  callvirt   instance void [mscorlib]System.IDisposable::Dispose()
            IL_003a:  nop
            IL_003b:  endfinally
        }  // end handler
        IL_003c:  nop
        IL_003d:  leave.s    IL_004f
    }  // end .try
    finally
    {
        IL_003f:  ldloc.0
        IL_0040:  ldnull
        IL_0041:  ceq
        IL_0043:  stloc.2
        IL_0044:  ldloc.2
        IL_0045:  brtrue.s   IL_004e
        IL_0047:  ldloc.0
        IL_0048:  callvirt   instance void [mscorlib]System.IDisposable::Dispose()
        IL_004d:  nop
        IL_004e:  endfinally
    }  // end handler
    IL_004f:  nop
    IL_0050:  nop
    IL_0051:  ret
} // end of method Class1::.ctor

ในขณะที่คุณสามารถดูที่บล็อกไม่เริ่มจนกว่าจะได้รับการจัดสรรครั้งแรกซึ่งจะมีขึ้นที่try {} IL_0012เมื่อมองแวบแรกสิ่งนี้ดูเหมือนจะจัดสรรรายการแรกในรหัสที่ไม่มีการป้องกัน อย่างไรก็ตามโปรดสังเกตว่าผลลัพธ์ถูกเก็บไว้ในตำแหน่ง 0 หากการจัดสรรครั้งที่สองล้มเหลวบล็อกด้านนอก finally {}จะดำเนินการและสิ่งนี้จะดึงอ็อบเจ็กต์จากตำแหน่ง 0 นั่นคือการจัดสรรครั้งแรกของfont3และเรียกDispose()ใช้เมธอด

ที่น่าสนใจคือการถอดรหัสแอสเซมบลีนี้ด้วยdotPeekจะสร้างแหล่งที่สร้างขึ้นใหม่ดังต่อไปนี้:

using System.Drawing;

public class Class1
{
    public Class1()
    {
        using (new Font("Arial", 10f))
        {
            using (new Font("Arial", 10f))
                ;
        }
    }
}

รหัสถอดรหัสยืนยันว่าทุกอย่างถูกต้องและส่วนusingใหญ่ขยายเป็นusings ซ้อน รหัส CIL ดูสับสนเล็กน้อยและฉันต้องจ้องดูมันสักครู่ก่อนที่จะเข้าใจสิ่งที่เกิดขึ้นอย่างถูกต้องดังนั้นฉันจึงไม่แปลกใจที่ 'เรื่องเล่าเรื่องเมียเก่า' บางเรื่องเริ่มผุดขึ้นมา นี้. อย่างไรก็ตามรหัสที่สร้างขึ้นเป็นความจริงที่ไม่สามารถเข้าถึงได้


@Peter Mortensen การแก้ไขของคุณลบส่วนของรหัส IL (ระหว่าง IL_0012 และ IL_0017) แสดงคำอธิบายทั้งที่ไม่ถูกต้องและสับสน รหัสนั้นตั้งใจให้เป็นสำเนาคำต่อคำของผลลัพธ์ที่ฉันได้รับและการแก้ไขทำให้ไม่ถูกต้อง คุณช่วยตรวจสอบการแก้ไขของคุณและยืนยันได้ไหมว่านี่คือสิ่งที่คุณต้องการ
Tim Long

7

นี่คือโค้ดตัวอย่างเพื่อพิสูจน์คำตอบ @SLaks:

void Main()
{
    try
    {
        using (TestUsing t1 = new TestUsing("t1"), t2 = new TestUsing("t2"))
        {
        }
    }
    catch(Exception ex)
    {
        Console.WriteLine("catch");
    }
    finally
    {
        Console.WriteLine("done");
    }

    /* outputs

        Construct: t1
        Construct: t2
        Dispose: t1
        catch
        done

    */
}

public class TestUsing : IDisposable
{
    public string Name {get; set;}

    public TestUsing(string name)
    {
        Name = name;

        Console.WriteLine("Construct: " + Name);

        if (Name == "t2") throw new Exception();
    }

    public void Dispose()
    {
        Console.WriteLine("Dispose: " + Name);
    }
}

1
ที่พิสูจน์ไม่ได้ Dispose อยู่ที่ไหน: t2? :)
Piotr Perak

1
คำถามเกี่ยวกับการทิ้งทรัพยากรแรกในรายการที่ใช้ไม่ใช่ครั้งที่สอง "จะเกิดอะไรขึ้นถ้ามีการfont4 = new Fontขว้างจากสิ่งที่ฉันเข้าใจ font3 จะทำให้ทรัพยากรรั่วไหลและจะไม่ถูกกำจัดทิ้ง"
wdosanjos
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.