Menghasilkan angka acak dalam bahasa C. Menghasilkan nomor acak (semu) Gunakan satu contoh kelas Acak untuk beberapa panggilan

Menghasilkan angka acak dalam bahasa C.  Menghasilkan nomor acak (semu) Gunakan satu contoh kelas Acak untuk beberapa panggilan
Menghasilkan angka acak dalam bahasa C. Menghasilkan nomor acak (semu) Gunakan satu contoh kelas Acak untuk beberapa panggilan

Terjemahan artikel Angka acak oleh Jon Skeete dikenal luas di kalangan sempit. Saya berhenti di artikel ini karena pada suatu waktu saya sendiri mengalami masalah yang dijelaskan di dalamnya.

Menjelajahi topik menurut .BERSIH Dan C# di situs web StackOverflow Anda dapat melihat pertanyaan yang tak terhitung jumlahnya yang menyebutkan kata "acak", yang, pada kenyataannya, menimbulkan pertanyaan abadi dan "tidak dapat dihancurkan" yang sama: mengapa generator nomor acak System.Random "tidak berfungsi" dan bagaimana cara "memperbaikinya" dia" " Artikel ini dikhususkan untuk mempertimbangkan masalah ini dan cara mengatasinya.

Pernyataan masalah

Di StackOverflow, di newsgroup dan milis, semua pertanyaan tentang topik "acak" terdengar seperti ini:
Saya menggunakan Random.Next untuk menghasilkan beberapa nomor acak, tetapi metode ini mengembalikan nomor yang sama ketika dipanggil beberapa kali. Jumlahnya berubah setiap kali aplikasi diluncurkan, namun dalam satu kali eksekusi program jumlahnya konstan.

Contoh kodenya seperti ini:
// Kode buruk! Jangan gunakan! untuk (int saya = 0; saya< 100; i++) { Console.WriteLine(GenerateDigit()); } ... static int GenerateDigit() { Random rng = new Random(); // Предположим, что здесь много логики return rng.Next(10); }
Jadi apa yang salah disini?

Penjelasan

Kelas Random bukanlah penghasil angka acak yang sebenarnya, ia berisi generator semu nomor acak. Setiap instance dari kelas Random berisi beberapa keadaan internal, dan ketika metode Next (atau NextDouble, atau NextBytes) dipanggil, metode tersebut menggunakan keadaan tersebut untuk mengembalikan nomor yang akan muncul secara acak. Keadaan internal kemudian diubah sehingga pada pemanggilan Next berikutnya, ia akan mengembalikan nomor acak yang berbeda dari yang dikembalikan sebelumnya.

Semua "internal" dari kelas Random sepenuhnya deterministik. Artinya jika Anda mengambil beberapa instance kelas Random dengan keadaan awal yang sama, yang ditentukan melalui parameter konstruktor benih, dan untuk setiap instance panggil metode tertentu dalam urutan yang sama dan dengan parameter yang sama, maka pada akhirnya Anda akan mendapatkan hasil yang sama.

Lalu apa yang salah dengan kode di atas? Hal buruknya adalah kita menggunakan instance baru dari kelas Random di dalam setiap iterasi loop. Konstruktor Acak, yang tidak mengambil parameter apa pun, mengambil tanggal dan waktu saat ini sebagai benihnya. Iterasi dalam loop akan “bergulir” begitu cepat sehingga waktu sistem “tidak akan punya waktu untuk berubah” setelah selesai; dengan demikian, semua instance Random akan menerima nilai yang sama dengan keadaan awalnya dan oleh karena itu akan mengembalikan nomor pseudo-acak yang sama.

Bagaimana cara memperbaikinya?

Ada banyak solusi untuk suatu masalah, masing-masing memiliki kelebihan dan kekurangannya sendiri. Kami akan melihat beberapa di antaranya.
Menggunakan Penghasil Angka Acak Kriptografi
.NET berisi kelas abstrak RandomNumberGenerator yang darinya semua implementasi generator bilangan acak kriptografi (selanjutnya disebut sebagai cryptoRNGs) harus diwarisi. .NET juga berisi salah satu implementasi ini - temui kelas RNGCryptoServiceProvider. Ide dari crypto-RNG adalah meskipun masih merupakan penghasil angka pseudo-acak, ia memberikan hasil yang cukup tidak dapat diprediksi. RNGCryptoServiceProvider menggunakan berbagai sumber entropi, yang pada dasarnya adalah "kebisingan" di komputer Anda, dan urutan angka yang dihasilkannya sangat sulit diprediksi. Selain itu, gangguan "dalam komputer" dapat digunakan tidak hanya sebagai keadaan awal, tetapi juga antara panggilan ke nomor acak berikutnya; dengan demikian, meskipun mengetahui keadaan kelas saat ini, menghitung angka-angka berikutnya yang akan dihasilkan di masa depan dan angka-angka yang dihasilkan sebelumnya tidak akan cukup. Faktanya, perilaku sebenarnya bergantung pada implementasi. Selain itu, Windows dapat menggunakan perangkat keras khusus yang merupakan sumber "keacakan sebenarnya" (misalnya, sensor peluruhan isotop radioaktif) untuk menghasilkan angka acak yang lebih aman dan andal.

Mari kita bandingkan dengan kelas Random yang telah dibahas sebelumnya. Katakanlah Anda menelepon Random.Next(100) sepuluh kali dan menyimpan hasilnya. Jika Anda memiliki daya komputasi yang cukup, Anda dapat, hanya berdasarkan hasil ini, menghitung keadaan awal (seed) yang digunakan untuk membuat instance Random, memprediksi hasil selanjutnya dari pemanggilan Random.Next(100), dan bahkan menghitung hasil dari pemanggilan metode sebelumnya. Perilaku ini sangat tidak dapat diterima jika Anda menggunakan nomor acak untuk keamanan, tujuan keuangan, dll. Crypto RNG bekerja jauh lebih lambat dibandingkan kelas Random, namun mereka menghasilkan serangkaian angka, yang masing-masing lebih independen dan tidak dapat diprediksi dari nilai yang lain.

