Haruskah pernyataan kembali berada di dalam atau di luar kunci?

142

Saya baru menyadari bahwa di suatu tempat dalam kode saya, saya memiliki pernyataan pengembalian di dalam kunci dan kadang di luar. Yang mana yang terbaik?

1)

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

2)

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

}

Yang mana yang harus saya gunakan?

Patrick Desjardins
sumber
Bagaimana dengan menembakkan Reflector dan melakukan perbandingan IL ;-).
Pop Catalin
6
@Pop: selesai - tidak ada yang lebih baik dalam hal IL - hanya gaya C # berlaku
Marc Gravell
1
Sangat menarik, wow saya belajar sesuatu hari ini!
Pokus

Jawaban:

192

Intinya, mana yang pernah membuat kode lebih sederhana. Satu titik keluar adalah ideal yang bagus, tapi saya tidak akan membengkokkan kode keluar dari bentuk hanya untuk mencapainya ... Dan jika alternatifnya mendeklarasikan variabel lokal (di luar kunci), menginisialisasi (di dalam kunci) dan lalu mengembalikannya (di luar kunci), maka saya akan mengatakan bahwa "pengembalian foo" sederhana di dalam kunci jauh lebih sederhana.

Untuk menunjukkan perbedaan dalam IL, mari kode:

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

