Việc “sử dụng” với nhiều tài nguyên có thể gây ra rò rỉ tài nguyên không?


106

C # cho phép tôi làm như sau (ví dụ từ MSDN):

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

Điều gì xảy ra nếu font4 = new Fontném? Từ những gì tôi hiểu, font3 sẽ làm rò rỉ tài nguyên và sẽ không bị xử lý.

  • Điều này có đúng không? (font4 sẽ không bị loại bỏ)
  • Có phải điều này using(... , ...)nên tránh hoàn toàn để ủng hộ việc sử dụng lồng nhau không?

7
Nó sẽ không bị rò rỉ bộ nhớ; trong trường hợp xấu nhất, nó vẫn sẽ nhận được GC'd.
SLaks

3
Tôi sẽ không ngạc nhiên nếu using(... , ...)được biên dịch thành các khối lồng nhau bằng cách sử dụng bất kể, nhưng tôi không biết chắc điều đó.
Dan J

1
Ý của tôi không phải như vậy. Ngay cả khi bạn không sử dụng using, GC cuối cùng vẫn sẽ thu thập nó.
SLaks

1
@zneak: Nếu nó được biên dịch thành một finallykhối, nó sẽ không vào khối cho đến khi tất cả các tài nguyên được xây dựng.
SLaks

2
@zneak: Bởi vì trong quá trình chuyển đổi a usingthành try- finally, biểu thức khởi tạo được đánh giá bên ngoài try. Vì vậy, nó là một câu hỏi hợp lý.
Ben Voigt

Câu trả lời:


158

Không.

Trình biên dịch sẽ tạo một finallykhối riêng biệt cho mỗi biến.

Thông số kỹ thuật (§8.13) cho biết:

Khi thu nhận tài nguyên có dạng khai báo biến-cục bộ, có thể thu được nhiều tài nguyên thuộc một loại nhất định. Một usingtuyên bố của biểu mẫu

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

chính xác tương đương với một chuỗi các câu lệnh sử dụng lồng nhau:

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

4
Đó là 8.13 trong Đặc tả C # phiên bản 5.0, btw.
Ben Voigt

11
@WeylandYutani: Bạn đang hỏi gì vậy?
SLaks

9
@WeylandYutani: Đây là một trang web hỏi đáp. Nếu bạn có một câu hỏi, hãy bắt đầu một câu hỏi mới!
Eric Lippert

5
@ user1306322 tại sao? Nếu tôi thực sự muốn biết thì sao?
Oxymoron

2
@Oxymoron thì bạn nên cung cấp một số bằng chứng về nỗ lực trước khi đăng câu hỏi dưới dạng nghiên cứu và phỏng đoán, nếu không bạn sẽ bị nói như vậy, mất sự chú ý và nếu không thì sẽ bị thiệt hại nặng nề hơn. Chỉ là một lời khuyên dựa trên kinh nghiệm cá nhân.
user1306322

67

CẬP NHẬT : Tôi đã sử dụng câu hỏi này làm cơ sở cho một bài báo có thể tìm thấy ở đây ; xem nó để thảo luận thêm về vấn đề này. Cảm ơn vì câu hỏi hay!


Mặc dù câu trả lời của Schabse tất nhiên là đúng và trả lời câu hỏi đã được hỏi, nhưng có một biến thể quan trọng về câu hỏi của bạn mà bạn chưa hỏi:

Điều gì xảy ra nếu font4 = new Font()ném sau khi tài nguyên không được quản lý được cấp phát bởi hàm tạo nhưng trước khi ctor trả về và điền vàofont4 tham chiếu?

Hãy để tôi nói rõ hơn một chút. Giả sử chúng ta có:

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

Bây giờ chúng tôi có

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

Điều này giống như

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

ĐỒNG Ý. Giả sử Whateverném. Sau đófinally khối chạy và tài nguyên được phân bổ. Không vấn đề gì.

