تولید اعداد تصادفی در زبان C. اعداد تصادفی در مولد اعداد تصادفی استاندارد C C

ترجمه مقاله اعداد تصادفی توسط Jon Skeete که در محافل باریک شناخته شده است. من در این مقاله متوقف شدم زیرا در یک زمان من خودم با مشکل توضیح داده شده در آن مواجه شدم.

مرور موضوعات توسط دات نتو سی شارپدر وب‌سایت StackOverflow، می‌توانید سؤالات بی‌شماری با ذکر کلمه «تصادفی» مشاهده کنید، که در واقع همان سؤال ابدی و «تخریب‌ناپذیر» را ایجاد می‌کند: چرا مولد اعداد تصادفی System.Random «کار نمی‌کند» و چگونه «اصلاح کنیم». آن"" این مقاله به بررسی این مشکل و راه های حل آن اختصاص دارد.

بیان مشکل

در StackOverflow، در گروه‌های خبری و لیست‌های پستی، همه سؤالات در مورد موضوع "تصادفی" چیزی شبیه به این هستند:
من از Random.Next برای تولید اعداد تصادفی متعدد استفاده می‌کنم، اما این روش زمانی که چندین بار فراخوانی می‌شود همان عدد را برمی‌گرداند. هر بار که برنامه راه اندازی می شود، عدد تغییر می کند، اما در یک اجرای برنامه ثابت است.

یک کد مثال چیزی شبیه به این است:
// کد بد! استفاده نکنید! برای (int i = 0; i< 100; i++) { Console.WriteLine(GenerateDigit()); } ... static int GenerateDigit() { Random rng = new Random(); // Предположим, что здесь много логики return rng.Next(10); }
پس اینجا چه اشکالی دارد؟

توضیح

کلاس Random یک مولد اعداد تصادفی واقعی نیست، بلکه شامل یک مولد است شبهاعداد تصادفی هر نمونه از کلاس Random شامل یک حالت داخلی است و زمانی که متد Next (یا NextDouble یا NextBytes) فراخوانی می شود، متد از آن حالت برای برگرداندن عددی استفاده می کند که تصادفی ظاهر می شود. سپس حالت داخلی تغییر می‌کند تا دفعه بعد که Next فراخوانی می‌شود، یک عدد ظاهراً تصادفی متفاوت از آنچه قبلاً برگردانده شده است، برمی‌گرداند.

همه «داخلی‌های» کلاس Random کاملا قطعی. این بدان معناست که اگر چندین نمونه از کلاس Random را با همان حالت اولیه بگیرید که از طریق پارامتر سازنده مشخص شده است. دانهو برای هر نمونه متدهای خاصی را به همان ترتیب و با پارامترهای یکسان فراخوانی کنید، سپس در پایان نتایج یکسانی خواهید داشت.

خب کد بالا چه اشکالی دارد؟ نکته بد این است که ما از یک نمونه جدید از کلاس Random در داخل هر تکرار حلقه استفاده می کنیم. سازنده تصادفی، که هیچ پارامتری را نمی گیرد، تاریخ و زمان فعلی را به عنوان بذر خود می گیرد. تکرارها در حلقه به سرعت "پیمایش" می شوند که زمان سیستم "زمانی برای تغییر نخواهد داشت" پس از تکمیل آنها. بنابراین، همه نمونه‌های Random همان مقدار حالت اولیه خود را دریافت می‌کنند و بنابراین همان عدد شبه تصادفی را برمی‌گردانند.

چگونه این را رفع کنیم؟

راه حل های زیادی برای حل این مشکل وجود دارد که هر کدام مزایا و معایب خاص خود را دارند. ما به چند مورد از آنها نگاه خواهیم کرد.
استفاده از مولد اعداد تصادفی رمزنگاری
دات نت حاوی یک کلاس انتزاعی RandomNumberGenerator است که تمامی پیاده سازی های مولد اعداد تصادفی رمزنگاری (که از این پس به عنوان cryptoRNG نامیده می شود) باید از آن به ارث برسند. .NET همچنین شامل یکی از این پیاده سازی ها است - کلاس RNGCryptoServiceProvider را ملاقات کنید. ایده رمزارز RNG این است که حتی اگر هنوز یک تولید کننده اعداد شبه تصادفی باشد، نتایج غیرقابل پیش بینی نسبتاً قوی را ارائه می دهد. RNGCryptoServiceProvider از چندین منبع آنتروپی استفاده می کند که اساساً "نویز" در رایانه شما هستند و پیش بینی توالی اعدادی که تولید می کند بسیار دشوار است. علاوه بر این، نویز "در کامپیوتر" نه تنها به عنوان حالت اولیه، بلکه بین تماس‌های شماره‌های تصادفی بعدی نیز قابل استفاده است. بنابراین، حتی دانستن وضعیت فعلیکلاس، این برای محاسبه اعداد بعدی که در آینده تولید می شوند و اعدادی که قبلا تولید شده اند کافی نخواهد بود. در واقع، رفتار دقیق به اجرا وابسته است. علاوه بر این، ویندوز می تواند از تخصصی استفاده کند سخت افزار، که منبعی از "تصادفی واقعی" است (به عنوان مثال، می تواند یک حسگر واپاشی ایزوتوپ رادیواکتیو باشد) تا اعداد تصادفی ایمن و قابل اعتمادتری تولید کند.

بیایید این را با کلاس Random که قبلا بحث شد مقایسه کنیم. فرض کنید ده بار با Random.Next(100) تماس گرفتید و نتایج را ذخیره کردید. اگر قدرت محاسباتی کافی دارید، می توانید تنها بر اساس این نتایج، حالت اولیه (seed) را که نمونه تصادفی با آن ایجاد شده است، محاسبه کنید، نتایج بعدی فراخوانی Random.Next(100) را پیش بینی کنید، و حتی نتایج را محاسبه کنید. فراخوانی های روش قبلی اگر از اعداد تصادفی برای اهداف امنیتی، مالی و غیره استفاده می کنید، این رفتار به شدت غیرقابل قبول است. Crypto RNG ها به طور قابل توجهی کندتر از کلاس Random عمل می کنند، اما دنباله ای از اعداد را تولید می کنند که هر کدام از مقادیر دیگر مستقل تر و غیرقابل پیش بینی تر هستند.