Dalam kebanyakan kasus, kinerja yang buruk bukanlah sebuah pemecah masalah - API yang buruklah yang menjadi penentunya. RandomNumberGenerator dirancang untuk menghasilkan urutan byte - itu saja. Bandingkan ini dengan metode kelas Random, yang memungkinkan untuk memperoleh bilangan bulat acak, bilangan pecahan, dan juga sekumpulan byte. Properti berguna lainnya adalah kemampuan untuk mendapatkan nomor acak dalam rentang tertentu. Bandingkan kemungkinan ini dengan array byte acak yang dihasilkan RandomNumberGenerator. Anda dapat memperbaiki situasi ini dengan membuat pembungkus (wrapper) Anda sendiri di sekitar RandomNumberGenerator, yang akan mengubah byte acak menjadi hasil yang "nyaman", tetapi solusi ini tidak sepele.

Namun, dalam banyak kasus, "kelemahan" kelas Random baik-baik saja jika Anda dapat menyelesaikan masalah yang dijelaskan di awal artikel. Mari kita lihat apa yang bisa kita lakukan di sini.

Gunakan satu instance dari kelas Random untuk beberapa panggilan
Ini dia, akar solusi masalahnya adalah dengan menggunakan hanya satu instance Random saat membuat banyak angka acak menggunakan Random.Next. Dan ini sangat sederhana - lihat bagaimana Anda dapat mengubah kode di atas:
// Kode ini akan lebih baik Random rng = new Random(); untuk (int saya = 0; saya< 100; i++) { Console.WriteLine(GenerateDigit(rng)); } ... static int GenerateDigit(Random rng) { // Предположим, что здесь много логики return rng.Next(10); }
Sekarang setiap iterasi akan memiliki nomor yang berbeda... tapi bukan itu saja. Apa yang terjadi jika kita memanggil blok kode ini dua kali berturut-turut? Benar sekali, kita akan membuat dua instance Acak dengan seed yang sama dan mendapatkan dua set angka acak yang identik. Angka-angka di setiap himpunan akan berbeda, tetapi himpunan-himpunan ini akan sama satu sama lain.

Ada dua cara untuk menyelesaikan masalah. Pertama, kita tidak bisa menggunakan sebuah instance, tapi sebuah field statis yang berisi sebuah instance dari Random, dan kemudian potongan kode di atas hanya akan membuat satu instance, dan akan menggunakannya, memanggilnya sebanyak yang diperlukan. Kedua, kita dapat sepenuhnya menghapus pembuatan instance Acak dari sana, memindahkannya "lebih tinggi", idealnya ke bagian paling "atas" program, di mana satu instance Acak akan dibuat, setelah itu akan ditransmisikan ke semua tempat di mana angka acak diperlukan. Ini adalah ide yang bagus, diungkapkan dengan baik oleh dependensi, tetapi ini akan berhasil selama kita hanya menggunakan satu thread.

Keamanan benang

Kelas Random tidak aman untuk thread. Mempertimbangkan betapa kami ingin membuat satu instance dan menggunakannya di seluruh program selama seluruh durasi eksekusi (tunggal, halo!), kurangnya keamanan thread menjadi sangat menyebalkan. Lagi pula, jika kita menggunakan satu instance secara bersamaan di beberapa thread, maka ada kemungkinan status internalnya akan direset, dan jika ini terjadi, maka sejak saat itu instance tersebut menjadi tidak berguna.

Sekali lagi, ada dua cara untuk menyelesaikan masalah ini. Jalur pertama masih melibatkan penggunaan satu instance, namun kali ini menggunakan penguncian sumber daya melalui monitor. Untuk melakukan ini, Anda perlu membuat pembungkus di sekitar Random yang akan membungkus panggilan ke metodenya dalam pernyataan kunci, memastikan akses eksklusif ke instance untuk pemanggil. Jalur ini buruk karena mengurangi kinerja dalam skenario intensif thread.

Cara lain, yang akan saya uraikan di bawah, adalah dengan menggunakan satu instance per thread. Satu-satunya hal yang perlu kita pastikan adalah kita menggunakan seed yang berbeda saat membuat instance, jadi kita tidak bisa menggunakan konstruktor default. Jika tidak, jalur ini relatif mudah.

Penyedia aman

Untungnya, kelas generik baru ThreadLocal , diperkenalkan di .NET 4, membuatnya sangat mudah untuk menulis penyedia yang menyediakan satu instance per thread. Anda hanya perlu meneruskan delegasi ke konstruktor ThreadLocal, yang akan merujuk pada perolehan nilai instance kita itu sendiri. Dalam hal ini, saya memutuskan untuk menggunakan nilai benih tunggal, menginisialisasinya menggunakan Environment.TickCount (yang merupakan cara kerja konstruktor Acak tanpa parameter). Selanjutnya, jumlah tick yang dihasilkan bertambah setiap kali kita perlu mendapatkan instance Random baru untuk thread terpisah.

