Dapatkah "menggunakan" dengan lebih dari satu sumber daya menyebabkan kebocoran sumber daya?

106

C # biarkan saya melakukan hal berikut (contoh dari MSDN):

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

Apa yang terjadi jika font4 = new Fontmelempar? Dari apa yang saya pahami font3 akan membocorkan sumber daya dan tidak akan dibuang.

  • Apakah ini benar? (font4 tidak akan dibuang)
  • Apakah ini berarti using(... , ...)harus dihindari sama sekali demi penggunaan bersarang?
Benjamin Gruenbaum
sumber
7
Itu tidak akan membocorkan memori; dalam kasus terburuk, itu masih akan mendapatkan GC.
SLaks
3
Saya tidak akan terkejut jika using(... , ...)dikompilasi menjadi bersarang menggunakan blok, tetapi saya tidak tahu pasti.
Dan J
1
Itu bukanlah apa yang saya maksud. Bahkan jika Anda tidak menggunakan usingsama sekali, GC akan tetap mengumpulkannya.
SLaks
1
@zneak: Seandainya dikompilasi menjadi satu finallyblok, itu tidak akan memasuki blok sampai semua sumber daya dibangun.
SLaks
2
@zneak: Karena dalam konversi a usingmenjadi try- finally, ekspresi inisialisasi dievaluasi di luar try. Jadi itu pertanyaan yang masuk akal.
Ben Voigt

Jawaban:

158

Tidak.

Kompilator akan membuat finallyblok terpisah untuk setiap variabel.

The spek (§8.13) mengatakan:

Ketika akuisisi sumber daya berbentuk deklarasi variabel lokal, dimungkinkan untuk memperoleh banyak sumber daya dari tipe tertentu. Sebuah usingpernyataan dari bentuk

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

persis sama dengan urutan pernyataan menggunakan bersarang:

using (ResourceType r1 = e1)
   using (ResourceType r2 = e2)
      ...
         using (ResourceType rN = eN)
            statement
SLaks
sumber
4
Itu 8,13 di C # Spesifikasi versi 5.0, btw.
Ben Voigt
11
@WeylandYutani: Apa yang Anda tanyakan?
SLaks
9
@WeylandYutani: Ini adalah situs tanya jawab. Jika Anda memiliki pertanyaan, mulailah pertanyaan baru!
Eric Lippert
5
@ user1306322 kenapa? Bagaimana jika saya benar-benar ingin tahu?
Oxymoron
2
@Oxymoron maka Anda harus memberikan beberapa bukti upaya sebelum memposting pertanyaan dalam bentuk penelitian dan tebakan, atau Anda akan diberitahu hal yang sama, kehilangan perhatian dan sebaliknya akan mengalami kerugian yang lebih besar. Hanya saran berdasarkan pengalaman pribadi.
pengguna1306322
67

PEMBARUAN : Saya menggunakan pertanyaan ini sebagai dasar untuk artikel yang dapat ditemukan di sini ; lihat untuk diskusi tambahan tentang masalah ini. Terima kasih atas pertanyaan yang bagus!


Meskipun jawaban Schabse tentu saja benar dan menjawab pertanyaan yang diajukan, ada varian penting dari pertanyaan Anda yang tidak Anda ajukan:

Apa yang terjadi jika font4 = new Font()lemparan setelah sumber daya yang tidak dikelola dialokasikan oleh konstruktor tetapi sebelum ctor kembali dan diisi font4dengan referensi?

Biarkan saya membuatnya sedikit lebih jelas. Misalkan kita memiliki:

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;
        }
    }
}

Sekarang kita punya

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

Ini sama dengan

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

BAIK. Misalkan Whatevermelempar. Kemudianfinally blok tersebut berjalan dan sumber daya dibatalkan alokasinya. Tidak masalah.

Misalkan Blah1()melempar. Kemudian lemparan terjadi sebelum sumber daya dialokasikan. Objek telah dialokasikan tetapi ctor tidak pernah kembali, jadi footidak pernah diisi. Kami tidak pernah masuk trysehingga kami tidak pernah memasukkan finallykeduanya. Referensi objek telah menjadi yatim piatu. Akhirnya GC akan menemukannya dan meletakkannya di antrean finalizer. handlemasih nol, jadi finalizer tidak melakukan apa pun. Perhatikan bahwa finalizer harus kuat dalam menghadapi objek yang sedang diselesaikan yang konstruktornya tidak pernah selesai . Anda dibutuhkan untuk menulis finalisator yang sekuat ini. Ini adalah alasan lain mengapa Anda harus menyerahkan tugas akhir penulisan kepada para ahli dan tidak mencoba melakukannya sendiri.

Misalkan Blah3()melempar. Lemparan terjadi setelah sumber daya dialokasikan. Tapi sekali lagi, footidak pernah diisi, kita tidak pernah masukfinally , dan objek dibersihkan oleh thread finalizer. Kali ini pegangannya bukan nol, dan finalizer membersihkannya. Sekali lagi, finalizer berjalan pada objek yang konstruktornya tidak pernah berhasil, tetapi finalizer tetap berjalan. Jelas harus karena kali ini, ada pekerjaan yang harus dilakukan.

Sekarang misalkan Blah2()lemparan. Lemparan terjadi setelah sumber daya dialokasikan tetapi sebelum handle diisi! Sekali lagi, finalizer akan berjalan tetapi sekarang handlemasih nol dan kami membocorkan pegangannya!

Anda perlu menulis kode yang sangat pintar untuk mencegah kebocoran ini terjadi. Sekarang, dalam kasus Fontsumber daya Anda , siapa yang peduli? Kami membocorkan pegangan font, masalah besar. Tetapi jika Anda benar-benar positif memerlukan bahwa setiap sumber daya unmanaged dibersihkan tidak peduli apa waktu pengecualian adalah maka Anda memiliki masalah yang sangat sulit di tangan Anda.