در بیشتر موارد بیشتر عملکرد پایینیک مانع نیست - یک API بد است. RandomNumberGenerator برای تولید توالی بایت طراحی شده است - این همه است. این را با متدهای کلاس Random مقایسه کنید، جایی که امکان به دست آوردن یک عدد صحیح تصادفی وجود دارد. عدد کسریو همچنین مجموعه ای از بایت ها. یکی دیگر از ویژگی های مفید، توانایی به دست آوردن یک عدد تصادفی در یک محدوده مشخص است. این احتمالات را با آرایه بایت های تصادفی که RandomNumberGenerator تولید می کند مقایسه کنید. می‌توانید با ایجاد wrapper (wrapper) خود در اطراف RandomNumberGenerator وضعیت را اصلاح کنید، که بایت‌های تصادفی را به یک نتیجه «مناسب» تبدیل می‌کند، اما این راه‌حل بی‌اهمیت است.

با این حال، در بیشتر موارد، اگر بتوانید مشکلی را که در ابتدای مقاله توضیح داده شد، حل کنید، "ضعف" کلاس Random خوب است. بیایید ببینیم در اینجا چه کاری می توانیم انجام دهیم.

از یک نمونه از کلاس Random برای چند تماس استفاده کنید
در اینجا، ریشه راه حل مشکل استفاده از تنها یک نمونه از Random هنگام ایجاد تعداد زیادی اعداد تصادفی با استفاده از Random.Next است. و این بسیار ساده است - ببینید چگونه می توانید کد بالا را تغییر دهید:
// این کد بهتر خواهد بود Random rng = new Random(); برای (int i = 0; i< 100; i++) { Console.WriteLine(GenerateDigit(rng)); } ... static int GenerateDigit(Random rng) { // Предположим, что здесь много логики return rng.Next(10); }
حالا هر تکرار اعداد متفاوتی خواهد داشت... اما این همه چیز نیست. اگر این بلوک کد را دو بار پشت سر هم فراخوانی کنیم چه اتفاقی می افتد؟ درست است، ما دو نمونه تصادفی با همان دانه ایجاد می کنیم و دو مجموعه یکسان از اعداد تصادفی به دست می آوریم. اعداد در هر مجموعه متفاوت خواهد بود، اما این مجموعه ها در بین خود برابر خواهند بود.

دو راه برای حل مشکل وجود دارد. ابتدا می‌توانیم از یک نمونه استفاده نکنیم، بلکه از یک فیلد ثابت حاوی نمونه‌ای از Random استفاده کنیم و سپس کد بالا تنها یک نمونه ایجاد می‌کند و از آن استفاده می‌کند و هر چند بار که لازم است آن را فراخوانی می‌کند. ثانیاً، می‌توانیم ایجاد یک نمونه تصادفی را از آنجا به طور کامل حذف کنیم، و آن را "بالاتر" منتقل کنیم، در حالت ایده‌آل به همان "بالای" برنامه، جایی که یک نمونه تصادفی واحد ایجاد می‌شود، پس از آن به همه مکان‌ها منتقل می‌شود. جایی که به اعداد تصادفی نیاز است. این ایده عالی، که به خوبی با وابستگی ها بیان می شود، اما تا زمانی که فقط از یک رشته استفاده کنیم کار خواهد کرد.

ایمنی نخ

کلاس Random ایمن نیست. با در نظر گرفتن اینکه چقدر دوست داریم یک نمونه واحد بسازیم و از آن در طول یک برنامه برای کل مدت اجرای آن استفاده کنیم (تک تک، سلام!)، عدم ایمنی نخ تبدیل به دردسر واقعی می شود. به هر حال، اگر از یک نمونه به طور همزمان در چندین رشته استفاده کنیم، احتمال بازنشانی حالت داخلی آن وجود دارد و اگر این اتفاق بیفتد، از آن لحظه به بعد نمونه بی‌فایده می‌شود.

باز هم دو راه برای حل مشکل وجود دارد. مسیر اول همچنان شامل استفاده از یک نمونه واحد است، اما این بار با استفاده از قفل کردن منابع از طریق مانیتور. برای انجام این کار، باید یک پوشش در اطراف Random ایجاد کنید که تماس‌های متدهای خود را در یک عبارت قفل قرار دهد و دسترسی انحصاری تماس‌گیرنده به نمونه را تضمین کند. این مسیر بد است زیرا عملکرد را در سناریوهای فشرده با نخ کاهش می دهد.

راه دیگری که در زیر توضیح خواهم داد، استفاده از یک نمونه در هر رشته است. تنها چیزی که باید مطمئن شویم این است که هنگام ایجاد نمونه از seed های مختلف استفاده می کنیم، بنابراین نمی توانیم از سازنده های پیش فرض استفاده کنیم. در غیر این صورت، این مسیر نسبتاً ساده است.

ارائه دهنده ایمن

خوشبختانه کلاس عمومی جدید ThreadLocal ، که در .NET 4 معرفی شده است، نوشتن ارائه دهندگانی را که یک نمونه در هر رشته ارائه می کنند بسیار آسان می کند. شما فقط باید یک نماینده به سازنده ThreadLocal ارسال کنید، که به دریافت مقدار خود نمونه ما اشاره دارد. در این مورد، من تصمیم گرفتم از یک مقدار seed استفاده کنم و آن را با استفاده از Environment.TickCount (که دقیقاً چگونه سازنده تصادفی بدون پارامتر کار می کند) مقداردهی اولیه کنم. در مرحله بعد، هر بار که نیاز داریم یک نمونه تصادفی جدید برای یک رشته جداگانه بدست آوریم، تعداد تیک های حاصل افزایش می یابد.

