Pseudosatunnaisten sekvenssien luominen. Pseudosatunnaiset luvut Kuinka luoda 2 satunnaislukua c

Artikkelin käännös Jon Skeeten satunnaiset luvut, jotka tunnetaan laajalti kapeissa piireissä. Pysähdyin tähän artikkeliin, koska törmäsin itsekin siinä kuvattuun ongelmaan.

Selataan aiheita mukaan .NETTO Ja C# StackOverflow-verkkosivustolla voit nähdä lukemattomia kysymyksiä, joissa mainitaan sana "random", jotka itse asiassa herättävät saman ikuisen ja "tuhoutumattoman" kysymyksen: miksi System.Random satunnaislukugeneraattori "ei toimi" ja miten " korjaa se"" Tämä artikkeli on omistettu tämän ongelman pohtimiseen ja sen ratkaisemiseen.

Ongelman muotoilu

StackOverflow:ssa, uutisryhmissä ja postituslistoissa, kaikki "satunnaista"-aihetta koskevat kysymykset kuulostavat tältä:
Käytän Random.Nextiä useiden satunnaislukujen luomiseen, mutta menetelmä palauttaa saman numeron, kun sitä kutsutaan useita kertoja. Numero muuttuu aina, kun sovellus käynnistetään, mutta yhden ohjelman suorituksen aikana se on vakio.

Esimerkkikoodi on jotain tämän kaltaista:
// Huono koodi! Älä käytä! for (int i = 0; i< 100; i++) { Console.WriteLine(GenerateDigit()); } ... static int GenerateDigit() { Random rng = new Random(); // Предположим, что здесь много логики return rng.Next(10); }
Joten mikä tässä on vialla?

Selitys

Random-luokka ei ole todellinen satunnaislukugeneraattori, se sisältää generaattorin pseudo satunnaisia ​​numeroita. Jokainen Random-luokan esiintymä sisältää jonkin sisäisen tilan, ja kun Next (tai NextDouble tai NextBytes) -metodia kutsutaan, menetelmä käyttää tätä tilaa palauttaakseen satunnaisena näkyvän luvun. Sisäistä tilaa muutetaan sitten niin, että seuraavan kerran, kun Next kutsutaan, se palauttaa erilaisen näennäisesti satunnaisen luvun kuin aiemmin palautettu.

Kaikki Random-luokan "sisäiset". täysin deterministinen. Tämä tarkoittaa, että jos otat useita esiintymiä Random-luokasta samalla alkutilalla, joka määritetään konstruktoriparametrin kautta siemen, ja kutsu jokaiselle tapaukselle tiettyjä menetelmiä samassa järjestyksessä ja samoilla parametreilla, niin lopulta saat samat tulokset.

Joten mikä vika yllä olevassa koodissa on? Huono asia on, että käytämme uutta Random-luokan esiintymää silmukan jokaisessa iteraatiossa. Random-konstruktori, joka ei ota parametreja, ottaa siemenensä nykyisen päivämäärän ja kellonajan. Iteraatiot silmukassa "vierivät" niin nopeasti, että järjestelmän aika "ei ehdi muuttua" niiden valmistuttua; siten kaikki Random-instanssit saavat alkutilan sama arvo ja siksi palauttaa saman pseudosatunnaisluvun.

Kuinka korjata se?

Ongelmaan on monia ratkaisuja, joista jokaisella on omat hyvät ja huonot puolensa. Tarkastellaan muutamia niistä.
Kryptografisen satunnaislukugeneraattorin käyttäminen
.NET sisältää abstraktin luokan RandomNumberGenerator, josta kaikkien kryptografisten satunnaislukugeneraattoreiden (jäljempänä kryptoRNG:t) on perittävä. .NET sisältää myös yhden näistä toteutuksista - täytä RNGCryptoServiceProvider-luokka. Krypto-RNG:n ideana on, että vaikka se on edelleen näennäissatunnaislukugeneraattori, se tarjoaa melko vahvan tulosten arvaamattomuuden. RNGCryptoServiceProvider käyttää useita entropialähteitä, jotka ovat pohjimmiltaan "kohinaa" tietokoneessasi, ja sen luomaa numerosarjaa on erittäin vaikea ennustaa. Lisäksi "tietokoneen sisäistä" kohinaa voidaan käyttää ei vain alkutilana, vaan myös seuraaviin satunnaisnumeroihin soitettujen puhelujen välillä; Näin ollen vaikka luokan nykyinen tila olisi tiedossa, ei riitä laskemaan sekä seuraavat tulevaisuudessa syntyneet että aiemmin luodut luvut. Itse asiassa tarkka käyttäytyminen riippuu toteutuksesta. Lisäksi Windows voi käyttää erikoislaitteistoa, joka on "todellisen satunnaisuuden" lähde (esimerkiksi radioaktiivisen isotoopin hajoamisanturi) luodakseen entistä turvallisempia ja luotettavampia satunnaislukuja.

Verrataan tätä aiemmin käsiteltyyn Random-luokkaan. Oletetaan, että soitit Random.Next(100) kymmenen kertaa ja tallensit tulokset. Jos sinulla on tarpeeksi laskentatehoa, voit pelkästään näiden tulosten perusteella laskea alkutilan (siemen), jolla Random-instanssi luotiin, ennustaa Random.Next(100)-kutsun seuraavat tulokset ja jopa laskea tulokset aikaisemmat menetelmäkutsut. Tämä käyttäytyminen on erittäin mahdotonta hyväksyä, jos käytät satunnaisia ​​lukuja turvallisuuteen, taloudellisiin tarkoituksiin jne. Crypto RNG:t toimivat huomattavasti hitaammin kuin Random-luokka, mutta ne luovat numerosarjan, joista jokainen on riippumattomampi ja arvaamattomampi muiden arvoista.