Kelas di bawah ini sepenuhnya statis dan hanya berisi satu metode publik (terbuka) GetThreadRandom. Metode ini dijadikan metode, bukan properti, terutama untuk kenyamanan: ini akan memastikan bahwa semua kelas yang memerlukan instance Random akan bergantung pada Func (delegasi yang menunjuk ke metode yang tidak mengambil parameter dan mengembalikan nilai bertipe Random), dan bukan dari kelas Random itu sendiri. Jika suatu tipe dimaksudkan untuk dijalankan pada satu thread, tipe tersebut dapat memanggil delegasi untuk mendapatkan satu instance Random dan kemudian menggunakannya secara keseluruhan; jika tipe tersebut perlu bekerja dalam skenario multi-utas, tipe tersebut dapat memanggil delegasi setiap kali memerlukan generator nomor acak. Kelas di bawah ini akan membuat instance kelas Random sebanyak jumlah thread, dan setiap instance akan dimulai dari nilai awal yang berbeda. Jika kita perlu menggunakan penyedia nomor acak sebagai ketergantungan pada tipe lain, kita dapat melakukan ini: new TypeThatNeedsRandom(RandomProvider.GetThreadRandom) . Nah, ini kodenya sendiri:
menggunakan Sistem; menggunakan Sistem.Threading; kelas statis publik RandomProvider ( private static int seed = Environment.TickCount; private static ThreadLocal randomWrapper = ThreadLocal baru (() => new Random(saling bertautan.Peningkatan(ref seed)));
getThreadRandom() statis publik statis ( return randomWrapper.Value; ) )

Cukup sederhana, bukan? Ini karena semua kode ditujukan untuk menghasilkan instance Random yang benar. Setelah sebuah instance dibuat dan dikembalikan, tidak masalah apa yang Anda lakukan selanjutnya: semua penerbitan instance selanjutnya sepenuhnya independen dari instance saat ini. Tentu saja, kode klien memiliki celah untuk penyalahgunaan yang berbahaya: kode tersebut dapat mengambil satu instance Random dan meneruskannya ke thread lain alih-alih memanggil RandomProvider kami di thread lain tersebut.

Masih ada satu masalah: kami menggunakan generator nomor acak yang dilindungi dengan lemah. Seperti disebutkan sebelumnya, ada versi RNG yang jauh lebih aman di RandomNumberGenerator, yang implementasinya ada di kelas RNGCryptoServiceProvider. Namun, API-nya cukup sulit digunakan dalam skenario standar.

Akan sangat bagus jika penyedia RNG dalam kerangka tersebut memiliki “sumber keacakan” yang terpisah. Dalam hal ini, kita dapat memiliki API tunggal, sederhana dan nyaman yang akan didukung oleh implementasi yang tidak aman namun cepat dan implementasi yang aman namun lambat. Yah, tidak ada salahnya bermimpi. Mungkin fungsi serupa akan muncul di versi .NET Framework yang akan datang. Mungkin seseorang yang bukan dari Microsoft akan menawarkan implementasi adaptornya sendiri. (Sayangnya, saya tidak akan menjadi orang itu...mengimplementasikan sesuatu seperti ini dengan benar ternyata sangat rumit.) Anda juga dapat membuat kelas Anda sendiri dengan mengambil dari Random dan mengganti metode Sample dan NextBytes, tetapi tidak jelas bagaimana tepatnya mereka seharusnya pekerjaan, atau bahkan Contoh Penerapan Anda sendiri bisa jadi jauh lebih rumit daripada yang terlihat. Mungkin lain kali...

Dalam permasalahan algoritmik pendidikan, kebutuhan untuk menghasilkan bilangan bulat acak cukup umum. Tentu saja, Anda dapat menerimanya dari pengguna, tetapi masalah mungkin timbul saat mengisi array dengan 100 nomor acak.

Fungsi perpustakaan standar bahasa C (bukan C++) rand() membantu kami.

int rand(batal);

Ini menghasilkan bilangan bulat pseudo-acak dalam rentang nilai dari 0 hingga RAND_MAX. Yang terakhir adalah konstanta yang bervariasi tergantung pada implementasi bahasa, tetapi dalam banyak kasus adalah 32767.
Bagaimana jika kita membutuhkan angka acak dari 0 hingga 9? Jalan keluar yang umum dari situasi ini adalah dengan menggunakan operasi pembagian modulo.

Jika kita membutuhkan angka dari 1 (bukan 0) sampai 9, maka kita dapat menambahkan satu...

Idenya begini: kita menghasilkan angka acak dari 0 hingga 8, dan setelah dijumlahkan 1, angka tersebut berubah menjadi angka acak dari 1 hingga 9.

Dan yang terakhir, hal yang paling menyedihkan.
Sayangnya, fungsi rand() menghasilkan angka pseudo-acak, yaitu angka-angka yang tampak acak, namun sebenarnya merupakan rangkaian nilai yang dihitung menggunakan algoritma cerdas yang menggunakan apa yang disebut grain sebagai parameter. Itu. Angka-angka yang dihasilkan oleh fungsi rand() akan bergantung pada nilai yang dimiliki grain pada saat dipanggil. Dan grain selalu disetel oleh compiler ke nilai 1. Dengan kata lain, urutan angkanya akan bersifat pseudo-acak, tetapi selalu sama.
Dan ini bukanlah yang kita butuhkan.

Fungsi srand() membantu memperbaiki situasi.

void srand(unsigned int seed);

Ini menetapkan butir sama dengan nilai parameter yang dipanggil. Dan urutan angkanya juga akan berbeda.

Namun masalahnya tetap ada. Bagaimana cara membuat butirannya acak, karena semuanya bergantung padanya?
Jalan keluar yang umum dari situasi ini adalah dengan menggunakan fungsi time().

waktu_t waktu(waktu_t* pengatur waktu);

Itu juga diwarisi dari bahasa C dan, ketika dipanggil dengan pointer nol sebagai parameter, mengembalikan jumlah detik yang telah berlalu sejak 1 Januari 1970. Tidak, ini bukan lelucon.

Sekarang kita dapat meneruskan nilai fungsi ini ke dalam fungsi srand() (yang melakukan cast implisit), dan kita akan mendapatkan butiran acak yang bagus.
Dan jumlahnya akan luar biasa dan tidak berulang.

Untuk menggunakan fungsi rand() dan srand() Anda perlu menyertakan file header , dan untuk menggunakan time() - file .

