Một tuyên bố trở lại nên được bên trong hoặc bên ngoài một khóa?


142

Tôi chỉ nhận ra rằng ở một nơi nào đó trong mã của tôi, tôi có câu lệnh return bên trong khóa và đôi khi ở bên ngoài. Cái nào là tốt nhất?

1)

void example()
{
    lock (mutex)
    {
    //...
    }
    return myData;
}

2)

void example()
{
    lock (mutex)
    {
    //...
    return myData;
    }

}

Tôi nên sử dụng cái nào?


Làm thế nào về việc bắn Reflector và làm một số so sánh IL ;-).
Pop Catalin

6
@Pop: xong - không tốt hơn về mặt IL - chỉ áp dụng kiểu C #
Marc Gravell

1
Rất thú vị, wow tôi học được điều gì hôm nay!
Pokus

Câu trả lời:


192

Về cơ bản, điều này bao giờ làm cho mã đơn giản hơn. Một điểm thoát duy nhất là một lý tưởng tốt đẹp, nhưng tôi sẽ không bẻ cong mã ra khỏi hình dạng chỉ để đạt được nó ... Và nếu phương án thay thế là khai báo một biến cục bộ (bên ngoài khóa), hãy khởi tạo nó (bên trong khóa) và sau đó trả lại (bên ngoài khóa), sau đó tôi sẽ nói rằng một "return foo" đơn giản bên trong khóa đơn giản hơn rất nhiều.

Để hiển thị sự khác biệt trong IL, hãy cho phép mã:

static class Program
{
    static void Main() { }

    static readonly object sync = new object();

    static int GetValue() { return 5; }

    static int ReturnInside()
    {
        lock (sync)
        {
            return GetValue();
        }
    }

    static int ReturnOutside()
    {
        int val;
        lock (sync)
        {
            val = GetValue();
        }
        return val;
    }
}