Useimmissa tapauksissa huono suorituskyky ei ole sopimusten katkaisija - huono API on. RandomNumberGenerator on suunniteltu luomaan tavuja - siinä kaikki. Vertaa tätä Random-luokan menetelmiin, joissa on mahdollista saada satunnainen kokonaisluku, murtoluku ja myös joukko tavuja. Toinen hyödyllinen ominaisuus on kyky saada satunnaisluku tietyllä alueella. Vertaa näitä mahdollisuuksia RandomNumberGeneratorin tuottamaan satunnaisten tavujen joukkoon. Voit korjata tilanteen luomalla oman kääreen (wrapper) RandomNumberGeneratorin ympärille, joka muuntaa satunnaiset tavut "käteväksi" tulokseksi, mutta tämä ratkaisu ei ole triviaali.

Useimmissa tapauksissa Random-luokan "heikkous" on kuitenkin hyvä, jos pystyt ratkaisemaan artikkelin alussa kuvatun ongelman. Katsotaan mitä voimme tehdä täällä.

Käytä yhtä Random-luokan esiintymää useisiin puheluihin
Tässä se on, ongelman ratkaisun juuri on käyttää vain yhtä Random-instanssia luotaessa useita satunnaislukuja käyttämällä Random.Next. Ja se on hyvin yksinkertaista - katso, kuinka voit muuttaa yllä olevan koodin:
// Tämä koodi on parempi Random rng = new Random(); for (int i = 0; i< 100; i++) { Console.WriteLine(GenerateDigit(rng)); } ... static int GenerateDigit(Random rng) { // Предположим, что здесь много логики return rng.Next(10); }
Nyt jokaisella iteraatiolla on eri numerot... mutta siinä ei vielä kaikki. Mitä tapahtuu, jos kutsumme tätä koodilohkoa kahdesti peräkkäin? Aivan oikein, luomme kaksi satunnaista esiintymää samalla siemenellä ja saamme kaksi identtistä satunnaislukujoukkoa. Jokaisen sarjan numerot ovat erilaisia, mutta nämä joukot ovat keskenään samansuuruisia.

On kaksi tapaa ratkaista ongelma. Ensinnäkin emme voi käyttää ilmentymää, vaan staattista kenttää, joka sisältää sattumanvaraisen esiintymän, ja sitten yllä oleva koodinpätkä luo vain yhden ilmentymän ja käyttää sitä kutsuen sitä niin monta kertaa kuin on tarpeen. Toiseksi, voimme kokonaan poistaa satunnaisen ilmentymän luomisen sieltä siirtämällä sen "korkeammaksi", mieluiten ohjelman "huipulle", jossa luodaan yksi satunnainen ilmentymä, jonka jälkeen se lähetetään kaikkiin paikkoihin. joissa tarvitaan satunnaislukuja. Tämä on hieno idea, joka ilmaistaan ​​kauniisti riippuvuuksilla, mutta se toimii niin kauan kuin käytämme vain yhtä säiettä.

Langan turvallisuus

Random-luokka ei ole lankaturvallinen. Ottaen huomioon, kuinka paljon haluamme luoda yksittäistä ilmentymää ja käyttää sitä läpi ohjelman koko sen suoritusajan (singleton, hei!), lankojen turvallisuuden puutteesta tulee todellinen tuska. Loppujen lopuksi, jos käytämme yhtä esiintymää samanaikaisesti useissa säikeissä, on mahdollista, että sen sisäinen tila nollataan, ja jos näin tapahtuu, siitä hetkestä lähtien ilmentymä tulee hyödyttömäksi.

Jälleen on kaksi tapaa ratkaista ongelma. Ensimmäinen polku sisältää edelleen yhden esiintymän käytön, mutta tällä kertaa resurssien lukituksen näytön kautta. Tätä varten sinun on luotava satunnaisen ympärille kääre, joka kääriä menetelmiensä kutsut lukituslauseeseen, mikä takaa soittajalle eksklusiivisen pääsyn esiintymään. Tämä polku on huono, koska se heikentää suorituskykyä säikeintensiivisissä skenaarioissa.

Toinen tapa, jonka kuvailen alla, on käyttää yhtä esiintymää säiettä kohti. Ainoa asia, joka meidän on varmistettava, on, että käytämme erilaisia ​​​​siemeniä luodessasi ilmentymiä, joten emme voi käyttää oletuskonstruktoreita. Muuten tämä tie on suhteellisen suoraviivainen.

Turvallinen palveluntarjoaja

Onneksi uusi yleinen luokka ThreadLocal .NET 4:ssä käyttöön otetun palvelun avulla on erittäin helppoa kirjoittaa palveluntarjoajia, jotka tarjoavat yhden esiintymän säiettä kohti. Sinun tarvitsee vain välittää edustaja ThreadLocal-konstruktorille, joka viittaa itse instanssimme arvon saamiseen. Tässä tapauksessa päätin käyttää yhtä siemenarvoa, alustaen sen Environment.TickCountilla (tämä on täsmälleen, miten parametriton Random-konstruktori toimii). Seuraavaksi tuloksena olevaa merkkien määrää kasvatetaan aina, kun meidän on hankittava uusi satunnainen esiintymä erilliselle säikeelle.