Berikut adalah contoh lengkapnya.

#termasuk
#termasuk
#termasuk

menggunakan namespace std;

ke dalam utama()
{
cout<< "10 random numbers (1..100): " << endl;
srand(waktu(NULL));
untuk(int i=0;i<10;i++) cout << rand() % 100 + 1 << " ";
cin.mendapatkan();
kembali 0;
}

Terjemahan artikel Angka acak oleh Jon Skeete dikenal luas di kalangan sempit. Saya berhenti di artikel ini karena pada suatu waktu saya sendiri mengalami masalah yang dijelaskan di dalamnya.

Menjelajahi topik menurut .BERSIH Dan C# di situs web StackOverflow Anda dapat melihat pertanyaan yang tak terhitung jumlahnya yang menyebutkan kata "acak", yang, pada kenyataannya, menimbulkan pertanyaan abadi dan "tidak dapat dihancurkan" yang sama: mengapa generator nomor acak System.Random "tidak berfungsi" dan bagaimana cara "memperbaikinya" dia" " Artikel ini dikhususkan untuk mempertimbangkan masalah ini dan cara mengatasinya.

Pernyataan masalah

Di StackOverflow, di newsgroup dan milis, semua pertanyaan tentang topik "acak" terdengar seperti ini:
Saya menggunakan Random.Next untuk menghasilkan beberapa nomor acak, tetapi metode ini mengembalikan nomor yang sama ketika dipanggil beberapa kali. Jumlahnya berubah setiap kali aplikasi diluncurkan, namun dalam satu kali eksekusi program jumlahnya konstan.

Contoh kodenya seperti ini:
// Kode buruk! Jangan gunakan! untuk (int saya = 0; saya< 100; i++) { Console.WriteLine(GenerateDigit()); } ... static int GenerateDigit() { Random rng = new Random(); // Предположим, что здесь много логики return rng.Next(10); }
Jadi apa yang salah disini?

Penjelasan

Kelas Random bukanlah penghasil angka acak yang sebenarnya, ia berisi generator semu nomor acak. Setiap instance dari kelas Random berisi beberapa keadaan internal, dan ketika metode Next (atau NextDouble, atau NextBytes) dipanggil, metode tersebut menggunakan keadaan tersebut untuk mengembalikan nomor yang akan muncul secara acak. Keadaan internal kemudian diubah sehingga pada pemanggilan Next berikutnya, ia akan mengembalikan nomor acak yang berbeda dari yang dikembalikan sebelumnya.

Semua "internal" dari kelas Random sepenuhnya deterministik. Artinya jika Anda mengambil beberapa instance kelas Random dengan keadaan awal yang sama, yang ditentukan melalui parameter konstruktor benih, dan untuk setiap instance panggil metode tertentu dalam urutan yang sama dan dengan parameter yang sama, maka pada akhirnya Anda akan mendapatkan hasil yang sama.

Lalu apa yang salah dengan kode di atas? Hal buruknya adalah kita menggunakan instance baru dari kelas Random di dalam setiap iterasi loop. Konstruktor Acak, yang tidak mengambil parameter apa pun, mengambil tanggal dan waktu saat ini sebagai benihnya. Iterasi dalam loop akan “bergulir” begitu cepat sehingga waktu sistem “tidak akan punya waktu untuk berubah” setelah selesai; dengan demikian, semua instance Random akan menerima nilai yang sama dengan keadaan awalnya dan oleh karena itu akan mengembalikan nomor pseudo-acak yang sama.

Bagaimana cara memperbaikinya?

Ada banyak solusi untuk suatu masalah, masing-masing memiliki kelebihan dan kekurangannya sendiri. Kami akan melihat beberapa di antaranya.
Menggunakan Penghasil Angka Acak Kriptografi
.NET berisi kelas abstrak RandomNumberGenerator yang darinya semua implementasi generator bilangan acak kriptografi (selanjutnya disebut sebagai cryptoRNGs) harus diwarisi. .NET juga berisi salah satu implementasi ini - temui kelas RNGCryptoServiceProvider. Ide dari crypto-RNG adalah meskipun masih merupakan penghasil angka pseudo-acak, ia memberikan hasil yang cukup tidak dapat diprediksi. RNGCryptoServiceProvider menggunakan berbagai sumber entropi, yang pada dasarnya adalah "kebisingan" di komputer Anda, dan urutan angka yang dihasilkannya sangat sulit diprediksi. Selain itu, gangguan "dalam komputer" dapat digunakan tidak hanya sebagai keadaan awal, tetapi juga antara panggilan ke nomor acak berikutnya; dengan demikian, meskipun mengetahui keadaan kelas saat ini, menghitung angka-angka berikutnya yang akan dihasilkan di masa depan dan angka-angka yang dihasilkan sebelumnya tidak akan cukup. Faktanya, perilaku sebenarnya bergantung pada implementasi. Selain itu, Windows dapat menggunakan perangkat keras khusus yang merupakan sumber "keacakan sebenarnya" (misalnya, sensor peluruhan isotop radioaktif) untuk menghasilkan angka acak yang lebih aman dan andal.

Mari kita bandingkan dengan kelas Random yang telah dibahas sebelumnya. Katakanlah Anda menelepon Random.Next(100) sepuluh kali dan menyimpan hasilnya. Jika Anda memiliki daya komputasi yang cukup, Anda dapat, hanya berdasarkan hasil ini, menghitung keadaan awal (seed) yang digunakan untuk membuat instance Random, memprediksi hasil selanjutnya dari pemanggilan Random.Next(100), dan bahkan menghitung hasil dari pemanggilan metode sebelumnya. Perilaku ini sangat tidak dapat diterima jika Anda menggunakan nomor acak untuk keamanan, tujuan keuangan, dll. Crypto RNG bekerja jauh lebih lambat dibandingkan kelas Random, namun mereka menghasilkan serangkaian angka, yang masing-masing lebih independen dan tidak dapat diprediksi dari nilai yang lain.