کلاس ارائه شده در زیر کاملا ثابت است و فقط شامل یک روش عمومی (باز) GetThreadRandom است. این متد به‌جای یک ویژگی، به‌عنوان یک متد ساخته می‌شود، عمدتاً برای راحتی: این تضمین می‌کند که همه کلاس‌هایی که به نمونه‌ای از Random نیاز دارند، به Func بستگی دارند. (نماینده ای که به متدی اشاره می کند که هیچ پارامتری را نمی گیرد و مقداری از نوع Random برمی گرداند)، و نه از خود کلاس Random. اگر یک نوع در نظر گرفته شده است که روی یک رشته واحد اجرا شود، می‌تواند یک نماینده را برای به دست آوردن یک نمونه تصادفی فراخوانی کند و سپس از آن در سراسر استفاده کند. اگر نوع نیاز به کار در سناریوهای چند رشته ای داشته باشد، می تواند هر بار که به یک مولد اعداد تصادفی نیاز دارد، نماینده را صدا کند. کلاس زیر به تعداد thread ها نمونه های کلاس Random ایجاد می کند و هر نمونه از مقدار اولیه متفاوتی شروع می شود. اگر نیاز به استفاده از ارائه‌دهنده اعداد تصادفی به عنوان یک وابستگی در انواع دیگر داشته باشیم، می‌توانیم این کار را انجام دهیم: TypeThatNeedsRandom(RandomProvider.GetThreadRandom) جدید. خب، این خود کد است:
با استفاده از سیستم؛ با استفاده از System.Threading. کلاس استاتیک عمومی RandomProvider ( خصوصی static int seed = Environment.TickCount؛ خصوصی استاتیک ThreadLocal randomWrapper = ThreadLocal جدید (() => new Random(Interlocked.Increment(ref seed)));
عمومی استاتیک تصادفی GetThreadRandom() ( randomWrapper.Value; ) )

به اندازه کافی ساده، درست است؟ این به این دلیل است که تمام کدها با هدف تولید نمونه صحیح Random هستند. هنگامی که یک نمونه ایجاد و برگردانده شد، مهم نیست که بعداً با آن چه کاری انجام می دهید: تمام موارد صدور نمونه های بعدی کاملاً مستقل از نمونه فعلی هستند. البته، کد کلاینت یک حفره برای سوء استفاده مخرب دارد: می‌تواند یک نمونه از Random را بگیرد و به جای فراخوانی RandomProvider ما در آن رشته‌های دیگر، آن را به رشته‌های دیگر ارسال کند.

یک مشکل هنوز باقی است: ما از یک مولد اعداد تصادفی محافظت شده ضعیف استفاده می کنیم. همانطور که قبلا ذکر شد، نسخه بسیار امن تری از RNG در RandomNumberGenerator وجود دارد که پیاده سازی آن در کلاس RNGCryptoServiceProvider است. با این حال، API آن برای استفاده در سناریوهای استاندارد بسیار دشوار است.

بسیار خوب است اگر ارائه دهندگان RNG در چارچوب «منابع تصادفی» جداگانه داشته باشند. در این مورد، ما می‌توانیم یک API واحد، ساده و راحت داشته باشیم که هم با اجرای ناامن اما سریع و هم با اجرای امن اما آهسته پشتیبانی می‌شود. خوب، خواب دیدن ضرری ندارد. شاید عملکرد مشابهی در نسخه های بعدی .NET Framework ظاهر شود. شاید کسی که از مایکروسافت نیست، اجرای خود را از آداپتور ارائه دهد. (متاسفانه، من آن شخص نخواهم بود... پیاده سازی درست چنین چیزی به طرز شگفت انگیزی پیچیده است.) همچنین می توانید کلاس خود را با استخراج از Random و نادیده گرفتن متدهای Sample و NextBytes ایجاد کنید، اما دقیقاً مشخص نیست که چگونه باید آنها را انجام دهند. کار، یا حتی نمونه پیاده سازی خودتان می تواند بسیار پیچیده تر از آن چیزی باشد که به نظر می رسد. شاید دفعه بعد...

ترجمه مقاله اعداد تصادفی توسط Jon Skeete که در محافل باریک شناخته شده است. من در این مقاله متوقف شدم زیرا در یک زمان من خودم با مشکل توضیح داده شده در آن مواجه شدم.

مرور موضوعات توسط دات نتو سی شارپدر وب‌سایت StackOverflow، می‌توانید سؤالات بی‌شماری با ذکر کلمه «تصادفی» مشاهده کنید، که در واقع همان سؤال ابدی و «تخریب‌ناپذیر» را ایجاد می‌کند: چرا مولد اعداد تصادفی System.Random «کار نمی‌کند» و چگونه «اصلاح کنیم». آن"" این مقاله به بررسی این مشکل و راه های حل آن اختصاص دارد.

بیان مشکل

در StackOverflow، در گروه‌های خبری و لیست‌های پستی، همه سؤالات در مورد موضوع "تصادفی" چیزی شبیه به این هستند:
من از Random.Next برای تولید اعداد تصادفی متعدد استفاده می‌کنم، اما این روش زمانی که چندین بار فراخوانی می‌شود همان عدد را برمی‌گرداند. هر بار که برنامه راه اندازی می شود، عدد تغییر می کند، اما در یک اجرای برنامه ثابت است.

یک کد مثال چیزی شبیه به این است:
// کد بد! استفاده نکنید! برای (int i = 0; i< 100; i++) { Console.WriteLine(GenerateDigit()); } ... static int GenerateDigit() { Random rng = new Random(); // Предположим, что здесь много логики return rng.Next(10); }
پس اینجا چه اشکالی دارد؟