Alla oleva luokka on täysin staattinen ja sisältää vain yhden julkisen (avoin) menetelmän GetThreadRandom. Tästä menetelmästä on tehty pikemminkin menetelmä kuin ominaisuus, lähinnä mukavuussyistä: tämä varmistaa, että kaikki luokat, jotka tarvitsevat Random-esiintymän, riippuvat Funcista (Delegaatti, joka osoittaa menetelmään, joka ei ota parametreja ja palauttaa Random-tyypin arvon), eikä itse Random-luokasta. Jos tyyppi on tarkoitettu ajamaan yhdessä säikeessä, se voi kutsua edustajaa hankkimaan yhden Random-instanssin ja käyttää sitä sitten koko ajan; jos tyypin on toimittava monisäikeisissä skenaarioissa, se voi kutsua edustajaa aina, kun se tarvitsee satunnaislukugeneraattorin. Alla oleva luokka luo niin monta Random-luokan esiintymää kuin on säiettä, ja jokainen esiintymä alkaa eri alkuarvosta. Jos joudumme käyttämään satunnaislukutoimittajaa riippuvuutena muissa tyypeissä, voimme tehdä tämän: new TypeThatNeedsRandom(RandomProvider.GetThreadRandom) . No, tässä itse koodi:
käyttämällä järjestelmää; käyttäen System.Threading; julkinen staattinen luokka RandomProvider ( yksityinen staattinen int siemen = Environment.TickCount; yksityinen staattinen ThreadLocal randomWrapper = uusi ThreadLocal (() => new Random(Interlocked.Increment(ref seed))); julkinen staattinen Satunnainen GetThreadRandom() ( palauttaa randomWrapper.Value; ) )
Tarpeeksi yksinkertainen, eikö? Tämä johtuu siitä, että kaikki koodi on tarkoitettu tuottamaan oikea Random-esiintymä. Kun ilmentymä on luotu ja palautettu, sillä ei ole väliä, mitä teet sille seuraavaksi: kaikki muut ilmentymien julkaisut ovat täysin riippumattomia nykyisestä. Tietysti asiakaskoodissa on porsaanreikä haitallista väärinkäyttöä varten: se voi ottaa yhden Random-instanssin ja siirtää sen muille säikeille sen sijaan, että kutsuisi RandomProvider-palveluamme näissä muissa säikeissä.

Käyttöliittymän suunnitteluongelmat

Yksi ongelma on edelleen olemassa: käytämme heikosti suojattua satunnaislukugeneraattoria. Kuten aiemmin mainittiin, RandomNumberGeneratorissa on paljon turvallisempi versio RNG:stä, jonka toteutus on RNGCryptoServiceProvider-luokassa. Sen API on kuitenkin melko vaikea käyttää vakioskenaarioissa.

Olisi erittäin mukavaa, jos RNG-palveluntarjoajilla olisi erilliset "satunnaisuuden lähteet". Tässä tapauksessa meillä voisi olla yksi, yksinkertainen ja kätevä API, jota sekä epävarma mutta nopea toteutus että turvallinen mutta hidas toteutus tukevat. No, unelmoinnista ei ole haittaa. Ehkä samanlaisia ​​toimintoja tulee näkyviin tuleviin .NET Frameworkin versioihin. Ehkä joku muu kuin Microsoft tarjoaa oman sovittimen toteutuksensa. (Valitettavasti en ole se henkilö...sellaisen oikein toteuttaminen on yllättävän monimutkaista.) Voit myös luoda oman luokan johtamalla Randomista ja ohittamalla Sample- ja NextBytes-metodit, mutta ei ole selvää miten niiden pitäisi työ tai jopa oma toteutusnäyte voi olla paljon monimutkaisempi kuin miltä näyttää. Ehkä ensi kerralla…

Artikkelin käännös Jon Skeeten satunnaiset luvut, jotka tunnetaan laajalti kapeissa piireissä. Pysähdyin tähän artikkeliin, koska törmäsin itsekin siinä kuvattuun ongelmaan.

Selataan aiheita mukaan .NETTO Ja C# StackOverflow-verkkosivustolla voit nähdä lukemattomia kysymyksiä, joissa mainitaan sana "random", jotka itse asiassa herättävät saman ikuisen ja "tuhoutumattoman" kysymyksen: miksi System.Random satunnaislukugeneraattori "ei toimi" ja miten " korjaa se"" Tämä artikkeli on omistettu tämän ongelman pohtimiseen ja sen ratkaisemiseen.

Ongelman muotoilu

StackOverflow:ssa, uutisryhmissä ja postituslistoissa, kaikki "satunnaista"-aihetta koskevat kysymykset kuulostavat tältä:
Käytän Random.Nextiä useiden satunnaislukujen luomiseen, mutta menetelmä palauttaa saman numeron, kun sitä kutsutaan useita kertoja. Numero muuttuu aina, kun sovellus käynnistetään, mutta yhden ohjelman suorituksen aikana se on vakio.

Esimerkkikoodi on jotain tämän kaltaista:
// Huono koodi! Älä käytä! for (int i = 0; i< 100; i++) { Console.WriteLine(GenerateDigit()); } ... static int GenerateDigit() { Random rng = new Random(); // Предположим, что здесь много логики return rng.Next(10); }
Joten mikä tässä on vialla?

Selitys

Random-luokka ei ole todellinen satunnaislukugeneraattori, se sisältää generaattorin pseudo satunnaisia ​​numeroita. Jokainen Random-luokan esiintymä sisältää jonkin sisäisen tilan, ja kun Next (tai NextDouble tai NextBytes) -metodia kutsutaan, menetelmä käyttää tätä tilaa palauttaakseen satunnaisena näkyvän luvun. Sisäistä tilaa muutetaan sitten niin, että seuraavan kerran, kun Next kutsutaan, se palauttaa erilaisen näennäisesti satunnaisen luvun kuin aiemmin palautettu.