(lưu ý rằng tôi vui vẻ tranh luận rằng đó ReturnInsidelà một bit đơn giản / gọn gàng hơn của C #)

Và nhìn vào IL (chế độ phát hành, v.v.):

.method private hidebysig static int32 ReturnInside() cil managed
{
    .maxstack 2
    .locals init (
        [0] int32 CS$1$0000,
        [1] object CS$2$0001)
    L_0000: ldsfld object Program::sync
    L_0005: dup 
    L_0006: stloc.1 
    L_0007: call void [mscorlib]System.Threading.Monitor::Enter(object)
    L_000c: call int32 Program::GetValue()
    L_0011: stloc.0 
    L_0012: leave.s L_001b
    L_0014: ldloc.1 
    L_0015: call void [mscorlib]System.Threading.Monitor::Exit(object)
    L_001a: endfinally 
    L_001b: ldloc.0 
    L_001c: ret 
    .try L_000c to L_0014 finally handler L_0014 to L_001b
} 

method private hidebysig static int32 ReturnOutside() cil managed
{
    .maxstack 2
    .locals init (
        [0] int32 val,
        [1] object CS$2$0000)
    L_0000: ldsfld object Program::sync
    L_0005: dup 
    L_0006: stloc.1 
    L_0007: call void [mscorlib]System.Threading.Monitor::Enter(object)
    L_000c: call int32 Program::GetValue()
    L_0011: stloc.0 
    L_0012: leave.s L_001b
    L_0014: ldloc.1 
    L_0015: call void [mscorlib]System.Threading.Monitor::Exit(object)
    L_001a: endfinally 
    L_001b: ldloc.0 
    L_001c: ret 
    .try L_000c to L_0014 finally handler L_0014 to L_001b
}

Vì vậy, ở cấp độ IL, chúng giống nhau [cho hoặc lấy một số tên] (tôi đã học được điều gì đó ;-p). Như vậy, so sánh hợp lý duy nhất là luật (rất chủ quan) của phong cách mã hóa địa phương ... Tôi thích ReturnInsidesự đơn giản, nhưng tôi cũng không hào hứng với điều đó.


15
Tôi đã sử dụng Bộ phản xạ .NET của Red Gate (miễn phí và xuất sắc) (là: Bộ phản xạ .NET của Lutz Roeder's), nhưng ILDASM cũng sẽ làm như vậy.
Marc Gravell

1
Một trong những khía cạnh mạnh mẽ nhất của Reflector là bạn thực sự có thể phân tách IL thành ngôn ngữ ưa thích của bạn (C #, VB, Delphi, MC ++, Chrome, v.v.)
Marc Gravell

3
Đối với ví dụ đơn giản của bạn, IL vẫn giữ nguyên, nhưng đó có lẽ là do bạn chỉ trả về một giá trị không đổi?! Tôi tin rằng đối với các tình huống thực tế, kết quả có thể khác nhau và các luồng song song có thể gây ra sự cố cho nhau bằng cách sửa đổi giá trị trước khi trả về, câu lệnh return nằm ngoài khối khóa. Nguy hiểm!
Torbjørn

@MarcGravell: Tôi vừa xem bài đăng của bạn trong khi suy nghĩ tương tự và thậm chí sau khi đọc câu trả lời của bạn, tôi vẫn không chắc chắn về những điều sau: Có bất kỳ trường hợp nào khi sử dụng phương pháp bên ngoài có thể phá vỡ logic an toàn của luồng. Tôi hỏi điều này vì tôi thích một điểm trở lại duy nhất và không 'CẢM NHẬN' về độ an toàn của luồng. Mặc dù, nếu IL là như nhau, mối quan tâm của tôi nên được đưa ra dù sao đi nữa.
Raheel Khan

1
@RaheelKhan không, không có; họ giống nhau. Ở cấp độ IL, bạn không thể ret ở trong một .trykhu vực.
Marc Gravell

42

Nó không làm cho bất kỳ sự khác biệt; cả hai đều được dịch sang cùng một thứ bởi trình biên dịch.

Để làm rõ, một trong hai được dịch một cách hiệu quả với một ngữ nghĩa sau:

T myData;
Monitor.Enter(mutex)
try
{
    myData= // something
}
finally
{
    Monitor.Exit(mutex);
}

return myData;

1
Chà, điều đó đúng với sự cố gắng / cuối cùng - tuy nhiên, việc quay lại bên ngoài khóa vẫn cần thêm người dân địa phương không thể tối ưu hóa - và lấy thêm mã ...
Marc Gravell

3
Bạn không thể quay lại từ một khối thử; nó phải kết thúc bằng mã op ".leave". Vì vậy, CIL phát ra phải giống nhau trong cả hai trường hợp.
Greg Beech

3
Bạn nói đúng - Tôi vừa xem IL (xem bài đăng cập nhật). Tôi đã học được điều gì đó ;-p
Marc Gravell

2
Thật tuyệt, thật không may, tôi đã học được từ những giờ đau đớn khi cố gắng phát ra các mã op trong các khối thử và CLR từ chối tải các phương thức động của tôi :-(
Greg Beech

Tôi có thể có liên quan; Tôi đã thực hiện một số lượng khá lớn của Reflection. Nhận, nhưng tôi lười biếng; trừ khi tôi rất chắc chắn về điều gì đó, tôi viết mã đại diện bằng C # và sau đó nhìn vào IL. Nhưng thật đáng ngạc nhiên khi bạn bắt đầu suy nghĩ nhanh chóng theo thuật ngữ IL (nghĩa là sắp xếp thứ tự ngăn xếp).
Marc Gravell

28

Tôi chắc chắn sẽ đặt sự trở lại bên trong khóa. Nếu không, bạn có nguy cơ một luồng khác nhập khóa và sửa đổi biến của bạn trước câu lệnh return, do đó làm cho người gọi ban đầu nhận được một giá trị khác so với dự kiến.


4
Điều này là chính xác, một điểm mà những người trả lời khác dường như bị thiếu. Các mẫu đơn giản mà họ đã thực hiện có thể tạo ra cùng một IL, nhưng điều này không đúng với hầu hết các tình huống thực tế.
Torbjørn

4
Tôi ngạc nhiên khi các câu trả lời khác không nói về điều này
Akshat Agarwal

5
Trong mẫu này, họ đang nói về việc sử dụng biến stack để lưu giá trị trả về, tức là chỉ có câu lệnh return bên ngoài khóa và tất nhiên là khai báo biến. Một chủ đề khác nên có một ngăn xếp khác và do đó không thể gây hại, phải không?
Guillermo Ruffino

3
Tôi không nghĩ rằng đây là một điểm hợp lệ, vì một luồng khác có thể cập nhật giá trị giữa lệnh gọi trở lại và gán thực tế của giá trị trả về cho biến trên luồng chính. Giá trị được trả về không thể thay đổi hoặc được đảm bảo tính nhất quán với giá trị thực tế hiện tại. Đúng?
Uroš Joksimović

Câu trả lời này không chính xác. Một chủ đề khác không thể thay đổi một biến cục bộ. Các biến cục bộ được lưu trữ trong ngăn xếp và mỗi luồng có ngăn xếp riêng. Btw kích thước mặc định của ngăn xếp của một luồng là 1 MB .
Theodor Zoulias

5

Nó phụ thuộc

Tôi sẽ đi ngược lại hạt lúa ở đây. Tôi thường sẽ trở lại bên trong khóa.

Thông thường biến mydata là một biến cục bộ. Tôi thích khai báo các biến cục bộ trong khi tôi khởi tạo chúng. Tôi hiếm khi có dữ liệu để khởi tạo giá trị trả lại của mình bên ngoài khóa.

Vì vậy, so sánh của bạn là thực sự thiếu sót. Mặc dù lý tưởng là sự khác biệt giữa hai tùy chọn sẽ như bạn đã viết, dường như đưa ra cái gật đầu cho trường hợp 1, trong thực tế, nó hơi xấu hơn một chút.

void example() { 
    int myData;
    lock (foo) { 
        myData = ...;
    }
    return myData
}

so với

void example() { 
    lock (foo) {
        return ...;
    }
}

Tôi thấy trường hợp 2 dễ đọc hơn và khó bắt vít hơn, đặc biệt là đối với các đoạn ngắn.


4

Nếu nghĩ rằng khóa bên ngoài có vẻ tốt hơn, nhưng hãy cẩn thận nếu bạn cuối cùng thay đổi mã thành:

return f(...)

Nếu f () cần được gọi với khóa được giữ thì rõ ràng nó cần phải ở bên trong khóa, vì việc giữ lại trả lại bên trong khóa để đảm bảo tính nhất quán.


1

Đối với giá trị của nó, tài liệu về MSDN có một ví dụ về việc quay lại từ bên trong khóa. Từ các câu trả lời khác ở đây, có vẻ như IL khá giống nhau, nhưng đối với tôi, việc quay trở lại từ bên trong khóa có vẻ an toàn hơn vì sau đó bạn không có nguy cơ biến trả về bị ghi đè bởi một luồng khác.


0

Để giúp các nhà phát triển đồng nghiệp đọc mã dễ dàng hơn, tôi sẽ đề xuất phương án đầu tiên.


0

lock() return <expression> báo cáo luôn:

1) nhập khóa

2) lưu trữ cục bộ (an toàn luồng) cho giá trị của loại đã chỉ định,

3) lấp đầy cửa hàng với giá trị được trả về bởi <expression>,

4) khóa thoát