توضیح

کلاس Random یک مولد اعداد تصادفی واقعی نیست، بلکه شامل یک مولد است شبهاعداد تصادفی هر نمونه از کلاس Random شامل یک حالت داخلی است و زمانی که متد Next (یا NextDouble یا NextBytes) فراخوانی می شود، متد از آن حالت برای برگرداندن عددی استفاده می کند که تصادفی ظاهر می شود. سپس حالت داخلی تغییر می‌کند تا دفعه بعد که Next فراخوانی می‌شود، یک عدد ظاهراً تصادفی متفاوت از آنچه قبلاً برگردانده شده است، برمی‌گرداند.

همه «داخلی‌های» کلاس Random کاملا قطعی. این بدان معناست که اگر چندین نمونه از کلاس Random را با همان حالت اولیه بگیرید که از طریق پارامتر سازنده مشخص شده است. دانهو برای هر نمونه متدهای خاصی را به همان ترتیب و با پارامترهای یکسان فراخوانی کنید، سپس در پایان نتایج یکسانی خواهید داشت.

خب کد بالا چه اشکالی دارد؟ نکته بد این است که ما از یک نمونه جدید از کلاس Random در داخل هر تکرار حلقه استفاده می کنیم. سازنده تصادفی، که هیچ پارامتری را نمی گیرد، تاریخ و زمان فعلی را به عنوان بذر خود می گیرد. تکرارها در حلقه به سرعت "پیمایش" می شوند که زمان سیستم "زمانی برای تغییر نخواهد داشت" پس از تکمیل آنها. بنابراین، همه نمونه‌های Random همان مقدار حالت اولیه خود را دریافت می‌کنند و بنابراین همان عدد شبه تصادفی را برمی‌گردانند.

چگونه این را رفع کنیم؟

راه حل های زیادی برای حل این مشکل وجود دارد که هر کدام مزایا و معایب خاص خود را دارند. ما به چند مورد از آنها نگاه خواهیم کرد.
استفاده از مولد اعداد تصادفی رمزنگاری
دات نت حاوی یک کلاس انتزاعی RandomNumberGenerator است که تمامی پیاده سازی های مولد اعداد تصادفی رمزنگاری (که از این پس به عنوان cryptoRNG نامیده می شود) باید از آن به ارث برسند. .NET همچنین شامل یکی از این پیاده سازی ها است - کلاس RNGCryptoServiceProvider را ملاقات کنید. ایده رمزارز RNG این است که حتی اگر هنوز یک تولید کننده اعداد شبه تصادفی باشد، نتایج غیرقابل پیش بینی نسبتاً قوی را ارائه می دهد. RNGCryptoServiceProvider از چندین منبع آنتروپی استفاده می کند که اساساً "نویز" در رایانه شما هستند و پیش بینی توالی اعدادی که تولید می کند بسیار دشوار است. علاوه بر این، نویز "در کامپیوتر" نه تنها به عنوان حالت اولیه، بلکه بین تماس‌های شماره‌های تصادفی بعدی نیز قابل استفاده است. بنابراین، حتی با دانستن وضعیت فعلی کلاس، محاسبه اعداد بعدی که در آینده تولید خواهند شد و اعدادی که قبلا تولید شده اند کافی نخواهد بود. در واقع، رفتار دقیق به اجرا وابسته است. علاوه بر این، ویندوز می‌تواند از سخت‌افزار تخصصی استفاده کند که منبع «تصادفی واقعی» است (به عنوان مثال، یک حسگر فروپاشی ایزوتوپ رادیواکتیو) تا حتی اعداد تصادفی مطمئن‌تر و مطمئن‌تری تولید کند.

بیایید این را با کلاس Random که قبلا بحث شد مقایسه کنیم. فرض کنید ده بار با Random.Next(100) تماس گرفتید و نتایج را ذخیره کردید. اگر قدرت محاسباتی کافی دارید، می توانید تنها بر اساس این نتایج، حالت اولیه (seed) را که نمونه تصادفی با آن ایجاد شده است، محاسبه کنید، نتایج بعدی فراخوانی Random.Next(100) را پیش بینی کنید، و حتی نتایج را محاسبه کنید. فراخوانی های روش قبلی اگر از اعداد تصادفی برای اهداف امنیتی، مالی و غیره استفاده می کنید، این رفتار به شدت غیرقابل قبول است. Crypto RNG ها به طور قابل توجهی کندتر از کلاس Random عمل می کنند، اما دنباله ای از اعداد را تولید می کنند که هر کدام از مقادیر دیگر مستقل تر و غیرقابل پیش بینی تر هستند.

در بیشتر موارد، عملکرد ضعیف مانع از معامله نیست - یک API بد است. RandomNumberGenerator برای تولید توالی بایت طراحی شده است - این همه است. این را با متدهای کلاس Random مقایسه کنید، جایی که می توان یک عدد صحیح تصادفی، عدد کسری و همچنین مجموعه ای از بایت ها را بدست آورد. یکی دیگر از ویژگی های مفید، توانایی به دست آوردن یک عدد تصادفی در یک محدوده مشخص است. این احتمالات را با آرایه بایت های تصادفی که RandomNumberGenerator تولید می کند مقایسه کنید. می‌توانید با ایجاد wrapper (wrapper) خود در اطراف RandomNumberGenerator وضعیت را اصلاح کنید، که بایت‌های تصادفی را به یک نتیجه «مناسب» تبدیل می‌کند، اما این راه‌حل بی‌اهمیت است.

با این حال، در بیشتر موارد، اگر بتوانید مشکلی را که در ابتدای مقاله توضیح داده شد، حل کنید، "ضعف" کلاس Random خوب است. بیایید ببینیم در اینجا چه کاری می توانیم انجام دهیم.