Dalam kebanyakan kasus, kinerja yang buruk bukanlah sebuah pemecah masalah - API yang buruklah yang menjadi penentunya. RandomNumberGenerator dirancang untuk menghasilkan urutan byte - itu saja. Bandingkan ini dengan metode kelas Random, yang memungkinkan untuk memperoleh bilangan bulat acak, bilangan pecahan, dan juga sekumpulan byte. Properti berguna lainnya adalah kemampuan untuk mendapatkan nomor acak dalam rentang tertentu. Bandingkan kemungkinan ini dengan array byte acak yang dihasilkan RandomNumberGenerator. Anda dapat memperbaiki situasi ini dengan membuat pembungkus (wrapper) Anda sendiri di sekitar RandomNumberGenerator, yang akan mengubah byte acak menjadi hasil yang "nyaman", tetapi solusi ini tidak sepele.

Namun, dalam banyak kasus, "kelemahan" kelas Random baik-baik saja jika Anda dapat menyelesaikan masalah yang dijelaskan di awal artikel. Mari kita lihat apa yang bisa kita lakukan di sini.

Gunakan satu instance dari kelas Random untuk beberapa panggilan
Ini dia, akar solusi masalahnya adalah dengan menggunakan hanya satu instance Random saat membuat banyak angka acak menggunakan Random.Next. Dan ini sangat sederhana - lihat bagaimana Anda dapat mengubah kode di atas:
// Kode ini akan lebih baik Random rng = new Random(); untuk (int saya = 0; saya< 100; i++) { Console.WriteLine(GenerateDigit(rng)); } ... static int GenerateDigit(Random rng) { // Предположим, что здесь много логики return rng.Next(10); }
Sekarang setiap iterasi akan memiliki nomor yang berbeda... tapi bukan itu saja. Apa yang terjadi jika kita memanggil blok kode ini dua kali berturut-turut? Benar sekali, kita akan membuat dua instance Acak dengan seed yang sama dan mendapatkan dua set angka acak yang identik. Angka-angka di setiap himpunan akan berbeda, tetapi himpunan-himpunan ini akan sama satu sama lain.

Ada dua cara untuk menyelesaikan masalah. Pertama, kita tidak bisa menggunakan sebuah instance, tapi sebuah field statis yang berisi sebuah instance dari Random, dan kemudian potongan kode di atas hanya akan membuat satu instance, dan akan menggunakannya, memanggilnya sebanyak yang diperlukan. Kedua, kita dapat sepenuhnya menghapus pembuatan instance Acak dari sana, memindahkannya "lebih tinggi", idealnya ke bagian paling "atas" program, di mana satu instance Acak akan dibuat, setelah itu akan ditransmisikan ke semua tempat di mana angka acak diperlukan. Ini adalah ide yang bagus, diungkapkan dengan baik oleh dependensi, tetapi ini akan berhasil selama kita hanya menggunakan satu thread.

Keamanan benang

Kelas Random tidak aman untuk thread. Mempertimbangkan betapa kami ingin membuat satu instance dan menggunakannya di seluruh program selama seluruh durasi eksekusi (tunggal, halo!), kurangnya keamanan thread menjadi sangat menyebalkan. Lagi pula, jika kita menggunakan satu instance secara bersamaan di beberapa thread, maka ada kemungkinan status internalnya akan direset, dan jika ini terjadi, maka sejak saat itu instance tersebut menjadi tidak berguna.

Sekali lagi, ada dua cara untuk menyelesaikan masalah ini. Jalur pertama masih melibatkan penggunaan satu instance, namun kali ini menggunakan penguncian sumber daya melalui monitor. Untuk melakukan ini, Anda perlu membuat pembungkus di sekitar Random yang akan membungkus panggilan ke metodenya dalam pernyataan kunci, memastikan akses eksklusif ke instance untuk pemanggil. Jalur ini buruk karena mengurangi kinerja dalam skenario intensif thread.

Cara lain, yang akan saya uraikan di bawah, adalah dengan menggunakan satu instance per thread. Satu-satunya hal yang perlu kita pastikan adalah kita menggunakan seed yang berbeda saat membuat instance, jadi kita tidak bisa menggunakan konstruktor default. Jika tidak, jalur ini relatif mudah.

Penyedia aman

Untungnya, kelas generik baru ThreadLocal , diperkenalkan di .NET 4, membuatnya sangat mudah untuk menulis penyedia yang menyediakan satu instance per thread. Anda hanya perlu meneruskan delegasi ke konstruktor ThreadLocal, yang akan merujuk pada perolehan nilai instance kita itu sendiri. Dalam hal ini, saya memutuskan untuk menggunakan nilai benih tunggal, menginisialisasinya menggunakan Environment.TickCount (yang merupakan cara kerja konstruktor Acak tanpa parameter). Selanjutnya, jumlah tick yang dihasilkan bertambah setiap kali kita perlu mendapatkan instance Random baru untuk thread terpisah.