Giả sử Blah1()ném. Sau đó, ném xảy ra trước khi tài nguyên được cấp phát. Đối tượng đã được cấp phát nhưng ctor không bao giờ trả về, vì vậy fookhông bao giờ được điền vào. Chúng tôi không bao giờ nhập vào tryvì vậy chúng tôi không bao giờ nhập một finallytrong hai. Tham chiếu đối tượng đã bị mất. Cuối cùng, GC sẽ phát hiện ra điều đó và đưa nó vào hàng đợi người hoàn thiện. handlevẫn là 0, vì vậy trình hoàn thiện không làm gì cả. Lưu ý rằng trình hoàn thiện được yêu cầu phải mạnh mẽ khi đối mặt với một đối tượng đang được hoàn thiện mà hàm tạo không bao giờ hoàn thành . Bạn được yêu cầu viết các bản hoàn thiện thật mạnh mẽ. Đây là một lý do khác tại sao bạn nên giao việc viết các bản hoàn thiện cho các chuyên gia chứ không phải cố gắng tự làm.

Giả sử Blah3()ném. Việc ném xảy ra sau khi tài nguyên được cấp phát. Nhưng một lần nữa, fookhông bao giờ được điền vào, chúng tôi không bao giờ nhậpfinally , và đối tượng được làm sạch bởi chuỗi hoàn thiện. Lần này tay cầm khác 0 và trình hoàn thiện sẽ làm sạch nó. Một lần nữa, trình hoàn thiện đang chạy trên một đối tượng mà hàm tạo không bao giờ thành công, nhưng trình hoàn thiện vẫn chạy. Rõ ràng là phải làm vì thời gian này, nó có việc phải làm.

Bây giờ, giả sử Blah2()ném. Việc ném xảy ra sau khi tài nguyên được cấp phát nhưng trước khi handle được điền vào! Một lần nữa, trình hoàn thiện sẽ chạy nhưng bây giờ handlevẫn là 0 và chúng tôi bị rò rỉ phần xử lý!

Bạn cần phải viết mã cực kỳ thông minh để ngăn chặn rò rỉ này xảy ra. Bây giờ, trong trường hợp Fonttài nguyên của bạn , ai là người quan tâm? Chúng tôi bị rò rỉ một cái xử lý phông chữ, chuyện lớn. Nhưng nếu bạn hoàn toàn tích cực yêu cầu rằng mọi tài nguyên không được quản lý phải được dọn dẹp bất kể thời gian của các trường hợp ngoại lệ là gì sau đó bạn có một vấn đề rất khó khăn trên tay của bạn.

CLR phải giải quyết vấn đề này với ổ khóa. Kể từ C # 4, các khóa sử dụng lockcâu lệnh đã được triển khai như sau:

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

Enterđã được viết rất cẩn thận để không có vấn đề gì ngoại lệ được ném ra , lockEnteredđược đặt thành true nếu và chỉ khi khóa thực sự được thực hiện. Nếu bạn có yêu cầu tương tự thì những gì bạn cần thực sự là viết:

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

và viết AllocateResourcemột cách khéo léo Monitor.Entersao cho không có vấn đề gì xảy ra bên trong AllocateResource, dấu handleđược điền vào nếu và chỉ khi nó cần được phân bổ.

Mô tả các kỹ thuật để làm như vậy nằm ngoài phạm vi của câu trả lời này. Tham khảo ý kiến ​​chuyên gia nếu bạn có yêu cầu này.


6
@gnat: Câu trả lời được chấp nhận. S đó phải đại diện cho một cái gì đó. :-)
Eric Lippert

12
@Joe: Tất nhiên ví dụ được trù định . Tôi chỉ hiểu nó . Rủi ro không được phóng đại bởi vì tôi chưa nói rõ mức độ rủi ro là bao nhiêu; đúng hơn, tôi đã tuyên bố rằng mô hình này là có thể . Việc bạn tin rằng thiết lập trường trực tiếp giải quyết vấn đề cho thấy chính xác quan điểm của tôi: giống như phần lớn các lập trình viên không có kinh nghiệm với loại vấn đề này, bạn không đủ năng lực để giải quyết vấn đề này; trên thực tế, hầu hết mọi người thậm chí không nhận ra rằng có một vấn đề, đó là lý do tại sao tôi đã viết câu trả lời này ở nơi đầu tiên .
Eric Lippert