Kaikki Random-luokan "sisäiset". täysin deterministinen. Tämä tarkoittaa, että jos otat useita esiintymiä Random-luokasta samalla alkutilalla, joka määritetään konstruktoriparametrin kautta siemen, ja kutsu jokaiselle tapaukselle tiettyjä menetelmiä samassa järjestyksessä ja samoilla parametreilla, niin lopulta saat samat tulokset.

Joten mikä vika yllä olevassa koodissa on? Huono asia on, että käytämme uutta Random-luokan esiintymää silmukan jokaisessa iteraatiossa. Random-konstruktori, joka ei ota parametreja, ottaa siemenensä nykyisen päivämäärän ja kellonajan. Iteraatiot silmukassa "vierivät" niin nopeasti, että järjestelmän aika "ei ehdi muuttua" niiden valmistuttua; siten kaikki Random-instanssit saavat saman arvon kuin niiden alkutila ja palauttavat siksi saman näennäissatunnaisen luvun.

Kuinka korjata se?

Ongelmaan on monia ratkaisuja, joista jokaisella on omat hyvät ja huonot puolensa. Tarkastellaan muutamia niistä.
Kryptografisen satunnaislukugeneraattorin käyttäminen
.NET sisältää abstraktin luokan RandomNumberGenerator, josta kaikkien kryptografisten satunnaislukugeneraattoreiden (jäljempänä kryptoRNG:t) on perittävä. .NET sisältää myös yhden näistä toteutuksista - täytä RNGCryptoServiceProvider-luokka. Krypto-RNG:n ideana on, että vaikka se on edelleen näennäissatunnaislukugeneraattori, se tarjoaa melko vahvan tulosten arvaamattomuuden. RNGCryptoServiceProvider käyttää useita entropialähteitä, jotka ovat pohjimmiltaan "kohinaa" tietokoneessasi, ja sen luomaa numerosarjaa on erittäin vaikea ennustaa. Lisäksi "tietokoneen sisäistä" kohinaa voidaan käyttää ei vain alkutilana, vaan myös seuraaviin satunnaisnumeroihin soitettujen puhelujen välillä; Näin ollen vaikka luokan nykyinen tila olisi tiedossa, ei riitä laskemaan sekä seuraavat tulevaisuudessa syntyneet että aiemmin luodut luvut. Itse asiassa tarkka käyttäytyminen riippuu toteutuksesta. Lisäksi Windows voi käyttää erikoislaitteistoa, joka on "todellisen satunnaisuuden" lähde (esimerkiksi radioaktiivisen isotoopin hajoamisanturi) luodakseen entistä turvallisempia ja luotettavampia satunnaislukuja.

Verrataan tätä aiemmin käsiteltyyn Random-luokkaan. Oletetaan, että soitit Random.Next(100) kymmenen kertaa ja tallensit tulokset. Jos sinulla on tarpeeksi laskentatehoa, voit pelkästään näiden tulosten perusteella laskea alkutilan (siemen), jolla Random-instanssi luotiin, ennustaa Random.Next(100)-kutsun seuraavat tulokset ja jopa laskea tulokset aikaisemmat menetelmäkutsut. Tämä käyttäytyminen on erittäin mahdotonta hyväksyä, jos käytät satunnaisia ​​lukuja turvallisuuteen, taloudellisiin tarkoituksiin jne. Crypto RNG:t toimivat huomattavasti hitaammin kuin Random-luokka, mutta ne luovat numerosarjan, joista jokainen on riippumattomampi ja arvaamattomampi muiden arvoista.

Useimmissa tapauksissa huono suorituskyky ei ole sopimusten katkaisija - huono API on. RandomNumberGenerator on suunniteltu luomaan tavuja - siinä kaikki. Vertaa tätä Random-luokan menetelmiin, joissa on mahdollista saada satunnainen kokonaisluku, murtoluku ja myös joukko tavuja. Toinen hyödyllinen ominaisuus on kyky saada satunnaisluku tietyllä alueella. Vertaa näitä mahdollisuuksia RandomNumberGeneratorin tuottamaan satunnaisten tavujen joukkoon. Voit korjata tilanteen luomalla oman kääreen (wrapper) RandomNumberGeneratorin ympärille, joka muuntaa satunnaiset tavut "käteväksi" tulokseksi, mutta tämä ratkaisu ei ole triviaali.

Useimmissa tapauksissa Random-luokan "heikkous" on kuitenkin hyvä, jos pystyt ratkaisemaan artikkelin alussa kuvatun ongelman. Katsotaan mitä voimme tehdä täällä.

Käytä yhtä Random-luokan esiintymää useisiin puheluihin
Tässä se on, ongelman ratkaisun juuri on käyttää vain yhtä Random-instanssia luotaessa useita satunnaislukuja käyttämällä Random.Next. Ja se on hyvin yksinkertaista - katso, kuinka voit muuttaa yllä olevan koodin:
// Tämä koodi on parempi Random rng = new Random(); for (int i = 0; i< 100; i++) { Console.WriteLine(GenerateDigit(rng)); } ... static int GenerateDigit(Random rng) { // Предположим, что здесь много логики return rng.Next(10); }
Nyt jokaisella iteraatiolla on eri numerot... mutta siinä ei vielä kaikki. Mitä tapahtuu, jos kutsumme tätä koodilohkoa kahdesti peräkkäin? Aivan oikein, luomme kaksi satunnaista esiintymää samalla siemenellä ja saamme kaksi identtistä satunnaislukujoukkoa. Jokaisen sarjan numerot ovat erilaisia, mutta nämä joukot ovat keskenään samansuuruisia.