Kelas di bawah ini sepenuhnya statis dan hanya berisi satu metode publik (terbuka) GetThreadRandom. Metode ini dijadikan metode, bukan properti, terutama untuk kenyamanan: ini akan memastikan bahwa semua kelas yang memerlukan instance Random akan bergantung pada Func (delegasi yang menunjuk ke metode yang tidak mengambil parameter dan mengembalikan nilai bertipe Random), dan bukan dari kelas Random itu sendiri. Jika suatu tipe dimaksudkan untuk dijalankan pada satu thread, tipe tersebut dapat memanggil delegasi untuk mendapatkan satu instance Random dan kemudian menggunakannya secara keseluruhan; jika tipe tersebut perlu bekerja dalam skenario multi-utas, tipe tersebut dapat memanggil delegasi setiap kali memerlukan generator nomor acak. Kelas di bawah ini akan membuat instance kelas Random sebanyak jumlah thread, dan setiap instance akan dimulai dari nilai awal yang berbeda. Jika kita perlu menggunakan penyedia nomor acak sebagai ketergantungan pada tipe lain, kita dapat melakukan ini: new TypeThatNeedsRandom(RandomProvider.GetThreadRandom) . Nah, ini kodenya sendiri:
menggunakan Sistem; menggunakan Sistem.Threading; kelas statis publik RandomProvider ( private static int seed = Environment.TickCount; private static ThreadLocal randomWrapper = ThreadLocal baru (() => new Random(saling bertautan.Peningkatan(ref seed)));
getThreadRandom() statis publik statis ( return randomWrapper.Value; ) )

Cukup sederhana, bukan? Ini karena semua kode ditujukan untuk menghasilkan instance Random yang benar. Setelah sebuah instance dibuat dan dikembalikan, tidak masalah apa yang Anda lakukan selanjutnya: semua penerbitan instance selanjutnya sepenuhnya independen dari instance saat ini. Tentu saja, kode klien memiliki celah untuk penyalahgunaan yang berbahaya: kode tersebut dapat mengambil satu instance Random dan meneruskannya ke thread lain alih-alih memanggil RandomProvider kami di thread lain tersebut.

Masih ada satu masalah: kami menggunakan generator nomor acak yang dilindungi dengan lemah. Seperti disebutkan sebelumnya, ada versi RNG yang jauh lebih aman di RandomNumberGenerator, yang implementasinya ada di kelas RNGCryptoServiceProvider. Namun, API-nya cukup sulit digunakan dalam skenario standar.

Akan sangat bagus jika penyedia RNG dalam kerangka tersebut memiliki “sumber keacakan” yang terpisah. Dalam hal ini, kita dapat memiliki API tunggal, sederhana dan nyaman yang akan didukung oleh implementasi yang tidak aman namun cepat dan implementasi yang aman namun lambat. Yah, tidak ada salahnya bermimpi. Mungkin fungsi serupa akan muncul di versi .NET Framework yang akan datang. Mungkin seseorang yang bukan dari Microsoft akan menawarkan implementasi adaptornya sendiri. (Sayangnya, saya tidak akan menjadi orang itu...mengimplementasikan sesuatu seperti ini dengan benar ternyata sangat rumit.) Anda juga dapat membuat kelas Anda sendiri dengan mengambil dari Random dan mengganti metode Sample dan NextBytes, tetapi tidak jelas bagaimana tepatnya mereka seharusnya pekerjaan, atau bahkan Contoh Penerapan Anda sendiri bisa jadi jauh lebih rumit daripada yang terlihat. Mungkin lain kali...

Harap tangguhkan AdBlock di situs ini.

Terkadang mungkin perlu untuk menghasilkan angka acak. Sebuah contoh sederhana.

Contoh: Penentuan pemenang dalam kompetisi repost.

Ada daftar 53 orang. Penting untuk memilih pemenang dari mereka. Jika Anda memilihnya sendiri, Anda mungkin dituduh bias. Jadi Anda memutuskan untuk menulis sebuah program. Ini akan bekerja sebagai berikut. Anda memasukkan jumlah peserta N, setelah itu program menampilkan satu nomor - nomor pemenang.

Anda sudah mengetahui cara mendapatkan nomor dari seorang pemain. Tapi bagaimana Anda bisa memaksa komputer memikirkan angka acak? Dalam pelajaran ini Anda akan belajar bagaimana melakukan hal ini.

fungsi Rand().

Fungsi ini mengembalikan bilangan bulat acak dalam rentang dari nol hingga RAND_MAX. RAND_MAX adalah konstanta C khusus yang menampung nilai integer maksimum yang dapat dikembalikan oleh fungsi rand().

Fungsi rand() didefinisikan dalam file header stdlib.h. Oleh karena itu, jika Anda ingin menggunakan rand dalam program Anda, jangan lupa sertakan file header ini. Konstanta RAND_MAX juga didefinisikan dalam file ini. Anda dapat menemukan file ini di komputer Anda dan melihat artinya.

Mari kita lihat fitur ini beraksi. Mari kita jalankan kode berikut:

Daftar 1.

#termasuk // untuk menggunakan fungsi printf #include // untuk menggunakan fungsi rand int main(void) ( /* menghasilkan lima bilangan bulat acak */ printf("%d\n", rand()); printf("%d\n", rand()); printf ("%d\n", rand()); printf("%d\n", rand()); printf("%d\n", rand());

Seharusnya terlihat seperti ini.

Gbr.1 Lima angka acak yang dihasilkan oleh fungsi rand

Namun kami ingin mendapatkan angka dari 1 hingga 53, dan tidak semuanya berturut-turut. Berikut beberapa trik untuk membantu Anda membatasi fungsi rand().

Batasi angka acak dari atas.

Siapa pun yang menunggu di sekolah saat matematika berguna, bersiaplah. Saatnya telah tiba. Untuk membatasi bilangan acak di atas, Anda dapat menggunakan operasi memperoleh sisa pembagian yang telah Anda pelajari pada pelajaran terakhir. Anda mungkin tahu bahwa sisa pembagian dengan bilangan K selalu lebih kecil dari bilangan K. Misalnya membagi dengan 4 akan menghasilkan sisa 0, 1, 2, dan 3. Oleh karena itu, jika ingin membatasi bilangan acak dari atas ke bilangan K, maka cukup ambil sisa pembagiannya dengan K. Seperti ini:

Daftar 2.