5) trả lại cửa hàng.

Nó có nghĩa là giá trị đó, được trả về từ câu lệnh khóa, luôn được "nấu chín" trước khi trả về.

Đừng lo lắng lock() return, đừng nghe ai ở đây))


-2

Lưu ý: Tôi tin rằng câu trả lời này là chính xác và tôi hy vọng rằng nó cũng hữu ích, nhưng tôi luôn vui lòng cải thiện nó dựa trên phản hồi cụ thể.

Để tóm tắt và bổ sung cho các câu trả lời hiện có:

  • Các câu trả lời được chấp nhận cho thấy, không phân biệt trong đó cú pháp mẫu bạn chọn trong của bạn # C mã, trong các mã IL - và do đó trong thời gian chạy - những returnkhông xảy ra cho đến khi sau khi khóa được phát hành.

    • Mặc dù đặt return bên trong các lockkhối do đó, Nghiêm nói, trình bày sai dòng điều khiển [1] , nó là cú pháp thuận tiện ở chỗ nó obviates sự cần thiết để lưu trữ giá trị trả về trong một aux. biến cục bộ (được khai báo bên ngoài khối, để nó có thể được sử dụng với returnbên ngoài khối) - xem câu trả lời của Edward KMETT .
  • Một cách riêng biệt - và khía cạnh này là ngẫu nhiên cho câu hỏi, nhưng vẫn có thể được quan tâm ( câu trả lời của Ricardo Villamil cố gắng giải quyết nó, nhưng tôi nghĩ không chính xác) - kết hợp một locktuyên bố với một returntuyên bố - tức là, đạt được giá trị returntrong một khối được bảo vệ khỏi một khối truy cập đồng thời - chỉ có ý nghĩa "bảo vệ" giá trị được trả về trong phạm vi của người gọi nếu nó không thực sự cần bảo vệ một khi có được , áp dụng trong các trường hợp sau:

    • Nếu giá trị được trả về là một phần tử từ bộ sưu tập chỉ cần bảo vệ về mặt thêm và xóa phần tử, không phải về mặt sửa đổi của chính các phần tử và / hoặc ...

    • ... nếu giá trị được trả về là một thể hiện của loại giá trị hoặc chuỗi .

      • Lưu ý rằng trong trường hợp này, người gọi nhận được một ảnh chụp nhanh (bản sao) [2] của giá trị - mà tại thời điểm người gọi kiểm tra, nó có thể không còn là giá trị hiện tại trong cấu trúc dữ liệu gốc.
    • Trong mọi trường hợp khác, việc khóa phải được thực hiện bởi người gọi , không phải (chỉ) bên trong phương thức.