On kaksi tapaa ratkaista ongelma. Ensinnäkin emme voi käyttää ilmentymää, vaan staattista kenttää, joka sisältää sattumanvaraisen esiintymän, ja sitten yllä oleva koodinpätkä luo vain yhden ilmentymän ja käyttää sitä kutsuen sitä niin monta kertaa kuin on tarpeen. Toiseksi, voimme kokonaan poistaa satunnaisen ilmentymän luomisen sieltä siirtämällä sen "korkeammaksi", mieluiten ohjelman "huipulle", jossa luodaan yksi satunnainen ilmentymä, jonka jälkeen se lähetetään kaikkiin paikkoihin. joissa tarvitaan satunnaislukuja. Tämä on hieno idea, joka ilmaistaan ​​kauniisti riippuvuuksilla, mutta se toimii niin kauan kuin käytämme vain yhtä säiettä.

Langan turvallisuus

Random-luokka ei ole lankaturvallinen. Ottaen huomioon, kuinka paljon haluamme luoda yksittäistä ilmentymää ja käyttää sitä läpi ohjelman koko sen suoritusajan (singleton, hei!), lankojen turvallisuuden puutteesta tulee todellinen tuska. Loppujen lopuksi, jos käytämme yhtä esiintymää samanaikaisesti useissa säikeissä, on mahdollista, että sen sisäinen tila nollataan, ja jos näin tapahtuu, siitä hetkestä lähtien ilmentymä tulee hyödyttömäksi.

Jälleen on kaksi tapaa ratkaista ongelma. Ensimmäinen polku sisältää edelleen yhden esiintymän käytön, mutta tällä kertaa resurssien lukituksen näytön kautta. Tätä varten sinun on luotava satunnaisen ympärille kääre, joka kääriä menetelmiensä kutsut lukituslauseeseen, mikä takaa soittajalle eksklusiivisen pääsyn esiintymään. Tämä polku on huono, koska se heikentää suorituskykyä säikeintensiivisissä skenaarioissa.

Toinen tapa, jonka kuvailen alla, on käyttää yhtä esiintymää säiettä kohti. Ainoa asia, joka meidän on varmistettava, on, että käytämme erilaisia ​​​​siemeniä luodessasi ilmentymiä, joten emme voi käyttää oletuskonstruktoreita. Muuten tämä tie on suhteellisen suoraviivainen.

Turvallinen palveluntarjoaja

Onneksi uusi yleinen luokka ThreadLocal .NET 4:ssä käyttöön otetun palvelun avulla on erittäin helppoa kirjoittaa palveluntarjoajia, jotka tarjoavat yhden esiintymän säiettä kohti. Sinun tarvitsee vain välittää edustaja ThreadLocal-konstruktorille, joka viittaa itse instanssimme arvon saamiseen. Tässä tapauksessa päätin käyttää yhtä siemenarvoa, alustaen sen Environment.TickCountilla (tämä on täsmälleen, miten parametriton Random-konstruktori toimii). Seuraavaksi tuloksena olevaa merkkien määrää kasvatetaan aina, kun meidän on hankittava uusi satunnainen esiintymä erilliselle säikeelle.

Alla oleva luokka on täysin staattinen ja sisältää vain yhden julkisen (avoin) menetelmän GetThreadRandom. Tästä menetelmästä on tehty pikemminkin menetelmä kuin ominaisuus, lähinnä mukavuussyistä: tämä varmistaa, että kaikki luokat, jotka tarvitsevat Random-esiintymän, riippuvat Funcista (Delegaatti, joka osoittaa menetelmään, joka ei ota parametreja ja palauttaa Random-tyypin arvon), eikä itse Random-luokasta. Jos tyyppi on tarkoitettu ajamaan yhdessä säikeessä, se voi kutsua edustajaa hankkimaan yhden Random-instanssin ja käyttää sitä sitten koko ajan; jos tyypin on toimittava monisäikeisissä skenaarioissa, se voi kutsua edustajaa aina, kun se tarvitsee satunnaislukugeneraattorin. Alla oleva luokka luo niin monta Random-luokan esiintymää kuin on säiettä, ja jokainen esiintymä alkaa eri alkuarvosta. Jos joudumme käyttämään satunnaislukutoimittajaa riippuvuutena muissa tyypeissä, voimme tehdä tämän: new TypeThatNeedsRandom(RandomProvider.GetThreadRandom) . No, tässä itse koodi:
käyttämällä järjestelmää; käyttäen System.Threading; julkinen staattinen luokka RandomProvider ( yksityinen staattinen int siemen = Environment.TickCount; yksityinen staattinen ThreadLocal randomWrapper = uusi ThreadLocal (() => new Random(Interlocked.Increment(ref seed))); julkinen staattinen Satunnainen GetThreadRandom() ( palauttaa randomWrapper.Value; ) )
Tarpeeksi yksinkertainen, eikö? Tämä johtuu siitä, että kaikki koodi on tarkoitettu tuottamaan oikea Random-esiintymä. Kun ilmentymä on luotu ja palautettu, sillä ei ole väliä, mitä teet sille seuraavaksi: kaikki muut ilmentymien julkaisut ovat täysin riippumattomia nykyisestä. Tietysti asiakaskoodissa on porsaanreikä haitallista väärinkäyttöä varten: se voi ottaa yhden Random-instanssin ja siirtää sen muille säikeille sen sijaan, että kutsuisi RandomProvider-palveluamme näissä muissa säikeissä.

Käyttöliittymän suunnitteluongelmat

Yksi ongelma on edelleen olemassa: käytämme heikosti suojattua satunnaislukugeneraattoria. Kuten aiemmin mainittiin, RandomNumberGeneratorissa on paljon turvallisempi versio RNG:stä, jonka toteutus on RNGCryptoServiceProvider-luokassa. Sen API on kuitenkin melko vaikea käyttää vakioskenaarioissa.