#termasuk #termasuk int main(void) ( /* menghasilkan lima bilangan bulat acak kurang dari 100 */ printf("%d\n", rand()%100); printf("%d\n", rand()%100); printf ("%d\n", rand()%100); printf("%d\n", rand()%100);


Gbr.2 Lima bilangan acak kurang dari 100

Batasi angka di bawah ini.

Fungsi Rand mengembalikan angka acak dari interval. Bagaimana jika kita hanya membutuhkan angka yang lebih besar dari M (misalnya 1000)? Apa yang harus saya lakukan? Sederhana saja. Mari kita tambahkan nilai M kita ke fungsi rand yang dikembalikan. Kemudian jika fungsinya mengembalikan 0 maka jawaban akhirnya adalah M, jika 2394 maka jawaban akhirnya adalah M + 2394. Dengan tindakan ini kita seolah-olah menggeser semua bilangan ke depan sebanyak M satuan.

Tetapkan batas atas dan bawah fungsi Rand.

Misalnya, dapatkan angka dari 80 hingga 100. Sepertinya Anda hanya perlu menggabungkan kedua cara di atas. Kita akan mendapatkan sesuatu seperti ini:

Daftar 3.

#termasuk #termasuk int main(void) ( /* menghasilkan lima bilangan bulat acak yang lebih besar dari 80 dan kurang dari 100 */ printf("%d\n", 80 + rand()%100); printf("%d\n", 80 + nilai ()%100); printf("%d\n", 80 + nilai()%100); printf("%d\n", 80 + nilai()%100); 100);

Coba jalankan program ini. Terkejut?

Ya, cara ini tidak akan berhasil. Mari kita jalankan program ini secara manual untuk melihat apakah kita melakukan kesalahan. Katakanlah rand() mengembalikan nomor 143. Sisanya jika dibagi 100 adalah 43. Maka 80 + 43 = 123. Jadi cara ini tidak berhasil. Desain ini akan menghasilkan angka dari 80 hingga 179.

Mari kita uraikan ekspresi kita selangkah demi selangkah. rand()%100 dapat mengembalikan angka dari 0 hingga 99 inklusif. Itu. dari segmen tersebut.
Operasi + 80 menggeser segmen kami 80 unit ke kanan. Kami mengerti.
Seperti yang Anda lihat, masalah kita terletak pada batas kanan ruas; bergeser ke kanan sebanyak 79 unit. Ini angka asli kita 80 dikurangi 1. Mari kita bereskan dan pindahkan batas kanan kembali: 80 + rand()%(100 - 80 + 1) . Maka semuanya akan berjalan sebagaimana mestinya.

Secara umum, jika kita perlu mendapatkan nomor dari segmen tersebut, maka kita perlu menggunakan konstruksi berikut:
A + Rand()%(B-A+1) .

Berdasarkan rumus ini, kami akan menulis ulang program terakhir kami:

Daftar 4.

#termasuk #termasuk int main(void) ( /* menghasilkan lima bilangan bulat acak dari segmen */ printf("%d\n", 80 + rand()%(100 - 80 + 1)); printf("%d\n", 80 + rand()%(100 - 79)); printf("%d\n", 80 + rand()%21); printf("%d\n", 80 + rand()%21); "%d\n", 80 + rand()%21 )

Hasil:


Gbr.3 Angka acak dari suatu rentang

Nah, sekarang Anda bisa menyelesaikan masalah awal pelajaran. Hasilkan nomor dari segmen. Atau tidak bisa?

Tapi pertama-tama, beberapa informasi berguna. Jalankan program terakhir tiga kali berturut-turut dan tuliskan angka acak yang dihasilkannya. Apakah Anda memperhatikan?

fungsi srand().

Ya, nomor identik yang sama muncul setiap saat. “Generator biasa-biasa saja!” - katamu. Dan Anda tidak sepenuhnya benar. Memang, angka yang sama dihasilkan setiap saat. Namun kita dapat memengaruhinya dengan menggunakan fungsi srand(), yang juga didefinisikan dalam file header stdlib.h. Ini menginisialisasi generator nomor acak dengan nomor benih.

Kompilasi dan jalankan program ini beberapa kali:

Daftar 5.

#termasuk #termasuk int main(void) ( srand(2); /* menghasilkan lima bilangan bulat acak dari segmen */ printf("%d\n", 80 + rand()%(100 - 80 + 1)); printf("% d\n", 80 + rand()%(100 - 79)); printf("%d\n", 80 + rand()%21); printf("%d\n", 80 + rand() %21); printf("%d\n", 80 + nilai()%21);

Sekarang ubah argumen fungsi srand() ke angka lain (saya harap Anda tidak lupa apa itu argumen fungsi?) lalu kompilasi dan jalankan program lagi. Urutan angka harus berubah. Segera setelah kita mengubah argumen dalam fungsi srand, urutannya berubah. Sangat tidak praktis, bukan? Untuk mengubah urutannya, Anda perlu mengkompilasi ulang program. Andai saja nomor ini dimasukkan di sana secara otomatis.

Dan itu bisa dilakukan. Misalnya, mari kita gunakan fungsi time(), yang didefinisikan dalam file header time.h. Fungsi ini, jika NULL dilewatkan sebagai argumen, mengembalikan jumlah detik yang telah berlalu sejak 1 Januari 1970. Berikut ini cara melakukannya.

Daftar 6.

#termasuk #termasuk #termasuk // untuk menggunakan fungsi time() int main(void) ( srand(time(NULL)); /* menghasilkan lima bilangan bulat acak dari segmen */ printf("%d\n", 80 + rand()%( 100 - 80 + 1)); printf("%d\n", 80 + rand()%(100 - 79)); printf("%d\n", 80 + rand()%21); \n", 80 + rand()%21); printf("%d\n", 80 + rand()%21); )