(perhatikan bahwa saya akan dengan senang hati berpendapat bahwa itu ReturnInsideadalah sedikit lebih mudah / bersih dari C #)

Dan lihat IL (mode rilis dll):

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

Jadi pada level IL, mereka [memberi atau menerima beberapa nama] identik (saya belajar sesuatu ;-p). Dengan demikian, satu-satunya perbandingan yang masuk akal adalah hukum (sangat subyektif) gaya pengkodean lokal ... Saya lebih suka ReturnInsideuntuk kesederhanaan, tapi saya tidak akan senang dengan baik.

Marc Gravell
sumber
15
Saya menggunakan Reflektor .NET Gate (gratis dan luar biasa) Red Gate (dulu: .NET Reflector Lutz Roeder), tetapi ILDASM juga akan melakukannya.
Marc Gravell
1
Salah satu aspek paling kuat dari Reflector adalah Anda benar-benar dapat membongkar IL ke bahasa pilihan Anda (C #, VB, Delphi, MC ++, Chrome, dll)
Marc Gravell
3
Sebagai contoh sederhana Anda, IL tetap sama, tetapi itu mungkin karena Anda hanya mengembalikan nilai konstan ?! Saya percaya untuk skenario kehidupan nyata hasilnya mungkin berbeda, dan utas paralel dapat menyebabkan masalah satu sama lain dengan memodifikasi nilai sebelum dikembalikan, pernyataan pengembalian berada di luar blok kunci. Berbahaya!
Torbjørn
@MarcGravell: Saya baru saja menemukan posting Anda sambil memikirkan hal yang sama dan bahkan setelah membaca jawaban Anda, saya masih tidak yakin tentang yang berikut: Apakah ada situasi di mana menggunakan pendekatan luar dapat mematahkan logika aman-thread. Saya menanyakan hal ini karena saya lebih suka satu titik pengembalian dan tidak 'merasa' baik tentang keamanannya. Meskipun, jika IL-nya sama, kekhawatiran saya harus diperdebatkan.
Raheel Khan
1
@ RaheelKhan tidak, tidak ada; mereka sama. Pada tingkat IL, Anda tidak dapat ret berada di dalam suatu .trywilayah.
Marc Gravell
42

Tidak ada bedanya; keduanya diterjemahkan ke hal yang sama oleh kompiler.

Untuk memperjelas, keduanya diterjemahkan secara efektif ke sesuatu dengan semantik berikut:

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

return myData;
Greg Beech
sumber
1
Ya, itu benar dari percobaan / akhirnya - namun, pengembalian di luar kunci masih memerlukan tambahan penduduk lokal yang tidak dapat dioptimalkan - dan mengambil lebih banyak kode ...
Marc Gravell
3
Anda tidak dapat kembali dari blok percobaan; itu harus diakhiri dengan "-leave" op-code. Jadi CIL yang dipancarkan harus sama dalam kedua kasus.
Greg Beech
3
Anda benar - Saya baru saja melihat IL (lihat posting terbaru). Saya belajar sesuatu ;-p
Marc Gravell
2
Keren, sayangnya saya belajar dari jam-jam yang menyakitkan mencoba memancarkan .ret op-kode dalam blok mencoba dan meminta CLR menolak untuk memuat metode dinamis saya :-(
Greg Beech
Saya bisa berhubungan; Saya sudah melakukan banyak Refleksi. Cukup, tapi saya malas; kecuali saya sangat yakin tentang sesuatu, saya menulis kode representatif dalam C # dan kemudian melihat IL. Tetapi mengejutkan betapa cepatnya Anda mulai berpikir dalam istilah IL (yaitu mengurutkan tumpukan).
Marc Gravell
28

Saya pasti akan memasukkan pengembalian di dalam kunci. Kalau tidak, Anda berisiko untaian lain memasuki kunci dan memodifikasi variabel Anda sebelum pernyataan kembali, sehingga membuat penelepon asli menerima nilai yang berbeda dari yang diharapkan.

Ricardo Villamil
sumber
4
Ini benar, suatu titik yang tampaknya tidak dijawab oleh responden lain. Sampel sederhana yang mereka buat mungkin menghasilkan IL yang sama, tetapi ini tidak berlaku untuk kebanyakan skenario kehidupan nyata.
Torbjørn
4
Saya terkejut dengan jawaban yang lain, jangan bicara tentang ini
Akshat Agarwal
5
Dalam sampel ini mereka berbicara tentang menggunakan variabel stack untuk menyimpan nilai kembali, yaitu hanya pernyataan kembali di luar kunci dan tentu saja deklarasi variabel. Thread lain harus memiliki tumpukan lain dan dengan demikian tidak dapat membahayakan, apakah saya benar?
Guillermo Ruffino
3
Saya tidak berpikir ini adalah poin yang valid, karena utas lain dapat memperbarui nilai antara panggilan balik dan penugasan sebenarnya dari nilai kembali ke variabel pada utas utama. Nilai yang dikembalikan tidak dapat diubah atau dijamin konsisten dengan nilai aktual saat ini. Baik?
Uroš Joksimović
Jawaban ini salah. Utas lain tidak dapat mengubah variabel lokal. Variabel lokal disimpan di tumpukan, dan setiap utas memiliki tumpukan sendiri. Btw ukuran default dari tumpukan thread adalah 1 MB .
Theodor Zoulias
5

Tergantung,

Saya akan menentang gandum di sini. Saya biasanya akan kembali ke dalam kunci.

Biasanya variabel mydata adalah variabel lokal. Saya suka mendeklarasikan variabel lokal saat saya menginisialisasi mereka. Saya jarang memiliki data untuk menginisialisasi nilai saya di luar kunci saya.

Jadi perbandingan Anda sebenarnya cacat. Meskipun idealnya perbedaan antara dua opsi akan seperti yang Anda tulis, yang tampaknya memberi anggukan pada kasus 1, dalam praktiknya sedikit lebih jelek.

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

vs.

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

Saya menemukan case 2 lebih mudah dibaca dan lebih sulit untuk dikacaukan, terutama untuk cuplikan singkat.

Edward KMETT
sumber
4

Jika mengira kunci di luar terlihat lebih baik, tetapi berhati-hatilah jika Anda akhirnya mengubah kode menjadi:

return f(...)

Jika f () perlu dipanggil dengan kunci dipegang maka jelas perlu di dalam kunci, karena itu menjaga kembali di dalam kunci untuk konsistensi masuk akal.

Rob Walker
sumber
1

Untuk apa nilainya, dokumentasi pada MSDN memiliki contoh untuk kembali dari dalam kunci. Dari jawaban lain di sini, tampaknya IL yang sangat mirip tetapi, bagi saya, itu tampak lebih aman untuk kembali dari dalam kunci karena dengan begitu Anda tidak menjalankan risiko variabel pengembalian ditimpa oleh utas lainnya.

greyseal96
sumber
0

Untuk membuatnya lebih mudah bagi sesama pengembang untuk membaca kode saya akan menyarankan alternatif pertama.

Adam Asham
sumber
0

lock() return <expression> pernyataan selalu:

1) masukkan kunci

2) membuat toko lokal (aman-thread) untuk nilai dari jenis yang ditentukan,

3) mengisi toko dengan nilai yang dikembalikan oleh <expression>,

4) kunci keluar

5) mengembalikan toko.