Olisi erittäin mukavaa, jos RNG-palveluntarjoajilla olisi erilliset "satunnaisuuden lähteet". Tässä tapauksessa meillä voisi olla yksi, yksinkertainen ja kätevä API, jota sekä epävarma mutta nopea toteutus että turvallinen mutta hidas toteutus tukevat. No, unelmoinnista ei ole haittaa. Ehkä samanlaisia ​​toimintoja tulee näkyviin tuleviin .NET Frameworkin versioihin. Ehkä joku muu kuin Microsoft tarjoaa oman sovittimen toteutuksensa. (Valitettavasti en ole se henkilö...sellaisen oikein toteuttaminen on yllättävän monimutkaista.) Voit myös luoda oman luokan johtamalla Randomista ja ohittamalla Sample- ja NextBytes-metodit, mutta ei ole selvää miten niiden pitäisi työ tai jopa oma toteutusnäyte voi olla paljon monimutkaisempi kuin miltä näyttää. Ehkä ensi kerralla…

Tunnisteet: C-satunnaisluku, C-satunnaisluku, satunnaislukugenerointi, RNG, pseudosatunnaisluvut, Monte Carlo -menetelmä

Pseudosatunnaiset luvut

Pseudosatunnaislukujen luominen on monimutkainen matemaattinen ongelma. Tämän artikkelin tarkoituksena ei ole käsitellä tätä aihetta. Seuraavassa käsite "satunnaisluku" tarkoittaa pseudosatunnaista, ellei toisin mainita.

Löydät esimerkkejä satunnaislukujen käytöstä kaikkialla. Pseudosatunnaislukuja käytetään suunnittelussa ja grafiikassa tasojen luomiseen tietokonepelit ja AI-simulaatio. Matemaattisissa algoritmeissa käytetään satunnaislukujoukkoja (katso Monte Carlon menetelmät).

On selvää, että satunnaislukujen generointiongelmaa ei voida ratkaista klassisella prosessorilla, koska tietokoneen toiminta on määritelmän mukaan determinististä. On kuitenkin mahdollista luoda hyvin pitkiä lukujoukkoja siten, että niiden jakaumalla on samat ominaisuudet kuin todella satunnaislukujen joukoilla.

On tärkeää, että tietyn ongelman ratkaisemiseksi sinun on valittava oikea generaattori tai ainakin tiedettävä sen ominaisuudet. Esimerkiksi fyysistä prosessia mallinnettaessa voidaan saada täysin erilaisia ​​ja usein virheellisiä tuloksia riippuen satunnaislukugeneraattorin valinnasta.

Katsotaanpa tavallista generaattoria.

#sisältää #sisältää #sisältää int main() ( int i, r; srand(42); for (i = 0; i< 10; i++) { r = rand(); printf("%d\n", r); } _getch(); return 0; }

Ensin sinun on alustettava satunnaislukugeneraattori (RNG tai RNG - satunnaislukugeneraattori), asetettava siemen, jonka perusteella generointi tapahtuu tulevaisuudessa. On tärkeää, että generaattori palauttaa samat luvut samalla alkuarvolla.

Srand(42);

Anna muuttujalle r satunnainen arvo

R = rand();

Arvo on välillä 0 - RAND_MAX.

Jotta saat uuden numerosarjan seuraavan käynnistyksen yhteydessä, sinun on alustettava generaattori joka kerta erilaisia ​​merkityksiä. Voit käyttää esimerkiksi järjestelmän aikaa:

Srand(aika(NULL));

Srand(_getpid());

Process.h-kirjaston getpid-funktio palauttaa prosessitunnuksen (voit myös käyttää getpid-funktiota, funktion ei-POSIX-versiota).

Keskirajalause

On erittäin tärkeää välittömästi muistuttaa tai ottaa käyttöön keskusrajalause. Epävirallinen määritelmä: heikosti riippuvaisten satunnaismuuttujien summan jakauma on normaali. Sormenmuotoinen selitys: jos lisäät useita satunnaismuuttujia niiden jakautumisesta riippumatta, summan jakautuminen on normaali. Voit usein nähdä tämän kaltaisen koodin

#sisältää #sisältää #sisältää int main() ( int i, r, r1, r2, r3; srand(aika(NULL)); r1 = rand(); r2 = rand(); r3 = rand(); r = (r1 + r2 + r3 ) / 3; printf("%d", r); _getch(); return 0; )

Luodaan satunnaislukuja tietyllä aikavälillä

Ensin saamme satunnaisluvun nollasta yhteen:

Vakio kelluva RAND_MAX_F = RAND_MAX; float get_rand() ( return rand() / RAND_MAX_F; )

Saadaksesi luvun nollasta N:ään, kerro N satunnaisluvulla nollasta yhteen. Saadaksemme satunnaisluvun M:stä N:ään siirrämme tuloksena olevaa lukua M:llä.

Float get_rand_range(const float min, const float max) ( return get_rand() * (max - min) + min; )

Kokonaisluvun saamiseksi otamme jaon loppuosan välin pituudella. Mutta jaon loppuosa palauttaa luvun yhden pienemmän kuin välimme, joten lisäämme sitä yhdellä:

Int get_rand_range_int(const int min, const int max) ( return rand() % (max - min + 1) + min; )

Esimerkki satunnaislukujen käyttämisestä integraalin laskemiseen. Otetaan jokin yhden muuttujan tasainen funktio. Rajataan se neliöön a:sta b:hen ja 0:sta johonkin pisteeseen, joka on selvästi suurempi kuin funktiomme.