از یک نمونه از کلاس Random برای چند تماس استفاده کنید
در اینجا، ریشه راه حل مشکل استفاده از تنها یک نمونه از Random هنگام ایجاد تعداد زیادی اعداد تصادفی با استفاده از Random.Next است. و این بسیار ساده است - ببینید چگونه می توانید کد بالا را تغییر دهید:
// این کد بهتر خواهد بود Random rng = new Random(); برای (int i = 0; i< 100; i++) { Console.WriteLine(GenerateDigit(rng)); } ... static int GenerateDigit(Random rng) { // Предположим, что здесь много логики return rng.Next(10); }
حالا هر تکرار اعداد متفاوتی خواهد داشت... اما این همه چیز نیست. اگر این بلوک کد را دو بار پشت سر هم فراخوانی کنیم چه اتفاقی می افتد؟ درست است، ما دو نمونه تصادفی با همان دانه ایجاد می کنیم و دو مجموعه یکسان از اعداد تصادفی به دست می آوریم. اعداد در هر مجموعه متفاوت خواهد بود، اما این مجموعه ها در بین خود برابر خواهند بود.

دو راه برای حل مشکل وجود دارد. ابتدا می‌توانیم از یک نمونه استفاده نکنیم، بلکه از یک فیلد ثابت حاوی نمونه‌ای از Random استفاده کنیم و سپس کد بالا تنها یک نمونه ایجاد می‌کند و از آن استفاده می‌کند و هر چند بار که لازم است آن را فراخوانی می‌کند. ثانیاً، می‌توانیم ایجاد یک نمونه تصادفی را از آنجا به طور کامل حذف کنیم، و آن را "بالاتر" منتقل کنیم، در حالت ایده‌آل به همان "بالای" برنامه، جایی که یک نمونه تصادفی واحد ایجاد می‌شود، پس از آن به همه مکان‌ها منتقل می‌شود. جایی که به اعداد تصادفی نیاز است. این یک ایده عالی است که به خوبی توسط وابستگی ها بیان می شود، اما تا زمانی که فقط از یک رشته استفاده کنیم کار خواهد کرد.

ایمنی نخ

کلاس Random ایمن نیست. با در نظر گرفتن اینکه چقدر دوست داریم یک نمونه واحد بسازیم و از آن در طول یک برنامه برای کل مدت اجرای آن استفاده کنیم (تک تک، سلام!)، عدم ایمنی نخ تبدیل به دردسر واقعی می شود. به هر حال، اگر از یک نمونه به طور همزمان در چندین رشته استفاده کنیم، احتمال بازنشانی حالت داخلی آن وجود دارد و اگر این اتفاق بیفتد، از آن لحظه به بعد نمونه بی‌فایده می‌شود.

باز هم دو راه برای حل مشکل وجود دارد. مسیر اول همچنان شامل استفاده از یک نمونه واحد است، اما این بار با استفاده از قفل کردن منابع از طریق مانیتور. برای انجام این کار، باید یک پوشش در اطراف Random ایجاد کنید که تماس‌های متدهای خود را در یک عبارت قفل قرار دهد و دسترسی انحصاری تماس‌گیرنده به نمونه را تضمین کند. این مسیر بد است زیرا عملکرد را در سناریوهای فشرده با نخ کاهش می دهد.

راه دیگری که در زیر توضیح خواهم داد، استفاده از یک نمونه در هر رشته است. تنها چیزی که باید مطمئن شویم این است که هنگام ایجاد نمونه از seed های مختلف استفاده می کنیم، بنابراین نمی توانیم از سازنده های پیش فرض استفاده کنیم. در غیر این صورت، این مسیر نسبتاً ساده است.

ارائه دهنده ایمن

خوشبختانه کلاس عمومی جدید ThreadLocal ، که در .NET 4 معرفی شده است، نوشتن ارائه دهندگانی را که یک نمونه در هر رشته ارائه می کنند بسیار آسان می کند. شما فقط باید یک نماینده به سازنده ThreadLocal ارسال کنید، که به دریافت مقدار خود نمونه ما اشاره دارد. در این مورد، من تصمیم گرفتم از یک مقدار seed استفاده کنم و آن را با استفاده از Environment.TickCount (که دقیقاً چگونه سازنده تصادفی بدون پارامتر کار می کند) مقداردهی اولیه کنم. در مرحله بعد، هر بار که نیاز داریم یک نمونه تصادفی جدید برای یک رشته جداگانه بدست آوریم، تعداد تیک های حاصل افزایش می یابد.

کلاس ارائه شده در زیر کاملا ثابت است و فقط شامل یک روش عمومی (باز) GetThreadRandom است. این متد به‌جای یک ویژگی، به‌عنوان یک متد ساخته می‌شود، عمدتاً برای راحتی: این تضمین می‌کند که همه کلاس‌هایی که به نمونه‌ای از Random نیاز دارند، به Func بستگی دارند. (نماینده ای که به متدی اشاره می کند که هیچ پارامتری را نمی گیرد و مقداری از نوع Random برمی گرداند)، و نه از خود کلاس Random. اگر یک نوع در نظر گرفته شده است که روی یک رشته واحد اجرا شود، می‌تواند یک نماینده را برای به دست آوردن یک نمونه تصادفی فراخوانی کند و سپس از آن در سراسر استفاده کند. اگر نوع نیاز به کار در سناریوهای چند رشته ای داشته باشد، می تواند هر بار که به یک مولد اعداد تصادفی نیاز دارد، نماینده را صدا کند. کلاس زیر به تعداد thread ها نمونه های کلاس Random ایجاد می کند و هر نمونه از مقدار اولیه متفاوتی شروع می شود. اگر نیاز به استفاده از ارائه‌دهنده اعداد تصادفی به عنوان یک وابستگی در انواع دیگر داشته باشیم، می‌توانیم این کار را انجام دهیم: TypeThatNeedsRandom(RandomProvider.GetThreadRandom) جدید. خب، این خود کد است:
با استفاده از سیستم؛ با استفاده از System.Threading. کلاس استاتیک عمومی RandomProvider ( خصوصی static int seed = Environment.TickCount؛ خصوصی استاتیک ThreadLocal randomWrapper = ThreadLocal جدید (() => new Random(Interlocked.Increment(ref seed)));
عمومی استاتیک تصادفی GetThreadRandom() ( randomWrapper.Value; ) )