Anda mungkin bertanya, apa itu NULL? Sebuah pertanyaan yang masuk akal. Sementara itu, saya akan memberi tahu Anda apa kata khusus yang dilindungi undang-undang ini. Saya juga bisa mengatakan apa arti pointer nol, tapi... Ini tidak memberi Anda informasi apa pun, jadi saya sarankan untuk tidak memikirkannya saat ini. Dan ingatlah itu sebagai semacam trik cerdas. Dalam pelajaran selanjutnya kita akan melihat hal ini secara lebih rinci.

Sangat sering dalam program ada kebutuhan untuk menggunakan nomor acak - mulai dari mengisi array hingga kriptografi. Untuk memperoleh barisan bilangan acak, bahasa C# memiliki kelas Random. Kelas ini menyediakan dua konstruktor:

  • Acak()- menginisialisasi instance kelas Random dengan nilai awal yang bergantung pada waktu saat ini. Seperti diketahui, waktu dapat direpresentasikan dalam kutu- Pulsa 100 nanodetik mulai 1 Januari 0001. Dan nilai waktu dalam tick adalah bilangan bulat 64-bit, yang akan digunakan untuk menginisialisasi instance penghasil angka acak.
  • Acak (Int32)- menginisialisasi instance kelas Random menggunakan nilai awal yang ditentukan. Menginisialisasi generator angka acak dengan cara ini dapat memudahkan saat men-debug suatu program, karena dalam hal ini angka “acak” yang sama akan dihasilkan setiap kali program diluncurkan.
Metode utama kelas ini adalah metode Next(), yang memungkinkan Anda mendapatkan nomor acak dan memiliki sejumlah kelebihan:
  • Next() - mengembalikan bilangan bulat non-negatif acak dalam format Int32.
  • Berikutnya( Int32)- mengembalikan bilangan bulat non-negatif acak yang kurang dari nilai yang ditentukan.
  • Berikutnya( Int32 mnt, Int32 maks)- mengembalikan bilangan bulat acak dalam rentang yang ditentukan. Dalam hal ini, kondisi min harus dipenuhi
Dan juga metode
  • Byte Berikutnya( byte)- mengisi elemen array byte yang ditentukan dengan angka acak.
  • NextDouble() - mengembalikan angka floating point acak, dalam rentang )
    merusak ; // kecocokan ditemukan, elemen tidak cocok
    }
    jika (j == saya)
    { // tidak ditemukan kecocokan
    a[i] = angka; // simpan elemennya
    saya++; // pergi ke elemen berikutnya
    }
    }
    untuk (int saya = 0; saya< 100; i++)
    {

    jika (saya % 10 == 9)
    Konsol.WriteLine();
    }
    Konsol .ReadKey();
    }
    }
    }

    Namun, semakin dekat ke akhir array, semakin banyak generasi yang harus dilakukan untuk mendapatkan nilai yang tidak berulang.
    Contoh berikut menampilkan jumlah panggilan ke metode Next() untuk mendapatkan setiap elemen, serta jumlah total angka acak yang dihasilkan untuk mengisi array 100 elemen dengan nilai yang tidak berulang.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53

    menggunakan Sistem;
    namespace Program Saya
    {
    Program kelas
    {
    kekosongan statis Utama (argumen string)
    {
    Rnd acak = baru Acak();
    int a = int baru; // susunan elemen
    int hitungan = int baru; // array jumlah generasi
    a = rnd.Berikutnya(0, 101);
    ke dalam c = 0; // penghitung jumlah generasi
    hitungan = 1; // a hanya dihasilkan satu kali
    untuk (int saya = 1; saya< 100;)
    {
    int angka = rnd.Berikutnya(0, 101);
    c++; // membuat elemen sekali lagi
    ke dalam j;
    untuk (j = 0; j< i; j++)
    {
    jika (angka == a[j])
    merusak ;
    }
    jika (j == saya)
    {
    a[i] = angka; saya++;
    hitung[i] = c; c = 0; // simpan jumlah generasi
    }
    }
    // Mencetak nilai elemen
    Konsol .WriteLine( "Nilai Elemen");
    untuk (int saya = 0; saya< 100; i++)
    {
    Konsol .Write("(0,4) " , a[i]);
    jika (saya % 10 == 9)
    Konsol.WriteLine();
    }
    Konsol.WriteLine();
    // Menampilkan jumlah generasi
    Konsol .WriteLine( "Jumlah pembuatan elemen");
    int jumlah = 0;
    untuk (int saya = 0; saya< 100; i++)
    {
    jumlah += hitungan[i];
    Konsol .Write("(0,4) " , hitung[i]);
    jika (saya % 10 == 9)
    Konsol.WriteLine();
    }
    Konsol .WriteLine( "Jumlah total generasi - (0)", jumlah);
    Konsol .ReadKey();
    }
    }
    }

    kekosongan Utama (argumen string)
    {
    Rnd acak = baru Acak();
    int a = int baru;
    untuk (int saya = 0; saya< 100; i++)
    a[saya] = saya;
    untuk (int saya = 0; saya< 50; i++)
    {
    int i1 = rnd.Berikutnya(0, 100); // indeks pertama
    int i2 = rnd.Berikutnya(0, 100); // indeks kedua
    // pertukaran nilai elemen dengan indeks i1 dan i2
    int suhu = a;
    sebuah = sebuah;
    a = suhu;
    }
    Konsol .WriteLine( "Nilai Elemen");
    untuk (int saya = 0; saya< 100; i++)
    {
    Konsol .Write("(0,4) " , a[i]);
    jika (saya % 10 == 9)
    Konsol.WriteLine();
    }
    Konsol .ReadKey();
    }
    }
    }

    Pengacakan nilai lebih efektif jika rentang nilai cocok (atau mendekati) jumlah nilai, karena dalam hal ini jumlah pembuatan elemen acak berkurang secara signifikan.