Ini berarti bahwa nilai, yang dikembalikan dari pernyataan kunci, selalu "dimasak" sebelum kembali.

Jangan khawatir lock() return, jangan dengarkan siapa pun di sini))

mshakurov
sumber
-2

Catatan: Saya yakin jawaban ini benar secara faktual dan saya harap ini membantu juga, tapi saya selalu senang memperbaikinya berdasarkan umpan balik yang konkret.

Untuk merangkum dan melengkapi jawaban yang ada:

  • The diterima jawaban menunjukkan bahwa, terlepas dari yang bentuk sintaks yang Anda pilih dalam Anda # C kode, dalam kode IL - dan karena itu pada saat runtime - yang returntidak terjadi sampai setelah mengunci dilepaskan.

    • Meskipun menempatkan return dalam satu lockblok Oleh karena itu, tegasnya, salah mengartikan aliran kontrol [1] , itu adalah sintaksis nyaman dalam hal itu menyingkirkan kebutuhan untuk menyimpan nilai kembali dalam aux. variabel lokal (dideklarasikan di luar blok, sehingga dapat digunakan dengan di returnluar blok) - lihat jawaban Edward KMETT .
  • Secara terpisah - dan aspek ini insidental untuk pertanyaan, tetapi mungkin masih menarik ( jawaban Ricardo Villamil mencoba mengatasinya, tetapi saya kira salah) - menggabungkan lockpernyataan dengan returnpernyataan - yaitu, mendapatkan nilai returndalam blok yang dilindungi dari akses bersamaan - hanya bermakna "melindungi" nilai yang dikembalikan dalam ruang lingkup penelepon jika sebenarnya tidak perlu dilindungi setelah diperoleh , yang berlaku dalam skenario berikut:

    • Jika nilai yang dikembalikan adalah elemen dari koleksi yang hanya perlu perlindungan dalam hal menambah dan menghapus elemen, tidak dalam hal modifikasi dari elemen itu sendiri dan / atau ...

    • ... jika nilai yang dikembalikan adalah turunan dari tipe nilai atau string .

      • Perhatikan bahwa dalam hal ini pemanggil menerima snapshot (copy) [2] dari nilai - yang pada saat pemanggil memeriksa tidak lagi mungkin nilai saat ini dalam struktur data asal.
    • Dalam kasus lain, penguncian harus dilakukan oleh pemanggil , bukan (hanya) di dalam metode.


[1] Theodor Zoulias menunjukkan bahwa itu secara teknis juga berlaku untuk penempatan returndi dalam try,catch , using, if, while, for, ... pernyataan; Namun, itu adalah tujuan khusus dari lockpernyataan yang cenderung mengundang pengawasan aliran kontrol yang sebenarnya, sebagaimana dibuktikan oleh pertanyaan ini yang telah ditanyakan dan telah menerima banyak perhatian.

[2] Mengakses contoh tipe nilai selalu menciptakan salinan thread-local, on-the-stack; walaupun string secara teknis adalah tipe instance, mereka secara efektif berperilaku seperti-nilai tipe.

mklement0
sumber
Mengenai keadaan saat ini dari jawaban Anda (revisi 13), Anda masih berspekulasi tentang alasan keberadaan lock, dan memperoleh makna dari penempatan pernyataan pengembalian. Yang merupakan diskusi yang tidak terkait dengan pertanyaan ini IMHO. Saya juga menemukan penggunaan "misrepresents" cukup mengganggu. Jika kembali dari locksalah mengartikan aliran kontrol, maka sama dapat dikatakan untuk kembali dari try, catch, using, if, while, for, dan setiap konstruk lain dari bahasa. Ini seperti mengatakan bahwa C # penuh dengan misrepresentasi aliran kontrol. Yesus ...
Theodor Zoulias
"Ini seperti mengatakan bahwa C # penuh dengan misrepresentasi aliran kontrol" - Ya, secara teknis itu benar, dan istilah "misrepresentasi" hanya penilaian nilai jika Anda memilih untuk mengambil jalan itu. Dengan try,, if... Saya pribadi bahkan cenderung untuk tidak memikirkannya, tetapi dalam konteks lock, secara khusus, muncul pertanyaan bagi saya - dan jika itu tidak muncul untuk orang lain juga, pertanyaan ini tidak akan pernah ditanyakan , dan jawaban yang diterima tidak akan berusaha keras untuk menyelidiki perilaku yang sebenarnya.
mklement0