5
@Chris: Giả sử không có công việc nào được thực hiện giữa phân bổ và trả lại, và giữa trả về và chuyển nhượng. Chúng tôi xóa tất cả các Blahcuộc gọi phương thức đó. Điều gì ngăn một ThreadAbortException xảy ra tại một trong hai điểm đó?
Eric Lippert

5
@Joe: Đây không phải là một xã hội tranh luận; Tôi không muốn ghi điểm bằng cách thuyết phục hơn . Nếu bạn hoài nghi và không muốn nghe lời tôi rằng đây là một vấn đề phức tạp cần tham khảo ý kiến ​​của các chuyên gia để giải quyết chính xác thì bạn có thể không đồng ý với tôi.
Eric Lippert

7
@GilesRoberts: Điều đó giải quyết vấn đề như thế nào? Giả sử ngoại lệ xảy ra sau lệnh gọi đến AllocateResourcenhưng trước khi chỉ định đến x. A ThreadAbortExceptioncó thể xảy ra tại thời điểm đó. Mọi người ở đây dường như đang thiếu quan điểm của tôi, đó là việc tạo ra một tài nguyên và việc gán một tham chiếu đến nó cho một biến không phải là một phép toán nguyên tử . Để giải quyết vấn đề tôi đã xác định, bạn phải biến nó thành một hoạt động nguyên tử.
Eric Lippert

32

Để bổ sung cho câu trả lời @SLaks, đây là IL cho mã của bạn:

.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

Lưu ý các khối thử / cuối cùng được lồng vào nhau.


17

Mã này (dựa trên mẫu ban đầu):

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

Nó tạo ra CIL sau (trong Visual Studio 2013 , nhắm mục tiêu .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

Như bạn có thể thấy, try {}khối không bắt đầu cho đến sau lần phân bổ đầu tiên, diễn ra lúc IL_0012. Thoạt nhìn, điều này dường như phân bổ mục đầu tiên trong mã không được bảo vệ. Tuy nhiên, lưu ý rằng kết quả được lưu trữ ở vị trí 0. Nếu lần cấp phát thứ hai không thành công, khối bên ngoài finally {} sẽ thực thi và khối này sẽ tìm nạp đối tượng từ vị trí 0, tức là lần cấp phát đầu tiên font3và gọi Dispose()phương thức của nó .

Thật thú vị, việc dịch ngược assembly này với dotPeek tạo ra nguồn hoàn nguyên sau:

using System.Drawing;

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

Mã được dịch ngược xác nhận rằng mọi thứ đều đúng và về usingcơ bản được mở rộng thành các usings lồng nhau . Mã CIL hơi khó hiểu khi nhìn vào và tôi đã phải nhìn chằm chằm vào nó trong vài phút trước khi tôi hiểu đúng những gì đang xảy ra, vì vậy tôi không ngạc nhiên khi một số 'câu chuyện về những người vợ cũ' đã bắt đầu xuất hiện về điều này. Tuy nhiên, mã được tạo là sự thật không thể công bố.


@Peter Mortensen chỉnh sửa của bạn đã loại bỏ các đoạn mã IL (giữa IL_0012 và IL_0017) khiến giải thích không hợp lệ và khó hiểu. Đoạn mã đó nhằm mục đích là một bản sao nguyên văn của kết quả tôi thu được và việc chỉnh sửa sẽ làm mất hiệu lực của nó. Bạn có thể vui lòng xem lại bản chỉnh sửa của mình và xác nhận đây có phải là những gì bạn dự định không?
Tim Long

7

Đây là mã mẫu để chứng minh câu trả lời của @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
Điều đó không chứng minh điều đó. Thải bỏ ở đâu: t2? :)
Piotr Perak

1
Câu hỏi là về việc loại bỏ tài nguyên đầu tiên trong danh sách sử dụng chứ không phải tài nguyên thứ hai. "Điều gì xảy ra nếu font4 = new Fontném? Theo những gì tôi hiểu thì font3 sẽ làm rò rỉ tài nguyên và sẽ không bị xử lý."
wdosanjos
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.