به اندازه کافی ساده، درست است؟ این به این دلیل است که تمام کدها با هدف تولید نمونه صحیح Random هستند. هنگامی که یک نمونه ایجاد و برگردانده شد، مهم نیست که بعداً با آن چه کاری انجام می دهید: تمام موارد صدور نمونه های بعدی کاملاً مستقل از نمونه فعلی هستند. البته، کد کلاینت یک حفره برای سوء استفاده مخرب دارد: می‌تواند یک نمونه از Random را بگیرد و به جای فراخوانی RandomProvider ما در آن رشته‌های دیگر، آن را به رشته‌های دیگر ارسال کند.

یک مشکل هنوز باقی است: ما از یک مولد اعداد تصادفی محافظت شده ضعیف استفاده می کنیم. همانطور که قبلا ذکر شد، نسخه بسیار امن تری از RNG در RandomNumberGenerator وجود دارد که پیاده سازی آن در کلاس RNGCryptoServiceProvider است. با این حال، API آن برای استفاده در سناریوهای استاندارد بسیار دشوار است.

بسیار خوب است اگر ارائه دهندگان RNG در چارچوب «منابع تصادفی» جداگانه داشته باشند. در این مورد، ما می‌توانیم یک API واحد، ساده و راحت داشته باشیم که هم با اجرای ناامن اما سریع و هم با اجرای امن اما آهسته پشتیبانی می‌شود. خوب، خواب دیدن ضرری ندارد. شاید عملکرد مشابهی در نسخه های بعدی .NET Framework ظاهر شود. شاید کسی که از مایکروسافت نیست، اجرای خود را از آداپتور ارائه دهد. (متاسفانه، من آن شخص نخواهم بود... پیاده سازی درست چنین چیزی به طرز شگفت انگیزی پیچیده است.) همچنین می توانید کلاس خود را با استخراج از Random و نادیده گرفتن متدهای Sample و NextBytes ایجاد کنید، اما دقیقاً مشخص نیست که چگونه باید آنها را انجام دهند. کار، یا حتی نمونه پیاده سازی خودتان می تواند بسیار پیچیده تر از آن چیزی باشد که به نظر می رسد. شاید دفعه بعد...

اغلب در برنامه ها نیاز به استفاده از اعداد تصادفی وجود دارد - از پر کردن یک آرایه تا رمزنگاری. برای بدست آوردن دنباله ای از اعداد تصادفی، زبان سی شارپ دارای کلاس Random است. این کلاس دو سازنده ارائه می دهد:

  • تصادفی()- نمونه ای از کلاس Random را با مقدار اولیه که به زمان فعلی بستگی دارد مقداردهی اولیه می کند. همانطور که مشخص است، زمان را می توان در نمایش داد کنه ها- 100 پالس نانوثانیه از 1 ژانویه 0001 شروع می شود. و مقدار زمان در تیک ها یک عدد صحیح 64 بیتی است که برای مقداردهی اولیه نمونه مولد اعداد تصادفی استفاده می شود.
  • تصادفی (Int32)- نمونه ای از کلاس Random را با استفاده از مقدار اولیه مشخص شده مقداردهی اولیه می کند. راه‌اندازی یک مولد اعداد تصادفی به این روش می‌تواند هنگام اشکال‌زدایی یک برنامه راحت باشد، زیرا در این مورد هر بار که برنامه راه‌اندازی می‌شود همان اعداد "تصادفی" تولید می‌شوند.
متد اصلی این کلاس متد Next() است که به شما امکان می دهد یک عدد تصادفی بدست آورید و تعدادی اضافه بار دارد:
  • Next() - یک عدد صحیح غیر منفی تصادفی در قالب Int32 برمی گرداند.
  • بعدی( Int32)- یک عدد صحیح غیر منفی تصادفی که کمتر از مقدار مشخص شده است را برمی گرداند.
  • بعدی( Int32 دقیقه، Int32 حداکثر)- یک عدد صحیح تصادفی در محدوده مشخص شده برمی گرداند. در این صورت شرط min باید رعایت شود