[1] Theodor Zoulias chỉ ra rằng đó là về mặt kỹ thuật cũng đúng đối với việc đặt returnbên trong try, catch, using, if, while, for, ... báo cáo; tuy nhiên, mục đích cụ thể của locktuyên bố có khả năng mời xem xét kỹ lưỡng luồng kiểm soát thực sự, bằng chứng là câu hỏi này đã được hỏi và nhận được nhiều sự chú ý.

[2] Truy cập một thể hiện kiểu giá trị luôn tạo ra một bản sao luồng cục bộ, trên ngăn xếp của nó; mặc dù các chuỗi là các thể hiện kiểu tham chiếu kỹ thuật, chúng hoạt động hiệu quả với các thể hiện kiểu giá trị.


Về trạng thái hiện tại của câu trả lời của bạn (bản sửa đổi 13), bạn vẫn đang suy đoán về lý do tồn tại lockvà xuất phát ý nghĩa từ việc đặt câu lệnh trả lại. Đó là một cuộc thảo luận không liên quan đến câu hỏi này IMHO. Ngoài ra tôi thấy việc sử dụng "thông tin sai lệch" khá đáng lo ngại. Nếu trở về từ một locktrình bày sai luồng kiểm soát, thì cũng có thể nói cho trở về từ một try, catch, using, if, while, for, và bất kỳ cấu trúc khác của ngôn ngữ. Nó giống như nói rằng C # bị đánh lừa với sự xuyên tạc dòng điều khiển. Chúa
ơi

"Giống như nói rằng C # bị đánh lừa bởi sự xuyên tạc dòng kiểm soát" - Vâng, điều đó đúng về mặt kỹ thuật và thuật ngữ "trình bày sai" chỉ là một phán đoán giá trị nếu bạn chọn thực hiện theo cách đó. Với try, if... Cá nhân tôi thậm chí không có xu hướng nghĩ về nó, nhưng trong bối cảnh lock, cụ thể, câu hỏi đặt ra cho tôi - và nếu nó không phát sinh cho người khác nữa, câu hỏi này sẽ không bao giờ được hỏi và câu trả lời được chấp nhận sẽ không mất nhiều thời gian để điều tra hành vi thực sự.
mkuity0
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.