CLR harus menyelesaikan masalah ini dengan kunci. Sejak C # 4, kunci yang menggunakan lockpernyataan tersebut telah diimplementasikan seperti ini:

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

Entertelah ditulis dengan sangat hati-hati sehingga apa pun pengecualian yang dilemparkan , lockEntereddisetel ke true jika dan hanya jika kunci benar-benar diambil. Jika Anda memiliki persyaratan serupa maka yang perlu Anda lakukan sebenarnya adalah menulis:

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

dan tulis AllocateResourcedengan cerdik Monitor.Entersehingga apa pun yang terjadi di dalamnya AllocateResource, handleisinya jika dan hanya jika perlu dibatalkan alokasinya.

Menjelaskan teknik untuk melakukannya berada di luar cakupan jawaban ini. Konsultasikan dengan ahlinya jika Anda memiliki persyaratan ini.

Eric Lippert
sumber
6
@gnat: Jawaban yang diterima. S itu harus berarti sesuatu. :-)
Eric Lippert
12
@Joe: Tentu saja contoh dibuat - buat . Saya baru saja membuatnya . Risikonya tidak dibesar - besarkan karena saya belum menyebutkan berapa tingkat risikonya; sebaliknya, saya telah menyatakan bahwa pola ini mungkin . Fakta bahwa Anda yakin bahwa menyetel bidang secara langsung menyelesaikan masalah menunjukkan dengan tepat maksud saya: bahwa seperti sebagian besar pemrogram yang tidak memiliki pengalaman dengan masalah semacam ini, Anda tidak kompeten untuk memecahkan masalah ini; memang, kebanyakan orang bahkan tidak menyadari bahwa ada adalah masalah, yang mengapa saya menulis jawaban ini di tempat pertama .
Eric Lippert
5
@ Chris: Misalkan tidak ada pekerjaan yang dilakukan antara alokasi dan pengembalian, dan antara pengembalian dan penugasan. Kami menghapus semua Blahpanggilan metode tersebut. Apa yang menghentikan ThreadAbortException terjadi di salah satu titik tersebut?
Eric Lippert
5
@ Joe: Ini bukan masyarakat yang berdebat; Saya tidak ingin mencetak poin dengan menjadi lebih meyakinkan . Jika Anda skeptis dan tidak ingin mempercayai kata-kata saya bahwa ini adalah masalah rumit yang memerlukan konsultasi dengan para ahli untuk menyelesaikannya dengan benar, maka Anda boleh tidak setuju dengan saya.
Eric Lippert
7
@ GilesRoberts: Bagaimana cara mengatasi masalah? Misalkan pengecualian terjadi setelah panggilan ke AllocateResourcetetapi sebelum penugasan ke x. A ThreadAbortExceptionbisa terjadi pada saat itu. Semua orang di sini tampaknya kehilangan maksud saya, yaitu pembuatan sumber daya dan penugasan referensi ke variabel bukanlah operasi atom . Untuk memecahkan masalah yang saya identifikasi, Anda harus menjadikannya operasi atom.
Eric Lippert
32

Sebagai pelengkap jawaban @SLaks, berikut IL untuk kode Anda:

.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

Perhatikan blok percobaan / akhirnya yang bersarang.

David Heffernan
sumber
17

Kode ini (berdasarkan contoh asli):

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.
        }
    }
}

Ini menghasilkan CIL berikut ini (dalam Visual Studio 2013 , menargetkan .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

Seperti yang Anda lihat, try {}pemblokiran tidak dimulai sampai setelah alokasi pertama, yang berlangsung di IL_0012. Sekilas, ini tampaknya mengalokasikan item pertama dalam kode yang tidak dilindungi. Namun, perhatikan bahwa hasilnya disimpan di lokasi 0. Jika alokasi kedua kemudian gagal, blok luar finally {} dijalankan, dan ini mengambil objek dari lokasi 0, yaitu alokasi pertama font3, dan memanggil Dispose()metodenya.

Menariknya, menguraikan rakitan ini dengan dotPeek menghasilkan sumber yang disusun kembali berikut ini:

using System.Drawing;

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

Kode yang didekompilasi menegaskan bahwa semuanya benar dan pada usingdasarnya diperluas menjadi usings bersarang . Kode CIL agak membingungkan untuk dilihat, dan saya harus menatapnya selama beberapa menit sebelum saya benar-benar memahami apa yang terjadi, jadi saya tidak terkejut bahwa beberapa 'dongeng istri lama' mulai bermunculan. ini. Namun, kode yang dihasilkan adalah kebenaran yang tidak dapat disangkal.

Tim Long
sumber
@Peter Mortensen hasil edit Anda menghapus potongan kode IL (antara IL_0012 dan IL_0017) membuat penjelasan tidak valid dan membingungkan. Kode itu dimaksudkan sebagai salinan kata demi kata dari hasil yang saya peroleh dan pengeditan membatalkannya. Bisakah Anda meninjau suntingan Anda dan mengonfirmasi bahwa ini yang Anda inginkan?
Tim Long
7

Berikut adalah contoh kode untuk membuktikan jawaban @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);
    }
}
wdosanjos.dll
sumber
1
Itu tidak membuktikannya. Dimana Buang: t2? :)
Piotr Perak
1
Pertanyaannya adalah tentang membuang sumber daya pertama pada daftar penggunaan, bukan yang kedua. "Apa yang terjadi jika font4 = new Fontmelempar? Dari apa yang saya pahami font3 akan membocorkan sumber daya dan tidak akan dibuang."
wdosanjos