و همچنین روش ها
  • NextBytes( بایت)- عناصر آرایه بایت مشخص شده را با اعداد تصادفی پر می کند.
  • NextDouble() - یک عدد ممیز شناور تصادفی را در محدوده برمی گرداند.
    شکستن ; // مطابق پیدا شد، عنصر مطابقت ندارد
    }
    اگر (j == i)
    { // مطابقت پیدا نشد
    a[i] = تعداد; // عنصر را ذخیره کنید
    i++; // به عنصر بعدی بروید
    }
    }
    برای (int i = 0; i< 100; i++)
    {

    اگر (i % 10 == 9)
    Console.WriteLine();
    }
    کنسول .ReadKey();
    }
    }
    }

    با این حال، هر چه به انتهای آرایه نزدیک‌تر باشد، باید نسل‌های بیشتری برای به دست آوردن یک مقدار غیر تکراری انجام شود.
    مثال زیر تعداد فراخوانی‌های متد Next() برای به دست آوردن هر عنصر و همچنین تعداد کل اعداد تصادفی تولید شده برای پر کردن یک آرایه 100 عنصری با مقادیر غیر تکراری را نشان می‌دهد.

    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

    با استفاده از سیستم؛
    فضای نام MyProgram
    {
    برنامه کلاس
    {
    فضای خالی استاتیک اصلی (آرگس های رشته ای)
    {
    تصادفی rnd = new Random();
    int a = جدید int ; // آرایه ای از عناصر
    int count = int جدید ; // آرایه از تعداد نسل
    a = rnd.Next(0, 101);
    int c = 0; // شمارنده تعداد نسل ها
    تعداد = 1; // a فقط یک بار تولید می شود
    برای (int i = 1; i< 100;)
    {
    int num = rnd.Next(0, 101);
    c++; // یک بار دیگر عنصر را تولید کرد
    int j;
    برای (j = 0؛ j< i; j++)
    {
    اگر (تعداد == a[j])
    شکستن ;
    }
    اگر (j == i)
    {
    a[i] = تعداد; i++;
    count[i] = c; c = 0; // تعداد نسل ها را ذخیره کنید
    }
    }
    // چاپ مقادیر عناصر
    کنسول .WriteLine( "ارزش عناصر");
    برای (int i = 0; i< 100; i++)
    {
    Console .Write("(0,4) " , a[i]);
    اگر (i % 10 == 9)
    Console.WriteLine();
    }
    Console.WriteLine();
    // نمایش تعداد نسل ها
    کنسول .WriteLine( "تعداد تولید عنصر");
    int sum = 0;
    برای (int i = 0; i< 100; i++)
    {
    sum += count[i];
    Console .Write("(0,4) " , count[i]);
    اگر (i % 10 == 9)
    Console.WriteLine();
    }
    کنسول .WriteLine( "تعداد کل نسل - (0)"، مجموع)؛
    کنسول .ReadKey();
    }
    }
    }

    void Main (ارگ های رشته ای)
    {
    تصادفی rnd = new Random();
    int a = int جدید ;
    برای (int i = 0; i< 100; i++)
    a[i] = i;
    برای (int i = 0; i< 50; i++)
    {
    int i1 = rnd.Next(0, 100); // اولین شاخص
    int i2 = rnd.Next(0, 100); // شاخص دوم
    // تبادل مقادیر عناصر با شاخص های i1 و i2
    int temp = a;
    a = a;
    a = دما
    }
    کنسول .WriteLine( "ارزش عناصر");
    برای (int i = 0; i< 100; i++)
    {
    Console .Write("(0,4) " , a[i]);
    اگر (i % 10 == 9)
    Console.WriteLine();
    }
    کنسول .ReadKey();
    }
    }
    }

    اگر دامنه مقادیر با تعداد مقادیر مطابقت داشته باشد (یا نزدیک به) باشد، مخلوط کردن مقادیر مؤثرتر است، زیرا در این حالت تعداد تولید عناصر تصادفی به میزان قابل توجهی کاهش می یابد.

    تابعی که اعداد شبه تصادفی را تولید می کند یک نمونه اولیه در فایل کتابخانه stdlib.h دارد:

    1
    2
    3
    4
    5
    6

    بدون علامت long int next = 1;
    اینت رند (باطل)
    {
    بعدی = بعدی * 1103515245;
    return ((unsigned int )(next / 65536) * 2768);
    }


    تابع rand () هیچ آرگومان نمی گیرد، اما روی متغیر بعدی با دامنه جهانی عمل می کند.

    اگر نیاز به ایجاد یک دنباله در محدوده دارید ، سپس از فرمول استفاده می شود:

    عدد = رند()%(M2-M1+1) + M1;

    کجا شماره- عدد تولید شده M2-M1+1- طیف کامل نمایش اعداد. M1- افست محدوده مشخص شده نسبت به 0؛ % - باقی مانده تقسیم.

    به عنوان مثال، اگر شما نیاز به ایجاد یک دنباله در محدوده [-10;10] داشته باشید، فراخوانی تابع شبیه به این خواهد بود.

    عدد = rand()%(10+10+1)-10

    عدد = rand()%(21)-10

    در نتیجه به دست آوردن باقی مانده از تقسیم بر 21، عددی از 0 تا 20 داریم. با کم کردن 10 از عدد حاصل، عددی در محدوده مورد نظر [-10;10] به دست می آید.

    با این حال، توالی ایجاد شده توسط تابع rand () هر بار که برنامه اجرا می شود یکسان به نظر می رسد.

    برای تولید توالی های مختلف هر بار که برنامه راه اندازی می شود، لازم است متغیر سراسری بعدی با مقداری غیر از 1 مقداردهی اولیه شود. برای این منظور از تابع استفاده کنید.
    void srand (بذر int بدون علامت)
    (بعدی = دانه؛ )

    برای اطمینان از اینکه مقداردهی اولیه next هر بار که برنامه راه اندازی می شود متفاوت است، زمان جاری اغلب به عنوان آرگومان seed استفاده می شود.

    مثال آرایه ای از 20 عنصر را با اعداد تصادفی در محدوده 0 تا 99 پر کنید.

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

    #شامل
    #شامل
    #شامل
    #SIZE 20 را تعریف کنید
    int main() (
    int a;
    srand(time(NULL));
    برای (int i = 0; i {
    a[i] = rand() % 100;
    printf("%d " , a[i]);
    }
    getchar();
    بازگشت 0;
    }


    نتیجه اجرا

    اغلب وظیفه مرتب کردن مجموعه ای از مقادیر موجود به ترتیب تصادفی است. برای این منظور از مولد اعداد شبه تصادفی نیز استفاده می شود. این یک آرایه ایجاد می کند و آن را با مقادیر پر می کند.
    روش اختلاط خود به شرح زیر است. دو مقدار شاخص آرایه به طور تصادفی تولید می شود و مقادیر عناصر با شاخص های حاصل تعویض می شوند. این روش حداقل N بار تکرار می شود که N تعداد عناصر آرایه است.
    به عنوان مثال، 20 مقدار (از 1 تا 20) را به هم بزنید و این روش را 20 بار تکرار کنید.

    پیاده سازی در 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

    #شامل
    #شامل
    #شامل
    #SIZE 20 را تعریف کنید
    int main() (
    int a;
    srand(time(NULL));

    برای (int i = 0; i< SIZE; i++)
    {
    a[i] = i + 1;
    printf("%2d " , a[i]);
    }
    برای (int i = 0; i< SIZE; i++)
    {
    // به طور تصادفی دو شاخص از عناصر تولید کنید
    int ind1 = rand() % 20;
    int ind2 = rand() % 20;
    // و عناصر را با این شاخص ها عوض کنید
    int temp = a;
    a = a;
    a = دما
    }
    printf("\n");

    برای (int i = 0; i< SIZE; i++)
    printf("%2d " , a[i]);
    getchar();
    بازگشت 0;
    }


    نتیجه اجرا


    اغلب وظیفه انتخاب تصادفی عناصر آرایه مشخص شده قبلی مطرح می شود. علاوه بر این، لازم است از عدم تکرار در انتخاب این عناصر اطمینان حاصل شود.
    الگوریتم این انتخاب به شرح زیر است:

    • به طور تصادفی شاخص یک عنصر آرایه را انتخاب می کنیم
    • اگر عنصری با همان شاخص قبلاً انتخاب شده است، به سمت راست حرکت کنید تا به عنصر انتخاب نشده بعدی برسیم. در همان زمان، ما مطمئن می شویم که "حرکت به سمت راست" از مرزهای آرایه فراتر نمی رود. اگر خارج از محدوده آرایه شناسایی شد، از ابتدا شروع به مشاهده عناصر آرایه می کنیم.
    • انتخاب یک عنصر
    • ثابت کردن عنصر به عنوان انتخاب شده
    • این مراحل را برای تمام عناصر دیگر تکرار کنید

    پیاده سازی در C
    در نتیجه، یک آرایه b جدید بدست می آوریم که با انتخاب تصادفی عناصر آرایه a تشکیل شده است.

    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

    #شامل
    #شامل
    #شامل
    #SIZE 20 را تعریف کنید
    int main() (
    int a;
    int b; // آرایه حاصل
    srand(time(NULL));
    // آرایه را با مقادیر متوالی از 1 تا 20 پر کنید
    برای (int i = 0; i< SIZE; i++)
    {
    a[i] = i + 1;
    printf("%2d " , a[i]);
    }

    برای (int i = 0; i< SIZE; i++)
    {
    int ind = rand() % 20; // یک شاخص دلخواه را انتخاب کنید
    در حالی که (a == -1) // در حالی که عنصر "انتخاب" است
    {
    ind++; // به سمت راست حرکت کنید
    ind % = 20; // اگر به مرز سمت راست رسیدیم به ابتدا برگردیم
    }
    b[i] = a; // عنصر بعدی آرایه b را بنویسید
    a = -1; // عنصر آرایه a را به عنوان "انتخاب" علامت گذاری کنید
    }
    printf("\n");
    // خروجی آرایه به دست آمده
    برای (int i = 0; i< SIZE; i++)
    printf("%2d " , b[i]);
    getchar();
    بازگشت 0;
    }


    نتیجه اجرا

    درود بر همه کسانی که سر زدند. این یادداشت کوتاه حاوی چند کلمه در مورد تولید اعداد شبه تصادفی در C/C++ است. به طور خاص، در مورد نحوه کار با ساده ترین تابع تولید - rand().

    تابع rand().

    در کتابخانه استاندارد C++ (stdlib.h) یافت شد. یک عدد شبه تصادفی در محدوده 0 تا RAND_MAX ایجاد و برمی گرداند. این ثابت ممکن است بسته به معماری کامپایلر یا پردازنده متفاوت باشد، اما به طور کلی حداکثر مقدار نوع داده int بدون علامت است. پارامترها را نمی پذیرد و هرگز ندارد.

    برای اینکه تولید انجام شود، باید seed را با استفاده از یک تابع کمکی از همان کتابخانه - srand() تنظیم کنید. یک عدد را می گیرد و آن را به عنوان نقطه شروع برای تولید یک عدد تصادفی قرار می دهد. اگر seed تنظیم نشده باشد، هر بار که برنامه شروع شود، دریافت خواهیم کرد یکسان اعداد تصادفی واضح ترین راه حل این است که زمان فعلی سیستم را به آنجا ارسال کنید. این را می توان با استفاده از تابع time (NULL) انجام داد. این تابع در کتابخانه time.h است.

    ما تئوری را مرتب کردیم، همه توابع تولید اعداد تصادفی را پیدا کردیم، حالا بیایید آنها را تولید کنیم.

    مثالی از استفاده از تابع تولید اعداد تصادفی rand().

    #شامل #شامل #شامل با استفاده از namespace std. int main() (srand(time(NULL)); for(int i = 0; i< 10; i++) { cout << rand() << endl; } return 0; }

    رند 10 عدد تصادفی برای ما تولید کرد. می توانید با اجرای دوباره و دوباره برنامه، تصادفی بودن آنها را تأیید کنید. اما این کافی نیست، بنابراین باید در مورد محدوده صحبت کنیم.

    تعیین مرزهای محدوده برای rand()

    برای تولید یک عدد در محدوده A تا B شامل، باید این را بنویسید:

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

    مقادیر مرزی همچنین می توانند منفی باشند، که به شما امکان می دهد یک عدد تصادفی منفی ایجاد کنید، بیایید به یک مثال نگاه کنیم.

    #شامل #شامل #شامل با استفاده از namespace std. int main() (srand(time(NULL))؛ int A = -2؛ int B = 8؛ for(int i = 0؛ i< 100; i++) { cout << A + rand() % ((B + 1) - A) << endl; } return 0; }