Heitämme satunnaisesti pisteitä ruudullemme. Jos ne sijaitsevat funktion yläpuolella (näkyy kuvassa vihreinä risteinä), kohdistamme ne ensimmäiseen ryhmään A, jos funktion alapuolelle (kuvassa punaiset), kohdistamme ne toiseen ryhmään B. pisteiden sijainti on satunnainen ja jakautunut tasaisesti (standardista generaattori antaa tasaisen jakauman. Tämä yksinkertainen esimerkki muuten osoittaa jo, kuinka tärkeää RNG:n ominaisuuksien tunteminen on). Silloin punaisten pisteiden suhde pisteiden kokonaismäärään on yhtä suuri kuin kaavion alla olevan alueen suhde kokonaispinta-alaan. Ja kokonaispinta-ala on neliö (b-a) q:lla.

Src="/images/c_random_integral.png" alt=" Kaikki mikä satunnaisesti putoaa funktiomme yläpuolelle, on vihreää, kaikki alla on punaista.
Vihreän ja punaisen suhde on yhtä suuri kuin kaavion yläpuolella olevan alueen suhde kaavion alapuolelle."> Всё, что случайно попадает выше нашей функции - зелёное, всё что ниже - красное. !}
Vihreän ja punaisen suhde on yhtä suuri kuin kaavion yläpuolella olevan alueen suhde kaavion alapuolelle.

Sovelletaan laskelmiamme - etsitään funktion x^2 integraali väliltä 0-2 kahdella tavalla.

#sisältää #sisältää #sisältää #sisältää #sisältää const float RAND_MAX_F = RAND_MAX; float get_rand() ( return rand() / RAND_MAX_F; ) float get_rand_range(const float min, const float max) ( return get_rand() * (max - min) + min; ) #define ROUNDS 1000 float fun(float x) ( paluu x * x; ) float square_square(float a, float b, float q) ( float h = (b - a) / (float) ROUNDS; float summa = 0; for (; a< b; a += h) { sum += fun(a) * h; } return sum; } float rand_square(float a, float b, float q) { float res; float x, y; int i; int lower = 0; float ratio; float square; srand(time(NULL)); for (i = 0; i < ROUNDS; i++) { x = get_rand_range(a, b); y = get_rand_range(0, q); res = fun(x); if (res >y) ( alempi++; ) ) suhde = (float)alempi / (kellu)PYÖRÄYS; neliö = (b - a) * q * -suhde; paluu neliö; ) int main() ( float abs_ans = 2.66667f; float sr = rand_square(0, 2, 4); float ss = square_square(0, 2, 4); printf("Rounds = %d\n", ROUNDS); printf("Sa = %.5f\n", abs_ans); printf("Sr = %.5f\n", sr); printf("Ss = %.5f\n", ss); printf("dr = %.5f\n", fabs(sr - abs_ans)); printf("ds = %.5f\n", fabs(ss - abs_ans)); _getch(); return 0; )

Pelaa ROUNDS-arvolla, muuta sitä ja katso kuinka laskennan tarkkuus muuttuu.

Todellisten satunnaislukujen luominen

Todellisten satunnaislukujen generoimiseen käytetään joihinkin satunnaisiin fyysisiin prosesseihin perustuvia generaattoreita. Esimerkiksi lämpömelusta, radioaktiivisen aineen fissioiden lukumäärän laskemisesta, ilmakehän melusta jne. Tällaisten generaattoreiden haittana on alhainen nopeus työ (sekunnissa luotujen numeroiden lukumäärä); tietysti tällaiset generaattorit ovat yleensä erillinen laite.

Pseudosatunnaislukuja luovalla funktiolla on prototyyppi stdlib.h-kirjastotiedostossa:

1
2
3
4
5
6

unsigned long int next = 1;
int rand (tyhjä)
{
seuraava = seuraava * 1103515245;
paluu ((allekirjoittamaton int )(seuraava / 65536) * 2768);
}


Funktio rand() ei ota argumentteja, vaan toimii seuraavan muuttujan kanssa globaalilla laajuudella.

Jos sinun on luotava sarja alueella , silloin käytetään kaavaa:

Luku = rand()%(M2-M1+1) + M1;

Missä Määrä– luotu numero. M2-M1+1– täydellinen lukujen esitysalue. M1– määritetyn alueen siirtymä suhteessa 0:aan; % - divisioonan loppuosa.

Jos esimerkiksi haluat luoda sekvenssin alueella [-10;10], funktiokutsu näyttää tältä

Luku = rand()%(10+10+1)-10

Luku = rand()%(21)-10

Kun saamme jakojäännöksen 21:llä, saamme luvun 0 - 20. Vähentämällä tuloksena olevasta luvusta 10, saadaan luku halutulla alueella [-10;10].

Kuitenkin rand()-funktion luoma sekvenssi näyttää samalta joka kerta, kun ohjelma ajetaan.

Eri sekvenssien luomiseksi joka kerta, kun ohjelma käynnistetään, on välttämätöntä alustaa globaali muuttuja seuraavaksi muulla arvolla kuin 1. Käytä tätä tarkoitusta varten funktiota
void srand (signed int seed)
(seuraava = siemen;)

Sen varmistamiseksi, että seuraavan alustus on erilainen joka kerta, kun ohjelma käynnistetään, siemenargumenttina käytetään useimmiten nykyistä aikaa.

Esimerkki Täytä 20 elementin joukko satunnaisluvuilla välillä 0-99.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

#sisältää
#sisältää
#sisältää
#define KOKO 20
int main() (
int a;
srand(aika(NULL ));
for (int i = 0; i {
a[i] = rand() % 100;
printf("%d " , a[i]);
}
getchar();
paluu 0;
}


Toteutustulos

Usein tehtävänä on järjestää olemassa oleva arvojoukko satunnaiseen järjestykseen. Tähän tarkoitukseen käytetään myös. Tämä luo taulukon ja täyttää sen arvoilla.
Itse sekoitusmenettely on seuraava. Kaksi taulukkoindeksiä luodaan satunnaisesti, ja elementtien arvot tuloksena olevilla indekseillä vaihdetaan. Toimenpide toistetaan vähintään N kertaa, missä N on taulukon elementtien lukumäärä.
Harkitse esimerkiksi 20 arvon sekoittamista (1:stä 20:een) ja menettelyn toistamista 20 kertaa.

Toteutus C

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

#sisältää
#sisältää
#sisältää
#define KOKO 20
int main() (
int a;
srand(aika(NULL ));

for (int i = 0; i< SIZE; i++)
{
a[i] = i + 1;
printf("%2d" , a[i]);
}
for (int i = 0; i< SIZE; i++)
{
// Luo satunnaisesti kaksi elementtiindeksiä
int ind1 = rand() % 20;
int ind2 = rand() % 20;
// ja vaihtaa elementtejä näillä indekseillä
sisälämpötila = a;
a = a;
a = lämpötila;
}
printf("\n" );

for (int i = 0; i< SIZE; i++)
printf("%2d" , a[i]);
getchar();
paluu 0;
}


Toteutustulos


Usein syntyy tehtävä valita satunnaisesti aiemmin määritettyjä taulukon elementtejä. Lisäksi on varmistettava, että näitä elementtejä valittaessa ei tapahdu toistoa.
Tämän valinnan algoritmi on seuraava:

  • Valitsemme satunnaisesti taulukkoelementin indeksin
  • Jos elementti, jolla on sama indeksi, on jo valittu, siirry oikealle, kunnes saavutamme seuraavan valitsemattoman elementin. Samalla varmistamme, että "liike oikealle" ei ylitä taulukon rajoja. Jos taulukon rajojen ulkopuolella havaitaan, alamme tarkastella taulukon elementtejä alusta.
  • Elementin valitseminen
  • Elementin kiinnittäminen valitun mukaisesti
  • Toista nämä vaiheet kaikille muille elementeille

Toteutukset C:ssä
Tuloksena saadaan uusi taulukko b, joka muodostuu taulukon a elementtien satunnaisesta valinnasta.

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

#sisältää
#sisältää
#sisältää
#define KOKO 20
int main() (
int a;
int b; // tuloksena oleva taulukko
srand(aika(NULL ));
// Täytä taulukko peräkkäisillä arvoilla 1-20
for (int i = 0; i< SIZE; i++)
{
a[i] = i + 1;
printf("%2d" , a[i]);
}

for (int i = 0; i< SIZE; i++)
{
int ind = rand() % 20; // valitse mielivaltainen indeksi
kun taas (a == -1) // kun elementti on "valittu"
{
ind++; // siirry oikealle
ind % = 20; // jos saavutamme oikean rajan, palaa alkuun
}
b[i] = a; // kirjoittaa taulukon seuraava elementti b
a = -1; // merkitse taulukon elementti a "valituksi"
}
printf("\n" );
// Tulostaa tuloksena olevan taulukon
for (int i = 0; i< SIZE; i++)
printf("%2d" , b[i]);
getchar();
paluu 0;
}


Toteutustulos

Terveisiä kaikille, jotka pysähtyivät. Tämä lyhyt huomautus sisältää muutaman sanan näennäissatunnaisten lukujen luomisesta C/C++:ssa. Erityisesti siitä, kuinka työskennellä yksinkertaisimman sukupolven funktion - rand() - kanssa.

rand()-funktio

Löytyy C++-standardikirjastosta (stdlib.h). Luo ja palauttaa näennäissatunnaisen luvun välillä 0 - RAND_MAX. Tämä vakio voi vaihdella kääntäjän tai prosessorin arkkitehtuurista riippuen, mutta se on yleensä etumerkittömän int-tietotyypin maksimiarvo. Se ei hyväksy parametreja eikä ole koskaan hyväksynyt.

Jotta sukupolvi tapahtuisi, sinun on asetettava siemen käyttämällä aputoimintoa samasta kirjastosta - srand(). Se ottaa luvun ja asettaa sen satunnaisluvun luomisen aloituspisteeksi. Jos siementä ei ole asetettu, saamme aina ohjelman käynnistyessä sama satunnaisia ​​numeroita. Ilmeisin ratkaisu on lähettää nykyinen järjestelmäaika sinne. Tämä voidaan tehdä käyttämällä aika(NULL)-funktiota. Tämä toiminto on time.h-kirjastossa.

Olemme selvittäneet teorian, löytäneet kaikki satunnaislukujen generointifunktiot, nyt generoidaan ne.

Esimerkki satunnaislukujen generointifunktion rand() käytöstä

#sisältää #sisältää #sisältää käyttäen nimiavaruutta std; int main() ( srand(aika(NULL)); for(int i = 0; i< 10; i++) { cout << rand() << endl; } return 0; }

Rand loi meille 10 satunnaislukua. Voit varmistaa, että ne ovat satunnaisia ​​suorittamalla ohjelman uudelleen ja uudelleen. Mutta tämä ei riitä, joten meidän on puhuttava etäisyydestä.

Aseta alueen rajat arvolle rand()

Luodaksesi luvun välillä A–B, sinun on kirjoitettava tämä:

A + rand() % ((B + 1) - A);

Raja-arvot voivat olla myös negatiivisia, minkä avulla voit luoda negatiivisen satunnaisluvun, katsotaanpa esimerkkiä.

#sisältää #sisältää #sisältää käyttäen nimiavaruutta std; int main() ( srand(aika(NULL)); int A = -2; int B = 8; for(int i = 0; i< 100; i++) { cout << A + rand() % ((B + 1) - A) << endl; } return 0; }