Skip to content

10. رشته‌های اجرایی

10.1. کلاس Thread

وقتی یک برنامه اجرا می‌شود، در یک جریان اجرایی به نام نخ (thread) اجرا می‌شود. کلاس .NET که یک thread را مدل می‌کند، کلاس System.Threading.Thread است و تعریف زیر را دارد:

سازنده‌ها

در مثال‌های زیر، ما فقط از سازندهای [1,3] استفاده خواهیم کرد. سازنده [1] یک متد با امضای [2] را به‌عنوان پارامتر می‌پذیرد، c.a.d که دارای یک پارامتر از نوع object است و هیچ نتیجه‌ای را بازنمی‌گرداند. سازنده [3] به عنوان پارامتر، متدی با امضای [4], c.a.d را می‌پذیرد که هیچ پارامتری ندارد و هیچ نتیجه‌ای بر نمی‌گرداند.

ویژگی‌ها

برخی از ویژگی‌های مفید:

  • رشته CurrentThread: یک خاصیت ایستا که مرجعی به رشته‌ای که کد درخواست‌کننده این خاصیت در آن قرار دارد، بازمی‌گرداند
  • string Name: نام تِرد
  • bool IsAlive: نشان می‌دهد که آیا تِرد در حال حاضر در حال اجرا است یا خیر.

متدها

رایج‌ترین متدها به شرح زیر هستند:

  • Start()، Start(object obj): اجرای ناهمزمان نخ را آغاز می‌کند، اختیاریاً اطلاعاتی را در نوع object به آن می‌گذراند.
  • Abort() و Abort(object obj): برای خاتمه اجباری یک نخ
  • Join(): نخ T1 که در حال اجرای T2.Join است، تا زمانی که نخ T2 به پایان نرسیده باشد، مسدود می‌ماند. برای پایان دادن به انتظار پس از یک زمان مشخص، گونه‌های مختلفی وجود دارد.
  • Sleep(int n): متد استاتیک – ثریدی که این متد را اجرا می‌کند برای n میلی‌ثانیه معلق می‌شود. سپس CPU را آزاد می‌کند که به ثریدی دیگر اختصاص داده می‌شود.

بیایید نگاهی به یک برنامه ساده بیندازیم که وجود یک نخ اجرای اصلی را نشان می‌دهد، نخی که در آن متد Main یک کلاس اجرا می‌شود:


using System;
using System.Threading;

namespace Chap8 {
    class Program {
        static void Main(string[] args) {
            // ابتکاری‌سازی نخ جاری
            Thread main = Thread.CurrentThread;
            //نمایش
            Console.WriteLine("Thread courant : {0}", main.Name);
            // تغییر نام
            main.Name = "main";
            //بررسی
            Console.WriteLine("Thread courant : {0}", main.Name);

            // حلقه بی‌نهایت
            while (true) {
                // نمایش
                Console.WriteLine("{0} : {1:hh:mm:ss}", main.Name, DateTime.Now);
                // ایست موقت
                Thread.Sleep(1000);
            }//در حالی که        
        }
    }
}
  • خط ۸: یک مرجع به ثریدی که متد [main] در آن در حال اجرا است، بازیابی می‌شود
  • خطوط ۱۰–۱۴: نام آن نمایش داده شده و تغییر می‌کند
  • خطوط ۱۷–۲۲: یک حلقه که هر ثانیه نام را نمایش می‌دهد
  • خط ۲۱: ثریدی که متد [main] در آن در حال اجرا است به مدت ۱ ثانیه متوقف می‌شود

خروجی صفحه به شرح زیر است:

1
2
3
4
5
6
7
8
Thread courant :
Thread courant : main
main : 04:19:00
main : 04:19:01
main : 04:19:02
main : 04:19:03
main : 04:19:04
^CAppuyez sur une touche pour continuer...
  • خط ۱: نخ فعلی نامی نداشت
  • خط ۲: اکنون یک نام دارد
  • خطوط ۳–۷: خروجی نمایش داده شده هر ثانیه
  • خط ۸: برنامه با کلید Ctrl-C خاتمه می‌یابد.

10.2. ایجاد نخ‌های اجرایی

ممکن است برنامه‌هایی وجود داشته باشند که در آن‌ها بخش‌هایی از کد به‌طور «هم‌زمان» در نخ‌های اجرایی مختلف اجرا شوند. وقتی می‌گوییم threadها به‌طور هم‌زمان اجرا می‌شوند، اغلب این یک اصطلاح نادرست است. اگر ماشین تنها یک پردازنده داشته باشد – که هنوز هم اغلب چنین است – threadها این پردازنده را به اشتراک می‌گذارند: هر یک به نوبت برای مدت کوتاهی (چند میلی‌ثانیه) به آن دسترسی پیدا می‌کنند. این همان چیزی است که توهم اجرای موازی را ایجاد می‌کند. میزان زمانی که به یک thread اختصاص داده می‌شود به عوامل مختلفی بستگی دارد، از جمله اولویت آن، که یک مقدار پیش‌فرض دارد اما می‌تواند به صورت برنامه‌ای نیز تنظیم شود. هنگامی که یک thread پردازنده را در اختیار دارد، معمولاً از آن برای تمام مدت زمان اختصاص‌یافته به خود استفاده می‌کند. با این حال، ممکن است آن را زودتر آزاد کند:

  • با انتظار برای یک رویداد (Wait, Join)
  • با ورود به حالت خواب برای مدت زمان مشخصی (Sleep)
  1. یک تِرد T ابتدا توسط یکی از سازندهای ارائه‌شده در بالا ایجاد می‌شود، برای مثال:
Thread thread=new Thread(Start);

که در آن Start یک متد با یکی از دو امضای زیر است:

void Start();
void Start(object obj);

ایجاد یک نخ، آن را اجرا نمی‌کند.

  1. اجرای نخ T توسط T.Start() آغاز می‌شود: متد Start که به کانستراکتور T پاس داده می‌شود، سپس توسط نخ T اجرا خواهد شد. برنامه‌ای که دستور T.Start() را اجرا می‌کند، منتظر به پایان رسیدن وظیفه T نمی‌ماند: این برنامه بلافاصله به دستور بعدی می‌رود. بنابراین ما دو وظیفه داریم که به صورت موازی در حال اجرا هستند. آنها اغلب باید بتوانند برای پیگیری پیشرفت کار مشترک، با یکدیگر ارتباط برقرار کنند. این مسئله، مشکل همگام‌سازی نخ‌ها است.
  2. پس از راه‌اندازی، نخ T به‌طور مستقل اجرا می‌شود. این نخ زمانی متوقف می‌شود که متد Start که در حال اجراست، کار خود را به پایان برساند.
  3. ما می‌توانیم نخ T را مجبور به خاتمه کنیم:
    1. تابع T.Abort() به نخ T دستور پایان کار می‌دهد.
  4. همچنین می‌توانیم با استفاده از T.Join() منتظر پایان اجرای آن بمانیم. این یک دستور مسدودکننده است: برنامه‌ای که آن را اجرا می‌کند تا زمانی که وظیفه T کار خود را به پایان نرساند، مسدود می‌شود. این یک روش همگام‌سازی است.

بیایید برنامه زیر را بررسی کنیم:


using System;
using System.Threading;

namespace Chap8 {
    class Program {
        public static void Main() {
            // ابتدایی‌سازی نخ جاری
            Thread main = Thread.CurrentThread;
            // اعطای نام به نخ
            main.Name = "Main";

            // ایجاد نخ‌های اجرایی
            Thread[] tâches = new Thread[5];
            for (int i = 0; i < tâches.Length; i++) {
                // ایجاد نخ i
                tâches[i] = new Thread(Affiche);
                // تنظیم نام نخ
                tâches[i].Name =  i.ToString();
                // راه‌اندازی نخ i
                tâches[i].Start();
            }

            //پایان main
            Console.WriteLine("Fin du thread {0} à {1:hh:mm:ss}",main.Name,DateTime.Now);
        }

        public static void Affiche() {
            // نمایش شروع اجرا
            Console.WriteLine("Début d'exécution de la méthode Affiche dans le Thread {0} : {1:hh:mm:ss}",Thread.CurrentThread.Name,DateTime.Now);
            // خواب برای ۱ ثانیه
            Thread.Sleep(1000);
            //پایان اجرای نمایش داده شد
            Console.WriteLine("Fin d'exécution de la méthode Affiche dans le Thread {0} : {1:hh:mm:ss}", Thread.CurrentThread.Name, DateTime.Now);
        }
    }
}
  • خطوط ۸–۱۰: ما به رشته‌ای که متد [Main] را اجرا می‌کند، یک نام می‌دهیم
  • خطوط ۱۳–۲۱: پنج رشته ایجاد و اجرا می‌شوند. ارجاعات رشته‌ها در یک آرایه ذخیره می‌شوند تا بعداً قابل بازیابی باشند. هر رشته متد Affiche را در خطوط ۲۷–۳۵ اجرا می‌کند.
  • خط ۲۰: نخ با شماره i راه‌اندازی می‌شود. این عملیات غیرمسدودکننده است. نخ با شماره i به صورت موازی با نخ متد [Main] که آن را راه‌اندازی کرده است، اجرا خواهد شد.
  • خط ۲۴: نخ در حال اجرای متد [Main] خاتمه می‌یابد.
  • خطوط ۲۷–۳۵: متد [Affiche] اطلاعات را نمایش می‌دهد. این متد نام رشته‌ای را که در حال اجراست، و همچنین زمان‌های شروع و پایان اجرا را نمایش می‌دهد.
  • خط ۳۱: هر رشته‌ای که متد [Affiche] را اجرا می‌کند، به مدت ۱ ثانیه متوقف می‌شود. سپس پردازنده به یک نخ دیگر که منتظر پردازنده است اختصاص داده می‌شود. در پایان مکث یک ثانیه‌ای، نخ متوقف‌شده به کاندیدای پردازنده تبدیل می‌شود. وقتی نوبتش برسد، پردازنده به آن اختصاص داده می‌شود. این موضوع به عوامل مختلفی از جمله اولویت نخ‌های دیگر منتظر پردازنده بستگی دارد.

نتایج به شرح زیر است:

Début d'exécution de la méthode Affiche dans le Thread 0 : 10:30:44
Début d'exécution de la méthode Affiche dans le Thread 1 : 10:30:44
Début d'exécution de la méthode Affiche dans le Thread 2 : 10:30:44
Début d'exécution de la méthode Affiche dans le Thread 3 : 10:30:44
Début d'exécution de la méthode Affiche dans le Thread 4 : 10:30:44
Fin du thread Main à 10:30:44
Fin d'exécution de la méthode Affiche dans le Thread 0 : 10:30:45
Fin d'exécution de la méthode Affiche dans le Thread 1 : 10:30:45
Fin d'exécution de la méthode Affiche dans le Thread 2 : 10:30:45
Fin d'exécution de la méthode Affiche dans le Thread 3 : 10:30:45
Fin d'exécution de la méthode Affiche dans le Thread 4 : 10:30:45

این نتایج بسیار آموزنده هستند:

  • اولاً، می‌توانیم ببینیم که شروع اجرای یک تار نخ مسدودکننده نیست. متد Main اجرای پنج تار نخ را به طور موازی آغاز کرد و اجرای خود را قبل از آن‌ها تکمیل نمود. عملیات
                // راه‌اندازی نخ i
                tâches[i].Start();

اجرای نخ tâches[i] را آغاز می‌کند، اما پس از انجام این کار، اجرای برنامه بلافاصله با دستور بعدی ادامه می‌یابد و منتظر پایان کار نخ نمی‌ماند.

  • تمام رشته‌های ایجاد شده باید متد Affiche را اجرا کنند. ترتیب اجرا غیرقابل پیش‌بینی است. اگرچه در این مثال، به نظر می‌رسد ترتیب اجرا از ترتیب درخواست‌های اجرا پیروی می‌کند، اما نمی‌توان از این موضوع نتیجه‌گیری کلی کرد. سیستم‌عامل در اینجا دارای ۶ نخ و یک پردازنده است. این سیستم پردازنده را بر اساس قوانین خود به این ۶ نخ تخصیص خواهد داد.
  • نتایج تأثیر متد Sleep را نشان می‌دهد. در این مثال، تِرد 0 اولین تِردی است که متد Affiche را اجرا می‌کند. پیام شروع اجرا نمایش داده می‌شود، و پس از آن متد Sleep اجرا می‌شود که آن را برای ۱ ثانیه معلق می‌کند. سپس پردازنده را از دست می‌دهد و در اختیار یک نخ دیگر قرار می‌گیرد. مثال نشان می‌دهد که این نخ ۱ است که آن را به دست می‌آورد. رشته‌ی 1 نیز همانند سایر رشته‌ها مسیر مشابهی را دنبال می‌کند. هنگامی که دوره‌ی خواب یک ثانیه‌ای رشته‌ی 0 به پایان می‌رسد، اجرای آن می‌تواند از سر گرفته شود. سیستم پردازنده را به آن اختصاص می‌دهد و می‌تواند اجرای متد Affiche را به پایان برساند.

بیایید برنامه خود را با دستورالعمل‌های زیر اصلاح کنیم تا متد Main خاتمه یابد:


            //پایان برنامه اصلی
            Console.WriteLine("Fin du thread " + main.Name);
            // تمام رشته‌ها متوقف شده‌اند
Environment.Exit(0);

اجرای برنامه جدید نتایج زیر را به دست می‌دهد:

1
2
3
4
5
6
Début d'exécution de la méthode Affiche dans le Thread 0 : 10:33:18
Début d'exécution de la méthode Affiche dans le Thread 1 : 10:33:18
Début d'exécution de la méthode Affiche dans le Thread 2 : 10:33:18
Début d'exécution de la méthode Affiche dans le Thread 3 : 10:33:18
Début d'exécution de la méthode Affiche dans le Thread 4 : 10:33:18
Fin du thread Main à 10:33:18
  • خطوط ۱–۵: رشته‌هایی که توسط تابع Main ایجاد شده‌اند، اجرای خود را آغاز کرده و به مدت ۱ ثانیه معلق می‌شوند
  • خط ۶: نخ [Main] کنترل پردازنده را بازپس می‌گیرد و دستور را اجرا می‌کند:
        Environment.Exit(0);

این دستور تمام نخ‌های برنامه را متوقف می‌کند، نه فقط نخ Main.

اگر متد Main بخواهد منتظر تمام شدن اجرای نخ‌هایی که ایجاد کرده باشد، می‌تواند از متد Join در کلاس Thread استفاده کند:


        public static void Main() {
...
            // انتظار برای تمام رشته‌ها
            for (int i = 0; i < tâches.Length; i++) {
                //در انتظار پایان اجرای نخ i
                tâches[i].Join();
            }
            //پایان main
            Console.WriteLine("Fin du thread {0} à {1:hh:mm:ss}", main.Name, DateTime.Now);
}
  • خط ۶: نخ [Main] منتظر تمام نخ‌ها می‌ماند. ابتدا هنگام انتظار برای نخ شمارهٔ ۱ مسدود می‌شود، سپس برای نخ شمارهٔ ۲ و به همین ترتیب. در نهایت، وقتی از حلقه در خطوط ۲–۵ خارج می‌شود، هر پنج نخ راه‌اندازی‌شده‌اش کار خود را به پایان رسانده‌اند.

این کار نتایج زیر را به همراه دارد:

Début d'exécution de la méthode Affiche dans le Thread 0 : 10:35:18
Début d'exécution de la méthode Affiche dans le Thread 1 : 10:35:18
Début d'exécution de la méthode Affiche dans le Thread 2 : 10:35:18
Début d'exécution de la méthode Affiche dans le Thread 3 : 10:35:18
Début d'exécution de la méthode Affiche dans le Thread 4 : 10:35:18
Fin d'exécution de la méthode Affiche dans le Thread 0 : 10:35:19
Fin d'exécution de la méthode Affiche dans le Thread 1 : 10:35:19
Fin d'exécution de la méthode Affiche dans le Thread 2 : 10:35:19
Fin d'exécution de la méthode Affiche dans le Thread 3 : 10:35:19
Fin d'exécution de la méthode Affiche dans le Thread 4 : 10:35:19
Fin du thread Main à 10:35:19
  • خط ۱۱: نخ [Main] پس از نخ‌هایی که راه‌اندازی کرده بود، به پایان رسید.

10.3. مزایای تِردها

اکنون که وجود یک نخ پیش‌فرض—همان نخی که متد Main را اجرا می‌کند—را برجسته کرده‌ایم و می‌دانیم چگونه نخ‌های دیگری ایجاد کنیم، بیایید مزایای نخل‌ها برای خودمان و دلایلی را که ما را بر آن داشته تا آنها را در اینجا ارائه دهیم، بررسی کنیم. نوعی از برنامه‌ها وجود دارد که استفاده از رشته‌ها برای آن بسیار مناسب است: برنامه‌های کلاینت-سرور در اینترنت. در فصل بعدی به بررسی این برنامه‌ها خواهیم پرداخت. در یک برنامه کاربردی کلاینت-سرور اینترنتی، یک سرور واقع در ماشینی S1 به درخواست‌های کلاینت‌هایی که در ماشین‌های راه دور C1، C2، …، Cn قرار دارند، پاسخ می‌دهد.

ما هر روز از برنامه‌های اینترنتی‌ای استفاده می‌کنیم که از این الگو پیروی می‌کنند: سرویس‌های وب، ایمیل، گشت‌وگذار در انجمن‌ها، انتقال فایل و غیره. در نمودار بالا، سرور S1 باید همزمان به کلاینت‌های Ci خدمت‌رسانی کند. اگر مثال سرور FTP (پروتکل انتقال فایل) را که فایل‌ها را به مشتریان خود تحویل می‌دهد در نظر بگیریم، می‌دانیم که انتقال یک فایل گاهی اوقات ممکن است چند دقیقه طول بکشد. البته، این که یک مشتری به مدت طولانی سرور را به انحصار خود درآورد، منتفی است. کاری که معمولاً انجام می‌شود این است که سرور به ازای تعداد مشتریان، به همان تعداد نخ اجرایی (execution thread) ایجاد کند. سپس هر نخ مسئول رسیدگی به یک مشتری خاص است. از آنجایی که پردازنده به صورت چرخه‌ای بین تمام نخ‌های فعال روی دستگاه به اشتراک گذاشته می‌شود، سرور زمان کوتاهی را با هر مشتری می‌گذراند و بدین ترتیب تضمین می‌کند که خدمات به طور همزمان ارائه می‌شوند.

در عمل، سرور از یک استخر نخ با تعداد محدودی نخ، مثلاً ۵۰ نخ، استفاده می‌کند. از مشتری پنجاه و یکم خواسته می‌شود که منتظر بماند.

10.4. تبادل اطلاعات بین نخ‌ها

در مثال‌های قبلی، یک تِرد به شرح زیر راه‌اندازی می‌شد:

Thread t=new Thread(Run);

که در آن Run روشی با امضای زیر بود:

void Run();

همچنین می‌توان از امضای زیر استفاده کرد:

void Run(object obj);

این امکان را فراهم می‌کند که اطلاعاتی به نخ راه‌اندازی‌شده منتقل شود. بنابراین

t.Start(obj1);

رشته t را راه‌اندازی می‌کند که سپس متد Run را—که از پیش با آن مرتبط شده است—با ارسال پارامتر واقعی obj1 اجرا می‌کند. در اینجا یک مثال آمده است:


using System;
using System.Threading;

namespace Chap8 {
    class Program4 {
        public static void Main() {
            // ابتدایی‌سازی نخ جاری
            Thread main = Thread.CurrentThread;
            // اعطای نام به نخ
            main.Name = "Main";

            // ایجاد رشته‌های اجرایی
            Thread[] tâches = new Thread[5];
            Data[] data = new Data[5];
            for (int i = 0; i < tâches.Length; i++) {
                // ایجاد نخ i
                tâches[i] = new Thread(Sleep);
                // تنظیم نام نخ
                tâches[i].Name = i.ToString();
                // راه‌اندازی نخ i
                tâches[i].Start(data[i] = new Data { Début = DateTime.Now, Durée = i+1 });
            }
            // انتظار برای تمام رشته‌ها
            for (int i = 0; i < tâches.Length; i++) {
                // منتظر پایان اجرای نخ i می‌شویم
                tâches[i].Join();
                // نمایش نتیجه
                Console.WriteLine("Thread {0} terminé : début {1:hh:mm:ss}, durée programmée {2} s, fin {3:hh:mm:ss}, durée effective {4}",
                    tâches[i].Name,data[i].Début,data[i].Durée,data[i].Fin,(data[i].Fin-data[i].Début));
            }        
            // پایان برنامه اصلی
            Console.WriteLine("Fin du thread {0} à {1:hh:mm:ss}", main.Name, DateTime.Now);
        }

        public static void Sleep(object infos) {
            // بازیابی پارامتر
            Data data = (Data)infos;
            // متوقف برای مدت ثانیه
            Thread.Sleep(data.Durée*1000);
            //پایان اجرا
            data.Fin = DateTime.Now;
        }
    }

    internal class Data {
        // اطلاعات متفرقه
        public DateTime Début { get; set; }
        public int Durée { get; set; }
        public DateTime Fin { get; set; }
    }
}
  • خطوط ۴۵–۵۰: اطلاعاتی از نوع [Data] که به رشته‌ها منتقل می‌شود:
    • Début: زمان شروع اجرای نخ – توسط نخ راه‌انداز تنظیم می‌شود
    • Durée: مدت‌زمان به ثانیهٔ دستور Sleep اجراشده توسط نخ راه‌انداز – توسط نخ راه‌انداز تنظیم می‌شود
    • Fin: زمان شروع اجرای نخ – توسط نخ راه‌انداز تنظیم می‌شود
  • خطوط ۳۵–۴۳: متد Sleep که توسط نخ‌ها اجرا می‌شود، امضای void Sleep(object obj) را دارد. پارامتر واقعی obj از نوع [Data] خواهد بود که در خط ۴۵ تعریف شده است.
  • خطوط ۱۵–۲۲: ایجاد ۵ نخ
  • خط ۱۷: هر تار به متد Sleep در خط ۳۵ متصل می‌شود
  • خط ۲۱: یک شیء از نوع [Data] به متد Start که نخ را راه‌اندازی می‌کند، پاس داده می‌شود. این شیء حاوی زمان شروع اجرای نخ و مدت زمانی است که باید در آن به خواب برود، به ثانیه‌محاسبه شده است. این شیء در آرایه در خط ۱۴ ذخیره می‌شود.
  • خطوط ۲۴–۳۰: نخ [Main] منتظر تمام شدن تمام نخ‌هایی که راه‌اندازی کرده است، می‌ماند.
  • خطوط ۲۸–۲۹: نخ [Main] شیء data[i] را از نخ شماره i بازیابی کرده و محتویات آن را نمایش می‌دهد.
  • خطوط ۳۵–۴۲: متد Sleep که توسط نخ‌ها اجرا می‌شود
  • خط ۳۷: پارامتر از نوع [Data] بازیابی می‌شود
  • خط ۳۹: فیلد Durée پارامتر برای تعیین مدت‌زمان Sleep استفاده می‌شود
  • خط ۴۱: فیلد پارامتر Fin مقداردهی اولیه شده است

نتایج اجرای برنامه به شرح زیر است:

1
2
3
4
5
6
Thread 0 terminé : début 11:18:50, durée programmée 1 s, fin 11:18:51, durée effective 00:00:01.0156250
Thread 1 terminé : début 11:18:50, durée programmée 2 s, fin 11:18:52, durée effective 00:00:02
Thread 2 terminé : début 11:18:50, durée programmée 3 s, fin 11:18:53, durée effective 00:00:03
Thread 3 terminé : début 11:18:50, durée programmée 4 s, fin 11:18:54, durée effective 00:00:04
Thread 4 terminé : début 11:18:50, durée programmée 5 s, fin 11:18:55, durée effective 00:00:05
Fin du thread Main à 11:18:55

این مثال نشان می‌دهد که دو نخ می‌توانند اطلاعات را مبادله کنند:

  • رشتهٔ فراخواننده می‌تواند با ارائهٔ اطلاعات به رشتهٔ فراخوانده، اجرای آن را کنترل کند
  • رشتهٔ راه‌اندازی‌شده می‌تواند نتایج را به رشتهٔ راه‌انداز بازگرداند.

برای اینکه نخ راه انداخته شده بداند چه زمانی نتایجی که منتظر آن است در دسترس قرار می‌گیرند، باید هنگام پایان یافتن نخ راه انداخته شده مطلع شود. در اینجا، با استفاده از متد Join منتظر پایان آن ماند. راه‌های دیگری نیز برای دستیابی به همین هدف وجود دارد. بعداً به این موارد خواهیم پرداخت.

10.5. دسترسی همزمان به منابع مشترک

10.5.1. دسترسی همزمان ناهمزمان

در بخش تبادل اطلاعات بین نخ‌ها، اطلاعات تنها بین دو نخ و در زمان‌های بسیار مشخصی مبادله می‌شد. این یک مثال کلاسیک از ارسال پارامتر بود. موارد دیگری نیز وجود دارد که در آن اطلاعات توسط چندین نخ به اشتراک گذاشته می‌شود که ممکن است بخواهند همزمان آن را بخوانند یا به‌روزرسانی کنند. این امر مسئله یکپارچگی این اطلاعات را مطرح می‌کند. فرض کنید که اطلاعات مشترک یک ساختار S است که حاوی اطلاعات مختلفی است: I1, I2, … در.

  • یک نخ T1 شروع به به‌روزرسانی ساختار S می‌کند: آن فیلد I1 را تغییر می‌دهد و پیش از تکمیل به‌روزرسانی کامل ساختار S متوقف می‌شود.
  • یک نخ با شناسه T2 که پردازنده را تصاحب می‌کند، سپس ساختار S را برای تصمیم‌گیری می‌خواند. این نخ ساختاری را در وضعیتی ناپایدار می‌خواند: برخی فیلدها به‌روز هستند و برخی دیگر نیستند.

این وضعیت به عنوان دسترسی به یک منبع مشترک – در این مورد، ساختار S – شناخته می‌شود و مدیریت آن اغلب بسیار دشوار است. برای روشن شدن مشکلاتی که ممکن است پیش آید، مثال زیر را در نظر بگیرید:

  • یک برنامه قرار است n نخ تولید کند، که در آن n به‌عنوان یک پارامتر ارسال می‌شود
  • منبع مشترک یک شمارنده است که باید توسط هر نخ ایجادشده افزایش یابد.
  • در پایان برنامه، مقدار شمارنده نمایش داده می‌شود. بنابراین باید n را مشاهده کنیم.

برنامه به شرح زیر است:


using System;
using System.Threading;

namespace Chap8 {
    class Program {

        // متغیرهای کلاس
        static int cptrThreads = 0;    // شمارندهٔ نخ

        //main
        public static void Main(string[] args) {
            //راهنمای کاربر
            const string syntaxe = "pg nbThreads";
            const int nbMaxThreads = 100;

            //بررسی تعداد آرگومان‌ها
            if (args.Length != 1) {
                // خطا
                Console.WriteLine(syntaxe);
                // ایست
                Environment.Exit(1);
            }
            // بررسی کیفیت آرگومان
            int nbThreads = 0;
            bool erreur = false;
            try {
                nbThreads = int.Parse(args[0]);
                if (nbThreads < 1 || nbThreads > nbMaxThreads)
                    erreur = true;
            } catch {
                // خطا
                erreur = true;
            }
            // خطا؟
            if (erreur) {
                // خطا
                Console.Error.WriteLine("Nombre de threads incorrect (entre 1 et 100)");
                //پایان
                Environment.Exit(2);
            }
            // ایجاد و تولید رشته‌ها
            Thread[] threads = new Thread[nbThreads];
            for (int i = 0; i < nbThreads; i++) {
                // ایجاد
                threads[i] = new Thread(Incrémente);
                // نام‌گذاری
                threads[i].Name = "" + i;
                // راه‌اندازی
                threads[i].Start();
            }//برای
            // انتظار برای پایان یافتن نخ‌ها
            for (int i = 0; i < nbThreads; i++) {
                threads[i].Join();
            }
            // شمارنده نمایش
            Console.WriteLine("Nombre de threads générés : " + cptrThreads);
        }

        public static void Incrémente() {
            // شمارندهٔ نخ‌ها را افزایش می‌دهد
            // شمارشگر خواندن
            int valeur = cptrThreads;
            // نظارت
            Console.WriteLine("A {0:hh:mm:ss}, le thread {1}  a lu la valeur du compteur : {2}", DateTime.Now, Thread.CurrentThread.Name, cptrThreads);
            // انتظار
            Thread.Sleep(1000);
            //افزایش شمارنده
            cptrThreads = valeur + 1;
            //ردیابی
            Console.WriteLine("A {0:hh:mm:ss}, le thread {1}  a écrit la valeur du compteur : {2}", DateTime.Now, Thread.CurrentThread.Name, cptrThreads);
        }
    }
}

ما بر روی بخش تولید نخ که قبلاً به آن پرداخته‌ایم، تمرکز نخواهیم کرد. در عوض، بیایید بر روی متد Incrémente در خط 59 تمرکز کنیم، که توسط هر نخ برای افزایش شمارنده ایستا cptrThreads در خط 8 استفاده می‌شود.

  1. خط ۶۲: شمارنده خوانده می‌شود
  2. خط ۶۶: نخ به مدت ۱ ثانیه مکث می‌کند. بنابراین کنترل پردازنده را از دست می‌دهد
  3. خط ۶۸: شمارنده افزایش می‌یابد

مرحله ۲ تنها برای مجبور کردن تار به واگذاری CPU وجود دارد. سپس CPU به تار دیگری واگذار می‌شود. در عمل، هیچ تضمینی وجود ندارد که یک تار بین لحظه‌ای که مقدار شمارنده را می‌خواند و لحظه‌ای که آن را افزایش می‌دهد، قطع نشود. حتی اگر cptrThreads++ را بنویسیم و بدین ترتیب توهم یک دستور واحد را ایجاد کنیم، این خطر وجود دارد که CPU بین لحظه‌ای که مقدار شمارنده خوانده می‌شود و لحظه‌ای که مقدار آن با ۱ افزایش نوشته می‌شود، از دست برود. در واقع، عملیات سطح بالای cptrThreads++ در سطح پردازنده به چندین دستور پایه تجزیه خواهد شد. بنابراین، خواب یک ثانیه‌ای در مرحله ۲ تنها برای سامان‌دهی این ریسک وجود دارد.

نتایج به‌دست‌آمده با ۵ نخ به شرح زیر است:

A 12:00:56, le thread 3  a lu la valeur du compteur : 0
A 12:00:56, le thread 2  a lu la valeur du compteur : 0
A 12:00:56, le thread 1  a lu la valeur du compteur : 0
A 12:00:56, le thread 0  a lu la valeur du compteur : 0
A 12:00:56, le thread 4  a lu la valeur du compteur : 0
A 12:00:57, le thread 3  a écrit la valeur du compteur : 1
A 12:00:57, le thread 2  a écrit la valeur du compteur : 1
A 12:00:57, le thread 1  a écrit la valeur du compteur : 1
A 12:00:57, le thread 0  a écrit la valeur du compteur : 1
A 12:00:57, le thread 4  a écrit la valeur du compteur : 1
Nombre de threads générés : 1

با نگاه به این نتایج، مشخص است که چه اتفاقی می‌افتد:

  • خط ۱: یک نخ اول شمارنده را می‌خواند. مقدار ۰ را می‌یابد. به مدت ۱ ثانیه مکث می‌کند و در نتیجه پردازنده را از دست می‌دهد
  • خط ۲: یک نخ دوم سپس CPU را تصاحب می‌کند و مقدار شمارنده را نیز می‌خواند. مقدار آن هنوز ۰ است زیرا نخ قبلی هنوز آن را افزایش نداده است. این نخ نیز به مدت ۱ ثانیه مکث می‌کند و در نتیجه CPU را از دست می‌دهد.
  • خطوط ۱–۵: در ۱ ثانیه، هر ۵ نخ فرصت اجرا پیدا می‌کنند و همگی مقدار ۰ را می‌خوانند.
  • خطوط ۶–۱۰: وقتی یکی پس از دیگری بیدار می‌شوند، مقداری را که خوانده‌اند (۰) افزایش داده و مقدار ۱ را در شمارنده ثبت می‌کنند، همانطور که در برنامه اصلی (Main) در خط ۱۱ تأیید شده است.

مشکل از کجا ناشی می‌شود؟ نخ دوم مقدار نادرستی را خواند زیرا نخ اول قبل از تکمیل وظیفه‌اش، یعنی به‌روزرسانی شمارنده در پنجره، متوقف شده بود. این ما را به مفهوم منبع بحرانی و بخش بحرانی در یک برنامه می‌رساند:

  • منبع بحرانی منبعی است که در هر لحظه تنها یک نخ می‌تواند آن را در اختیار داشته باشد. در اینجا، منبع بحرانی، شمارنده است.
  • بخش بحرانی یک برنامه، دنباله‌ای از دستورات در جریان اجرای یک نخ است که در طی آن به یک منبع بحرانی دسترسی پیدا می‌کند. ما باید اطمینان حاصل کنیم که در طول این بخش بحرانی، تنها همان نخ به منبع دسترسی دارد.

در مثال ما، بخش بحرانی کدی است که بین خواندن شمارنده و نوشتن مقدار جدید آن قرار دارد:


            // قرائت کنتور
            int valeur = cptrThreads;
            //در انتظار
            Thread.Sleep(1000);
            // افزایش شمارنده
cptrThreads = valeur + 1;

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

10.5.2. کلاز قفل

کلاز lock به شما امکان می‌دهد یک بخش بحرانی را به صورت زیر مشخص کنید:

lock(obj){section critique}

obj باید یک مرجع شیء باشد که برای تمام رشته‌هایی که بخش بحرانی را اجرا می‌کنند، قابل مشاهده باشد. عبارت قفل تضمین می‌کند که در هر لحظه تنها یک رشته بخش بحرانی را اجرا خواهد کرد. مثال قبلی به شکل زیر بازنویسی شده است:


using System;
using System.Threading;

namespace Chap8 {
    class Program2 {

        // متغیرهای کلاس
        static int cptrThreads = 0;    // شمارنده نخ
        static object synchro = new object(); // شیء همگام‌سازی

        //اصلی
        public static void Main(string[] args) {
    ...
            // انتظار برای پایان یافتن نخ‌ها
            Thread.CurrentThread.Name = "Main";
            for (int i = nbThreads - 1; i >= 0; i--) {
                Console.WriteLine("A {0:hh:mm:ss}, le thread {1} attend la fin du thread {2}", DateTime.Now, Thread.CurrentThread.Name, threads[i].Name);
                threads[i].Join();
                Console.WriteLine("A {0:hh:mm:ss}, le thread {1} a été prévenu de la fin du thread {2}", DateTime.Now, Thread.CurrentThread.Name, threads[i].Name);
            }
            //نمایش شمارنده
            Console.WriteLine("Nombre de threads générés : " + cptrThreads);
        }

        public static void Incrémente() {
            // شمارشگر نخ را افزایش می‌دهد
            //دسترسی انحصاری به شمارنده درخواست می‌شود
            Console.WriteLine("A {0:hh:mm:ss}, le thread {1}  attend l'autorisation d'entrer dans la section critique", DateTime.Now, Thread.CurrentThread.Name);
            lock (synchro) {
                // خواندن شمارنده
                int valeur = cptrThreads;
                //پیگیری
                Console.WriteLine("A {0:hh:mm:ss}, le thread {1}  a lu la valeur du compteur : {2}", DateTime.Now, Thread.CurrentThread.Name, cptrThreads);
                // انتظار
                Thread.Sleep(1000);
                // افزایش شمارنده
                cptrThreads = valeur + 1;
                //پیگیری
                Console.WriteLine("A {0:hh:mm:ss}, le thread {1}  a écrit la valeur du compteur : {2}", DateTime.Now, Thread.CurrentThread.Name, cptrThreads);
            }
            Console.WriteLine("A {0:hh:mm:ss}, le thread {1} a quitté la section critique", DateTime.Now, Thread.CurrentThread.Name);
        }
    }
}
  • خط ۹: synchro ابجکتی است که همگام‌سازی تمام نخ‌ها را امکان‌پذیر می‌سازد.
  • خطوط ۱۶–۲۳: متد [Main] به صورت معکوس ترتیب ایجاد شدن نخ‌ها منتظر می‌ماند.
  • خطوط 29–40: بخش بحرانی متد Incrémente توسط عبارت lock محصور شده است.

نتایج به‌دست‌آمده با ۳ نخ به شرح زیر است:

A 09:37:09, le thread 0 attend l'autorisation d'entrer dans la section critique
A 09:37:09, le thread 0 a lu la valeur du compteur : 0
A 09:37:09, le thread 1 attend l'autorisation d'entrer dans la section critique
A 09:37:09, le thread 2 attend l'autorisation d'entrer dans la section critique
A 09:37:09, le thread Main attend la fin du thread 2
A 09:37:10, le thread 0 a écrit la valeur du compteur : 1
A 09:37:10, le thread 1 a lu la valeur du compteur : 1
A 09:37:10, le thread 0 a quitté la section critique
A 09:37:11, le thread 1 a écrit la valeur du compteur : 2
A 09:37:11, le thread 1 a quitté la section critique
A 09:37:11, le thread 2 a lu la valeur du compteur : 2
A 09:37:12, le thread 2 a écrit la valeur du compteur : 3
A 09:37:12, le thread 2 a quitté la section critique
A 09:37:12, le thread Main a été prévenu de la fin du thread 2
A 09:37:12, le thread Main attend la fin du thread 1
A 09:37:12, le thread Main a été prévenu de la fin du thread 1
A 09:37:12, le thread Main attend la fin du thread 0
A 09:37:12, le thread Main a été prévenu de la fin du thread 0
Nombre de threads générés : 3
  • رشتهٔ ۰ اولین کسی است که وارد بخش بحرانی می‌شود: خطوط ۱، ۲، ۶، ۸
  • دو نخ دیگر تا زمانی که نخ ۰ از بخش بحرانی خارج نشده باشد مسدود خواهند ماند: خطوط ۳ و ۴
  • سپس نخ ۱ اجرا می‌شود: خطوط ۷، ۹، ۱۰
  • سپس نخ ۲ اجرا می‌شود: خطوط ۱۱، ۱۲، ۱۳
  • خط ۱۴: نخ اصلی که منتظر پایان نخ ۲ بود، مطلع می‌شود
  • خط ۱۵: نخ اصلی اکنون منتظر پایان نخ ۱ است. نخ ۱ قبلاً به پایان رسیده است. نخ اصلی بلافاصله در خط ۱۶ مطلع می‌شود.
  • خطوط ۱۷–۱۸: همین فرآیند با رشته ۰ نیز رخ می‌دهد.
  • خط ۱۹: تعداد رشته‌ها صحیح است

10.5.3. کلاس Mutex

کلاس System.Threading.Mutex همچنین امکان محدود کردن بخش‌های بحرانی را فراهم می‌کند. این کلاس از نظر دیده‌شدن با دستور قفل تفاوت دارد:

  • کلاز قفل برای همگام‌سازی رشته‌ها در داخل یک برنامه واحد استفاده می‌شود
  • کلاس Mutex امکان همگام‌سازی نخ‌های برنامه‌های مختلف را فراهم می‌کند.

ما از سازنده و متدهای زیر استفاده خواهیم کرد:

public Mutex()
یک M از کلاس Mutex ایجاد می‌کند
public bool WaitOne()
رشته‌ای با شناسه T1 که عملیات M.WaitOne() را اجرا می‌کند، مالکیت شیء همگام‌سازی M را درخواست می‌کند. اگر شیء M با شناسه Mutex توسط هیچ رشته‌ای در اختیار گرفته نشده باشد (که در ابتدا همین‌طور است)، این مالکیت به نخ T1 که آن را درخواست کرده بود، «اعطا» می‌شود. اگر کمی بعد، نخی T2 همان عملیات را انجام دهد، مسدود خواهد شد. این به این دلیل است که یک Mutex فقط می‌تواند متعلق به یک نخ باشد. این زمانی آزاد می‌شود که نخ T1، M را که در اختیار دارد، رها کند. بنابراین ممکن است چندین نخ در حین انتظار برای M، Mutex مسدود شوند.
public void ReleaseMutex()
رشته‌ی T1 که عملیات M.ReleaseMutex() را انجام می‌دهد، مالکیت Mِ Mutex را واگذار می‌کند. هنگامی که رشته‌ی T1 پردازنده را از دست می‌دهد، سیستم ممکن است آن را به یکی از رشته‌هایی که منتظر mutex M هستند اختصاص دهد. تنها یکی به‌ترتیب آن را به‌دست می‌آورد؛ سایر رشته‌های منتظر M مسدود باقی می‌مانند

یک Mutex M دسترسی به منبع مشترک R را مدیریت می‌کند. یک نخ از طریق M.WaitOne() منبع R را درخواست می‌کند و از طریق M.ReleaseMutex() آن را آزاد می‌کند. یک بخش بحرانی کد که باید به‌طور همزمان تنها توسط یک نخ اجرا شود، یک منبع مشترک است. اجرای بخش بحرانی را می‌توان به شرح زیر همگام‌سازی کرد:

M.WaitOne();
// فقط این نخ وارد اینجا می‌شود
//بخش بحرانی
....
M.ReleaseMutex();

که در آن M یک شیء Mutex است. مهم است که فراموش نکنیم یک Mutex را که دیگر مورد نیاز نیست آزاد کنیم تا نخ دیگری بتواند وارد بخش بحرانی شود؛ در غیر این صورت، نخ‌هایی که منتظر Mutex هستند که هرگز آزاد نشده است، هرگز به پردازنده دسترسی پیدا نخواهند کرد.

اگر آنچه را که به تازگی دیدیم بر مثال قبلی اعمال کنیم، برنامه ما به شکل زیر درمی‌آید:


using System;
using System.Threading;

namespace Chap8 {
    class Program3 {

        // متغیرهای کلاس
        static int cptrThreads = 0;    // شمارنده نخ
        static Mutex synchro = new Mutex(); // شیء همگام‌سازی

        //main
        public static void Main(string[] args) {
    ...
        }

        public static void Incrémente() {
....
            synchro.WaitOne();
            try {
...
            } finally {
...
                synchro.ReleaseMutex();
            }
        }
    }
}
  • خط ۹: شیء همگام‌سازی نخ اکنون یک Mutex است.
  • خط ۱۸: شروع بخش بحرانی – تنها یک نخ باید وارد آن شود. ما منتظر می‌مانیم تا Mutex synchro آزاد شود.
  • خط ۳۳: از آنجا که یک Mutex باید همیشه آزاد شود، صرف نظر از اینکه استثنا رخ دهد یا نه، بخش بحرانی با استفاده از یک بلوک try/finally برای آزاد کردن Mutex درون finally مدیریت می‌شود.
  • خط ۲۳: Mutex پس از اتمام بخش بحرانی آزاد می‌شود.

نتایج به‌دست‌آمده مشابه قبل است.

10.5.4. کلاس AutoResetEvent

یک شیء AutoResetEvent به‌عنوان یک مانع عمل می‌کند و تنها به یک نخ اجازه عبور هم‌زمان را می‌دهد، درست مانند دو ابزار قبلی lock و Mutex. یک شیء AutoResetEvent به‌صورت زیر ساخته می‌شود:

AutoResetEvent barrière=new AutoresetEvent(bool état);

متغیر بولی état نشان می‌دهد که مانع بسته (false) است یا باز (true). یک نخ که می‌خواهد از مانع عبور کند، به شرح زیر این موضوع را اعلام می‌کند:

barrière.WaitOne();
  • اگر مانع باز باشد، نخ از آن عبور می‌کند و مانع پشت سر آن بسته می‌شود. اگر چندین نخ در انتظار باشند، تضمین می‌شود که تنها یکی از آنها عبور خواهد کرد.
  • اگر مانع بسته باشد، نخ مسدود می‌شود. نخ دیگری در زمان مناسب آن را باز خواهد کرد. این زمان‌بندی کاملاً به مسئله‌ای که حل می‌شود بستگی دارد. مانع توسط عملیات زیر باز خواهد شد:
barrière.Set(); 

یک نخ ممکن است بخواهد یک مانع را ببندد. این کار را با استفاده از موارد زیر انجام می‌دهد:

barrière.Reset(); 

اگر در مثال قبلی، شیء Mutex را با یک شیء از نوع AutoResetEvent جایگزین کنیم، کد به شکل زیر درمی‌آید:


using System;
using System.Threading;

namespace Chap8 {
    class Program4 {

        // متغیرهای کلاس
        static int cptrThreads = 0;    // شمارندهٔ نخ
        static EventWaitHandle synchro = new AutoResetEvent(false); // شیء همگام‌سازی

        //اصلی
        public static void Main(string[] args) {
....
            //مانع بخش بحرانی باز می‌شود
            Console.WriteLine("A {0:hh:mm:ss}, le thread {1} ouvre la barrière de la section critique", DateTime.Now, Thread.CurrentThread.Name);
            synchro.Set();
            // منتظر پایان نخ‌ها
...
            // شمارنده نمایش
            Console.WriteLine("Nombre de threads générés : " + cptrThreads);
        }

        public static void Incrémente() {
            // شمارنده نخ‌ها را افزایش می‌دهد
            //دسترسی انحصاری به شمارنده درخواست می‌شود
...
            synchro.WaitOne();
            try {
...
            } finally {
                // منبع آزاد می‌شود
...
                synchro.Set();
            }
        }
    }
}
  • خط ۹: مانع در حالت بسته ایجاد می‌شود. این مانع توسط نخ Main در خط ۱۶ باز خواهد شد.
  • خط ۲۷: نخ مسئول افزایش شمارنده نخ‌ها درخواست اجازه ورود به بخش بحرانی را می‌کند. نخ‌های مختلف در مقابل مانع بسته صف می‌کشند. وقتی نخ Main آن را باز می‌کند، یکی از نخ‌های منتظر عبور می‌کند.
  • خط ۳۳: پس از اتمام کار، مانع را دوباره باز می‌کند و به نخ دیگری اجازه ورود می‌دهد.

نتایج مشابه نتایج قبلی به دست می‌آید.

10.5.5. کلاس Interlocked

کلاس Interlocked به یک گروه از عملیات اجازه می‌دهد تا اتمیک باشند. در یک گروه عملیات atomique، یا همه عملیات توسط رشته‌ای که گروه را اجرا می‌کند، انجام می‌شوند، یا هیچ‌کدام انجام نمی‌شوند. سیستم در حالتی باقی نمی‌ماند که در آن برخی از عملیات اجرا شده و برخی دیگر اجرا نشده باشند. ابجکت‌های همگام‌سازی lock، Mutex و AutoResetEvent همگی برای تبدیل atomique به یک گروه عملیات به کار می‌روند. این نتیجه به قیمت مسدود شدن نخ‌ها به دست می‌آید. کلاس Interlocked امکان اجتناب از مسدودسازی نخ‌ها را برای عملیات ساده اما نسبتاً پرتکرار فراهم می‌کند. کلاس Interlocked متدهای استاتیک زیر را ارائه می‌دهد:

Image

متد Increment دارای امضای زیر است:

public static int Increment(ref int location);

این متد پارامتر location را به مقدار ۱ افزایش می‌دهد. تضمین می‌شود که این عملیات atomique باشد.

برنامهٔ شمارش رشته‌های ما می‌تواند به صورت زیر باشد:


using System;
using System.Threading;

namespace Chap8 {
    class Program5 {

        // متغیرهای کلاس
        static int cptrThreads = 0;    // شمارنده نخ

        //اصلی
        public static void Main(string[] args) {
...
        }

        public static void Incrémente() {
            // افزایش شمارندهٔ نخ
            Interlocked.Increment(ref cptrThreads);
        }
    }
}
  • خط ۱۷: شمارنده نخ به‌صورت اتمی افزایش می‌یابد.

10.6. دسترسی همزمان به چندین منبع مشترک

10.6.1. یک مثال

در مثال‌های قبلی ما، یک منبع واحد توسط نخ‌های مختلف به اشتراک گذاشته می‌شد. اگر چندین منبع وجود داشته باشد و آن‌ها به یکدیگر وابسته باشند، وضعیت می‌تواند پیچیده‌تر شود. به‌ویژه ممکن است وضعیت بن‌بست (deadlock) رخ دهد. این وضعیت، که همچنین با نام deadlock شناخته می‌شود، حالتی است که دو نخ در انتظار یکدیگر هستند. اقدامات زیر را در نظر بگیرید که به ترتیب اتفاق می‌افتند:

  • یک نخ T1 یک متکس M1 را برای دسترسی به یک منبع مشترک R1 به دست می‌آورد
  • یک نخ T2 برای دسترسی به یک منبع مشترک R2، یک متکس M2 را به دست می‌آورد.
  • رشته T1 مَیتکس M2 را درخواست می‌کند. مسدود شده است.
  • رشته T2 مکس M1 را درخواست می‌کند. مسدود شده است.

در اینجا، نخ‌های T1 و T2 منتظر یکدیگر هستند. این وضعیت زمانی پیش می‌آید که نخ‌ها به دو منبع مشترک نیاز دارند: منبع R1 که توسط موتکس M1 کنترل می‌شود، و منبع R2 که توسط موتکس M2 کنترل می‌شود. یک راه‌حل ممکن این است که با استفاده از یک موتکس واحد M، هر دو منبع را به‌طور همزمان درخواست کنیم. با این حال، این همیشه ممکن نیست اگر، برای مثال، منجر به این شود که منبعی که به‌دست‌آوردنش پرهزینه است برای مدت طولانی درگیر شود. راه حل دیگر این است که یک تار که M1 را در اختیار دارد و قادر به تصاحب M2 نیست، برای جلوگیری از بن‌بست، M1 را آزاد کند.

  1. ما یک آرایه داریم که برخی رشته‌ها در آن داده‌نویسی می‌کنند (نویسنده‌ها) و برخی دیگر آن را می‌خوانند (خواننده‌ها).
  2. نویسنده‌ها نسبت به یکدیگر برابر اما انحصاری هستند: در هر لحظه تنها یک نویسنده می‌تواند داده‌ها را در آرایه قرار دهد.
  3. خواننده‌ها همه برابر هستند اما به‌طور متقابل انحصاری عمل می‌کنند: تنها یک خواننده در هر لحظه می‌تواند داده‌های ذخیره‌شده در آرایه را بخواند.
  4. یک خواننده تنها پس از آنکه یک نویسنده داده‌ای را در آرایه نوشته باشد، می‌تواند آن را بخواند، و یک نویسنده تنها پس از آنکه داده‌های موجود توسط یک خواننده خوانده شده باشد، می‌تواند داده‌ی جدیدی را در آرایه بنویسد.

ما می‌توانیم بین دو نوع منابع مشترک تمایز قائل شویم:

  • جدول قابل نوشتن: تنها یک نویسنده در هر زمان می‌تواند به آن دسترسی داشته باشد.
  • جدول فقط-خواندنی: تنها یک خواننده در هر لحظه می‌تواند به آن دسترسی داشته باشد.

و ترتیبی که این منابع باید به کار گرفته شوند:

  • یک خواننده باید همیشه پس از یک نویسنده بیاید.
  • یک نویسنده باید همیشه پس از یک خواننده بیاید، به جز بار اول.

دسترسی به این دو منبع را می‌توان با استفاده از دو مانع از نوع AutoResetEvent کنترل کرد:

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

برنامه‌ای که این همگام‌سازی مبتنی بر رویداد را نشان می‌دهد به شرح زیر است:


using System;
using System.Threading;

namespace Chap8 {
    class Program {
        // استفاده از رشته‌های خواندن و نوشتن
        // استفاده از رویدادهای همگام‌سازی را نشان می‌دهد


        // متغیرهای کلاس
        static int[] data = new int[3];    // منبعی که بین رشته‌های خواندن و نوشتن مشترک است
        static Random objRandom = new Random(DateTime.Now.Second);    // یک تولیدکنندهٔ عدد تصادفی
        static AutoResetEvent peutLire;    // نشان می‌دهد که محتویات `data` قابل خواندن هستند
        static AutoResetEvent peutEcrire;    // نشان می‌دهد که محتویات `data` قابل نوشتن هستند

        //اصلی
        public static void Main(string[] args) {

            // تعداد رشته‌هایی که باید تولید شوند
            const int nbThreads = 2;

            // ابتدای‌سازی پرچم‌ها
            peutLire = new AutoResetEvent(false);    //هنوز قابل خواندن نیست
            peutEcrire = new AutoResetEvent(true);    //نوشتن از قبل ممکن است

            // ایجاد رشته‌های خواندن
            Thread[] lecteurs = new Thread[nbThreads];
            for (int i = 0; i < nbThreads; i++) {
                // ایجاد
                lecteurs[i] = new Thread(Lire);
                lecteurs[i].Name = "L" + i.ToString();
                // راه‌اندازی
                lecteurs[i].Start();
            }

            //ایجاد نخ‌های نویسنده
            Thread[] écrivains = new Thread[nbThreads];
            for (int i = 0; i < nbThreads; i++) {
                //ایجاد
                écrivains[i] = new Thread(Ecrire);
                écrivains[i].Name = "E" + i.ToString();
                //شروع
                écrivains[i].Start();
            }

            //پایان دست
            Console.WriteLine("Fin de Main...");
        }

        // خواندن محتویات آرایه
        public static void Lire() {
...
        }

        //نوشتن در آرایه
        public static void Ecrire() {
....
        }
    }
}
  • خط ۱۱: آرایه data منبع مشترک بین نخ‌های خواننده و نویسنده است. این آرایه برای خواندن توسط نخ‌های خواننده و برای نوشتن توسط نخ‌های نویسنده به اشتراک گذاشته می‌شود.
  • خط ۱۳: شیء peutLire برای اطلاع‌رسانی به نخ‌های خواننده استفاده می‌شود که می‌توانند آرایه data را بخوانند. این مقدار توسط نخ نویسنده‌ای که آرایه data را پر کرده است، روی true تنظیم می‌شود. این مقدار در خط ۲۳ روی false مقداردهی اولیه می‌شود. یک نخ نویسنده باید ابتدا آرایه را پر کند و سپس رویداد را از peutLire به vrai ارسال نماید.
  • خط ۱۴: شیء peutEcrire برای اطلاع‌رسانی به نخ‌های نویسنده استفاده می‌شود که می‌توانند در آرایه data بنویسند. این مقدار توسط نخ خواننده که پردازش کل آرایه data را به پایان رسانده است، روی true تنظیم می‌شود. این مقدار در خط ۲۴ روی true مقداردهی اولیه می‌شود. دلیل آن این است که آرایه data برای نوشتن آزاد است.
  • خطوط ۲۷–۳۴: ایجاد و راه‌اندازی نخ‌های خواننده
  • خطوط ۳۷–۴۴: ایجاد و راه‌اندازی نخ‌های نویسنده

روش Lire که توسط نخ‌های خواننده اجرا می‌شود به شرح زیر است:


public static void Lire() {
            // نظارت
            Console.WriteLine("Méthode [Lire] démarrée par le thread n° {0}", Thread.CurrentThread.Name);
            // ما باید منتظر تأیید خواندن بمانیم
            peutLire.WaitOne();
            // خواندن جدول
            for (int i = 0; i < data.Length; i++) {
                //منتظر بمانید 1 ثانیه
                Thread.Sleep(1000);
                //نمایش
                Console.WriteLine("{0:hh:mm:ss} : Le lecteur {1} a lu le nombre {2}", DateTime.Now, Thread.CurrentThread.Name, data[i]);
            }
            //دسترسی نوشتن اعطا شد
            peutEcrire.Set();
            // ردیابی
            Console.WriteLine("Méthode [Lire] terminée par le thread n° {0}", Thread.CurrentThread.Name);
        }
  • خط ۵: برنامه منتظر است تا نخ نویسنده سیگنال دهد که آرایه پر شده است. وقتی این سیگنال دریافت شود، تنها یکی از نخ‌های خواننده که منتظر این سیگنال بوده‌اند اجازه ادامه پیدا می‌کند.
  • خطوط ۷–۱۲: پردازش آرایه data با یک Sleep در وسط برای وادار کردن نخ به واگذاری پردازنده.
  • خط ۱۴: به نخ‌های نویسنده اطلاع می‌دهد که آرایه خوانده شده و می‌توان آن را دوباره پر کرد.

روش Ecrire که توسط نخ‌های نویسنده اجرا می‌شود به شرح زیر است:


public static void Ecrire() {
            //پیگیری
            Console.WriteLine("Méthode [Ecrire] démarrée par le thread n° {0}", Thread.CurrentThread.Name);
            //باید منتظر مجوز نوشتن بماند
            peutEcrire.WaitOne();
            //نوشتن در آرایه
            for (int i = 0; i < data.Length; i++) {
                //در حال انتظار ۱ ثانیه
                Thread.Sleep(1000);
                // نمایش
                data[i] = objRandom.Next(0, 1000);
                Console.WriteLine("{0:hh:mm:ss} : L'écrivain {1} a écrit le nombre {2}", DateTime.Now, Thread.CurrentThread.Name, data[i]);
            }
            // می‌تواند بخواند
            peutLire.Set();
            // نظارت
            Console.WriteLine("Méthode [Ecrire] terminée par le thread n° {0}", Thread.CurrentThread.Name);
        }
  • خط ۵: منتظر می‌ماند تا یک نخ خواننده سیگنال دهد که آرایه خوانده شده است. وقتی این سیگنال دریافت شود، تنها یکی از نخ‌های نویسنده که منتظر این سیگنال بوده‌اند اجازه ادامه پیدا می‌کند.
  • خطوط ۷–۱۳: پردازش آرایه data با یک Sleep در وسط برای وادار کردن نخ به واگذاری پردازنده.
  • خط ۱۵: به نخ‌های خواننده اطلاع می‌دهد که آرایه پر شده و می‌توان آن را دوباره خواند.

اجرا نتایج زیر را به دست می‌دهد:

Méthode [Lire] démarrée par le thread n° L0
Méthode [Lire] démarrée par le thread n° L1
Méthode [Ecrire] démarrée par le thread n° E0
Méthode [Ecrire] démarrée par le thread n° E1
Fin de Main...
02:29:18 : L'écrivain E0 a écrit le nombre 607
02:29:19 : L'écrivain E0 a écrit le nombre 805
02:29:20 : L'écrivain E0 a écrit le nombre 650
Méthode [Ecrire] terminée par le thread n° E0
02:29:21 : Le lecteur L0 a lu le nombre 607
02:29:22 : Le lecteur L0 a lu le nombre 805
02:29:23 : Le lecteur L0 a lu le nombre 650
Méthode [Lire] terminée par le thread n° L0
02:29:24 : L'écrivain E1 a écrit le nombre 186
02:29:25 : L'écrivain E1 a écrit le nombre 881
02:29:26 : L'écrivain E1 a écrit le nombre 415
Méthode [Ecrire] terminée par le thread n° E1
02:29:27 : Le lecteur L1 a lu le nombre 186
02:29:28 : Le lecteur L1 a lu le nombre 881
02:29:29 : Le lecteur L1 a lu le nombre 415
Méthode [Lire] terminée par le thread n° L1

نکات زیر قابل توجه است:

  • در واقع در هر لحظه تنها یک خواننده وجود دارد، اگرچه این خواننده در بخش بحرانی Lire پردازنده را از دست می‌دهد
  • در واقع در هر لحظه تنها یک نویسنده وجود دارد، اگرچه این نویسنده در بخش بحرانی CPU را از دست می‌دهد Ecrire
  • یک خواننده تنها زمانی می‌خواند که چیزی برای خواندن در آرایه وجود داشته باشد
  • یک نویسنده تنها پس از اینکه آرایه به‌طور کامل خوانده شده باشد، می‌نویسد

10.6.2. کلاس Monitor

در مثال قبلی:

  • دو منبع مشترک برای مدیریت وجود دارد
  • برای یک منبع معین، نخ‌ها برابر هستند.

وقتی رشته‌های نویسنده روی دستور peutEcrire.WaitOne مسدود می‌شوند، یکی از آن‌ها – هر کدام – توسط عملیات peutEcrire.Set آزاد می‌شود. اگر عملیات قبلی برای باز کردن مانع برای یک نویسندهٔ خاص در نظر گرفته شده باشد، اوضاع پیچیده‌تر می‌شود.

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

ما می‌توانیم به شیوه‌ای مشابه در اینجا عمل کنیم. بیایید مثال نویسندگان را در نظر بگیریم:

plusieurs écrivains attendent pour un même guichet
رده‌هایشان مسدود شده است
le guichet se libère et le n° de l'écrivain suivant est appelé
رشته‌ای که از سیگنال‌های آرایه می‌خواند به نویسنده‌ها اطلاع می‌دهد که آرایه در دسترس است. یا این رشته یا رشته‌ای دیگر رشتهٔ نویسنده را قفل کرده است، که سپس باید مانع را عبور دهد.
chaque écrivain regarde son n° et seul celui qui a le n° appelé
va au guichet. Les autres se remettent en attente.
هر نخ بررسی می‌کند که آیا نخ انتخاب‌شده است یا خیر. اگر چنین باشد، از مانع عبور می‌کند. در غیر این صورت، به صف انتظار بازمی‌گردد.

کلاس Monitor امکان پیاده‌سازی این سناریو را فراهم می‌کند.

Image

اکنون یک الگوی استاندارد (pattern) را شرح می‌دهیم که در فصل Threading کتاب *C# 3.0* که در مقدمه این سند به آن ارجاع شده است، ارائه شده و قادر به حل مشکلات مانع با شرایط ورود است.

  • اولاً، نخ‌هایی که یک منبع (مانند شمارنده و غیره) را به اشتراک می‌گذارند، از طریق ابجکتی که آن را توکن می‌نامیم، به آن دسترسی پیدا می‌کنند. برای باز کردن مانع منتهی به شمارنده، باید توکن را در اختیار داشت و تنها یک توکن وجود دارد. بنابراین، نخ‌ها باید توکن را بین خود منتقل کنند.
object jeton=new object();
  • برای رسیدن به باجه، نخ‌ها ابتدا توکن را درخواست می‌کنند:
Monitor.Enter(jeton);

اگر توکن موجود باشد، به تریدی که عملیات قبلی را اجرا کرده است داده می‌شود؛ در غیر این صورت، آن ترید در لیست انتظار برای توکن قرار می‌گیرد.

  • اگر دسترسی به شمارنده بدون ترتیب باشد، c.a.d. در مواردی که ترتیب ورود افراد اهمیتی ندارد، عملیات قبلی کافی است. رشته‌ای که توکن را در اختیار دارد به شمارنده می‌رود. اگر دسترسی ترتیب‌دار باشد، رشتهٔ دارندهٔ توکن بررسی می‌کند که شرط رفتن به شمارنده را دارد:
while (! jeNeSuisPasCeluiQuiEstAttendu) {Monitor.Wait(jeton);}

اگر نخ، نخ مورد انتظار در پیشخوان نباشد، با بازگرداندن توکن نوبت خود را واگذار می‌کند. این نخ وارد حالت مسدود شده (blocked) می‌شود. به محض اینکه توکن دوباره برای او در دسترس قرار گیرد، بیدار خواهد شد. سپس یک بار دیگر بررسی می‌کند که آیا شرط لازم برای رفتن به پیشخوان را دارد یا خیر. عملیات Monitor.Wait(token)، که توکن را آزاد می‌کند، تنها در صورتی قابل انجام است که نخ مالک توکن باشد. در غیر این صورت، یک استثنا پرتاب می‌شود.

  • رشته‌ای که شرط پیشروی به سمت شمارنده را بررسی می‌کند، به این صورت عمل می‌کند:
  1. // کار در کانتر
  2. ....

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

  • وضعیت اول جایی است که رشته‌ای که توکن را در اختیار دارد، همان رشته‌ای است که به رشته‌های منتظر برای توکن سیگنال می‌دهد که توکن آزاد شده است. این کار به شرح زیر انجام می‌شود:
1
2
3
4
5
6
7
8
// کار شمارنده
....
//تغییر شرایط دسترسی به شمارنده
...
// بیدار کردن نخ‌هایی که منتظر توکن هستند
Monitor.PulseAll(jeton);
// رهاسازی توکن
Monitor.Exit(jeton);

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

  • سناریوی دوم زمانی است که رشته‌ای که توکن را در اختیار دارد، مسئول سیگنال‌دهی به رشته‌های منتظر برای توکن مبنی بر آزاد بودن آن نیست. با این حال، او باید توکن را آزاد کند زیرا رشته مسئول ارسال این سیگنال باید دارنده توکن باشد. او این کار را با استفاده از عملیات زیر انجام خواهد داد:
Monitor.Exit(jeton);

توکن اکنون در دسترس است، اما رشته‌هایی که منتظر آن هستند (که عملیات Wait(token) را انجام داده‌اند) مطلع نمی‌شوند. این وظیفه به یک رشته دیگر سپرده شده است که در مقطعی کد مشابه زیر را اجرا خواهد کرد:

1
2
3
4
5
6
7
8
// دریافت توکن
Monitor.Enter(jeton);
// اصلاح شرط دسترسی صف
....
// بیدار کردن نخ‌های منتظر توکن
Monitor.PulseAll(jeton);
// رهاسازی توکن
Monitor.Exit(jeton);

در نهایت، ساختار استاندارد پیشنهادی در فصل Threading کتاب C# 3.0 به شرح زیر است:

  • توکن دسترسی را در شمارنده تعریف کنید:
object jeton=new object();
  • درخواست دسترسی به شمارنده:
lock(jeton){
    while (! jeNeSuisPasCeluiQuiEstAttendu) 
        Monitor.Wait(jeton);
}
// مراجعه به باجه
...
lock(jeton){...} 

برابر است با

Monitor.Enter(jeton);
try{...} finally{Monitor.Exit(jeton);}

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

lock(jeton){
    while (! jeNeSuisPasCeluiQuiEstAttendu) 
        Monitor.Wait(jeton);
     // مراجعه به باجه
    ...
}

که توکن تنها پس از پردازش تراکنش آزاد می‌شود.

  • شرط دسترسی به کانتر را تغییر دهید و سایر نخ‌ها را مطلع کنید
lock(jeton){
     // تغییر شرط دسترسی به صف
    ...
     // اطلاع‌رسانی به رشته‌هایی که منتظر توکن هستند
    Monitor.PulseAll(jeton);
}

در مثال بالا، شرط دسترسی تنها توسط رشته‌ای که توکن را در اختیار دارد قابل تغییر است. همچنین می‌توانیم بنویسیم:

     //تغییر شرط دسترسی به توکن
    ...
     // اطلاع‌رسانی به نخ‌های منتظر توکن
    Monitor.PulseAll(jeton);
     // رها کردن توکن
    Monitor.Exit(jeton);

اگر نخ قبلاً توکن را داشته باشد.

با در اختیار داشتن این اطلاعات، می‌توانیم برنامهٔ خواندن/نوشتن را با تعیین اولویتی برای خواننده‌ها و نویسنده‌ها جهت دسترسی به کانال‌های مربوطه بازنویسی کنیم. کد به شرح زیر است:


using System;
using System.Threading;

namespace Chap8 {
    class Program2 {
        // استفاده از رشته‌های خواندن و نوشتن
        // استفاده از رویدادهای همگام‌سازی را نشان می‌دهد


        // متغیرهای کلاس
        static int[] data = new int[3];            // منبعی که بین رشته‌های خواندن و نوشتن مشترک است
        static Random objRandom = new Random(DateTime.Now.Second);    // یک تولیدکنندهٔ عدد تصادفی
        static object peutLire = new object();        // نشان می‌دهد که محتویات `data` قابل خواندن هستند
        static object peutEcrire = new object();    // نشان می‌دهد که محتویات `data` قابل نوشتن هستند
        static bool lectureAutorisée = false;    //برای مجاز کردن خواندن آرایه
        static bool écritureAutorisée = false;    //برای مجاز کردن نوشتن به آرایه
        static string[] ordreLecture;    //ترتیب خواننده‌ها را تعیین می‌کند
        static string[] ordreEcriture;    //ترتیب نویسنده‌ها را تعیین می‌کند
        static int lecteurSuivant = 0;    // نشان‌دهنده شماره خواننده بعدی است
        static int écrivainSuivant = 0;    // نشان‌دهنده شماره نویسنده بعدی است

        //اصلی
        public static void Main(string[] args) {

            // تعداد رشته‌هایی که باید ایجاد شوند
            const int nbThreads = 5;

            // ایجاد نخ‌های خواننده
            Thread[] lecteurs = new Thread[nbThreads];
            for (int i = 0; i < nbThreads; i++) {
                // ایجاد
                lecteurs[i] = new Thread(Lire);
                lecteurs[i].Name = "L" + i.ToString();
                // راه‌اندازی
                lecteurs[i].Start();
            }

            // ایجاد ترتیب خواندن
            ordreLecture = new string[nbThreads];
            for (int i = 0; i < nbThreads; i++) {
                ordreLecture[i] = lecteurs[nbThreads - i - 1].Name;
                Console.WriteLine("Le lecteur {0} est en position {1}", ordreLecture[i], i);
            }

            // ایجاد نخ‌های نویسنده
            Thread[] écrivains = new Thread[nbThreads];
            for (int i = 0; i < nbThreads; i++) {
                // ایجاد
                écrivains[i] = new Thread(Ecrire);
                écrivains[i].Name = "E" + i.ToString();
                // شروع
                écrivains[i].Start();
            }

            //ایجاد ترتیب نوشتن
            ordreEcriture = new string[nbThreads];
            for (int i = 0; i < nbThreads; i++) {
                ordreEcriture[i] = écrivains[i].Name;
                Console.WriteLine("L'écrivain {0} est en position {1}", ordreEcriture[i], i);
            }

             // مجوز نوشتن
            lock (peutEcrire) {
               écritureAutorisée = true;
                Monitor.Pulse(peutEcrire);
            }


            //پایان دست
            Console.WriteLine("Fin de Main...");
        }

        // خواندن محتویات جدول
        public static void Lire() {
...
        }

        //نوشتن در آرایه
        public static void Ecrire() {
...
        }
    }
}

دسترسی به پورت خواندن مشروط به شرایط زیر است:

  • خط ۱۳: توکن peutLire
  • خط ۱۵: بولین lectureAutorisée
  • خط ۱۷: آرایه مرتب‌شده خواننده‌ها. خواننده‌ها طبق ترتیبی که در این آرایه مشخص شده و حاوی نام آن‌هاست، به سمت باجه خواندن پیش می‌روند.
  • خط ۱۹: lecteurSuivant نشان‌دهنده شماره خواننده بعدی مجاز برای مراجعه به میز پذیرش است.

دسترسی به میز ثبت‌نام مشروط به شرایط زیر است:

  • خط ۱۴: توکن peutEcrire
  • خط ۱۶: بولین écritureAutorisée
  • خط ۱۸: آرایه مرتب‌شده نویسندگان. نویسندگان در ترتیبی که توسط این آرایه مشخص شده است، به میز ثبت می‌روند، که شامل نام‌های آن‌هاست.
  • خط ۲۰: écrivainSuivant نشان‌دهنده شماره نویسنده بعدی مجاز برای مراجعه به میز است.

سایر عناصر کد به شرح زیر هستند:

  • خطوط ۲۹–۳۶: ایجاد و راه‌اندازی نخ‌های خواننده. همه آن‌ها مسدود خواهند شد زیرا خواندن مجاز نیست (خط ۱۵).
  • خطوط ۳۹–۴۳: ترتیب نزدیک شدن آن‌ها به صندوق برعکس ترتیب ایجادشان خواهد بود.
  • خطوط ۴۶–۵۳: ایجاد و راه‌اندازی نخ‌های نویسنده. همه آنها مسدود خواهند شد زیرا نوشتن مجاز نیست (خط ۱۶).
  • خطوط ۵۶–۶۰: آن‌ها در ترتیبی که ایجاد شده‌اند، در صندوق‌دار خدمت‌رسانی خواهند شد.
  • خط ۶۴: نوشتن مجاز است
  • خط ۶۵: نویسندگان مطلع می‌شوند که چیزی تغییر کرده است.

روش Lire به شرح زیر است:


        public static void Lire() {
            // ردیابی
            Console.WriteLine("Méthode [Lire] démarrée par le thread n° {0}", Thread.CurrentThread.Name);
            //باید منتظر تأیید خواندن بماند
            lock (peutLire) {
                while (!lectureAutorisée || ordreLecture[lecteurSuivant] != Thread.CurrentThread.Name) {
                    Monitor.Wait(peutLire);
                }
                // خواندن جدول
                for (int i = 0; i < data.Length; i++) {
                    //در حال انتظار ۱ ثانیه
                    Thread.Sleep(1000);
                    //نمایش
                    Console.WriteLine("{0:hh:mm:ss} : Le lecteur {1} a lu le nombre {2}", DateTime.Now, Thread.CurrentThread.Name, data[i]);
                }
                 // خواننده بعدی
                lectureAutorisée = false;
                lecteurSuivant++;
                // به نویسندگان اطلاع دهید که می‌توانند بنویسند
                lock (peutEcrire) {
                    écritureAutorisée = true;
                    Monitor.PulseAll(peutEcrire);
                }

                //پیگیری
                Console.WriteLine("Méthode [Lire] terminée par le thread n° {0}", Thread.CurrentThread.Name);
            }
}
  • دسترسی به باجه به‌طور کامل توسط lock در خطوط ۵–۲۷ کنترل می‌شود. خواننده‌ای که توکن را بازیابی می‌کند، در تمام مدت حضورش در باجه آن را نزد خود نگه می‌دارد
  • خطوط ۶–۸: خواننده‌ای که در خط ۵ توکن را دریافت کرده، در صورتی که اسکن مجاز نباشد یا نوبت او برای ادامه نباشد، آن را آزاد می‌کند.
  • خطوط ۱۰–۱۵: تعامل در پیشخوان (پردازش آرایه)
  • خطوط 17–18: نخ شرایط دسترسی برای صف خواندن را تغییر می‌دهد. توجه داشته باشید که هنوز توکن خواندن را در اختیار دارد و این تغییرات هنوز به خواننده‌ای اجازه ادامه کار را نمی‌دهند.
  • خطوط ۲۰–۲۳: نخ شرایط دسترسی به صف نوشتن را تغییر می‌دهد و به همه نویسندگان در حال انتظار اطلاع می‌دهد که تغییری رخ داده است.
  • خط ۲۷: lock خاتمه می‌یابد؛ توکن peutLire آزاد می‌شود. یک نخ خواندن سپس می‌تواند آن را به دست آورد (خط ۵)، اما شرط دسترسی را پاس نخواهد کرد زیرا متغیر بولی lectureAutorisée مقدار false دارد. علاوه بر این، تمام رشته‌هایی که منتظر توکن peutLire هستند، در حالت انتظار باقی می‌مانند زیرا عملیات PulseAll(peutLire) هنوز انجام نشده است.

روش Ecrire به شرح زیر است:


        public static void Ecrire() {
            //پیگیری
            Console.WriteLine("Méthode [Ecrire] démarrée par le thread n° {0}", Thread.CurrentThread.Name);
            // ما باید منتظر مجوز نوشتن بمانیم
            lock (peutEcrire) {
                while (!écritureAutorisée || ordreEcriture[écrivainSuivant] != Thread.CurrentThread.Name) {
                    Monitor.Wait(peutEcrire);
                }
                //نوشتن در آرایه
                for (int i = 0; i < data.Length; i++) {
                    //در انتظار ۱ ثانیه
                    Thread.Sleep(1000);
                    // نمایش
                    data[i] = objRandom.Next(0, 1000);
                    Console.WriteLine("{0:hh:mm:ss} : L'écrivain {1} a écrit le nombre {2}", DateTime.Now, Thread.CurrentThread.Name, data[i]);
                }
                // نویسنده بعدی
                écritureAutorisée = false;
                écrivainSuivant++;
                // بیدار کردن خوانندگانی که منتظر توکن peutLire هستند
                lock (peutLire) {
                    lectureAutorisée = true;
                    Monitor.PulseAll(peutLire);
                }
                //پیگیری
                Console.WriteLine("Méthode [Ecrire] terminée par le thread n° {0}", Thread.CurrentThread.Name);
            }
}
  • تمام دسترسی به شمارنده نوشتن توسط lock در خطوط 5–27 کنترل می‌شود. نویسنده‌ای که توکن را بازیابی می‌کند، در تمام مدت حضورش در کنار شمارنده آن را حفظ می‌کند
  • خطوط ۶–۸: نویسنده‌ای که در خط ۵ توکن را به‌دست آورده است، در صورتی که عملیات نوشتن مجاز نباشد یا نوبت او نباشد، توکن را آزاد می‌کند.
  • خطوط ۱۰–۱۶: تعامل با صف (پردازش آرایه)
  • خطوط ۱۸–۱۹: نخ شرایط دسترسی برای صف نوشتن را تغییر می‌دهد. توجه داشته باشید که این نخ هنوز توکن نوشتن را در اختیار دارد و این تغییرات هنوز به نویسنده اجازه ادامه کار را نمی‌دهند.
  • خطوط ۲۱–۲۴: نخ شرایط دسترسی به صف خواندن را تغییر می‌دهد و به همه خواننده‌های در حال انتظار اطلاع می‌دهد که تغییری رخ داده است.
  • خط ۲۷: lock خاتمه می‌یابد؛ توکن peutEcrire آزاد می‌شود. یک نخ نوشتن سپس می‌تواند آن را در خط ۵ به دست آورد، اما شرط دسترسی را پاس نخواهد کرد زیرا بولین écritureAutorisée نادرست است. علاوه بر این، تمام رشته‌هایی که منتظر توکن peutEcrire هستند، در حالت انتظار باقی می‌مانند و منتظر یک عملیات جدید PulseAll(peutEcrire) هستند.

یک مثال از اجرا به شرح زیر است:

Méthode [Lire] démarrée par le thread n° L0
Méthode [Lire] démarrée par le thread n° L2
Méthode [Lire] démarrée par le thread n° L1
Le lecteur L2 est en position 0
Le lecteur L1 est en position 1
Le lecteur L0 est en position 2
Méthode [Ecrire] démarrée par le thread n° E0
Méthode [Ecrire] démarrée par le thread n° E1
L'écrivain E0 est en position 0
L'écrivain E1 est en position 1
L'écrivain E2 est en position 2
Fin de Main...
Méthode [Ecrire] démarrée par le thread n° E2
12:09:05 : L'écrivain E0 a écrit le nombre 815
12:09:06 : L'écrivain E0 a écrit le nombre 990
12:09:07 : L'écrivain E0 a écrit le nombre 563
Méthode [Ecrire] terminée par le thread n° E0
12:09:08 : Le lecteur L2 a lu le nombre 815
12:09:09 : Le lecteur L2 a lu le nombre 990
12:09:10 : Le lecteur L2 a lu le nombre 563
Méthode [Lire] terminée par le thread n° L2
12:09:11 : L'écrivain E1 a écrit le nombre 411
12:09:12 : L'écrivain E1 a écrit le nombre 11
12:09:13 : L'écrivain E1 a écrit le nombre 54
Méthode [Ecrire] terminée par le thread n° E1
12:09:14 : Le lecteur L1 a lu le nombre 411
12:09:15 : Le lecteur L1 a lu le nombre 11
12:09:16 : Le lecteur L1 a lu le nombre 54
Méthode [Lire] terminée par le thread n° L1
12:09:17 : L'écrivain E2 a écrit le nombre 698
12:09:18 : L'écrivain E2 a écrit le nombre 448
12:09:19 : L'écrivain E2 a écrit le nombre 472
Méthode [Ecrire] terminée par le thread n° E2
12:09:20 : Le lecteur L0 a lu le nombre 698
12:09:21 : Le lecteur L0 a lu le nombre 448
12:09:22 : Le lecteur L0 a lu le nombre 472
Méthode [Lire] terminée par le thread n° L0

10.7. استخرهای نخ

تا کنون، برای مدیریت رشته‌ها:

  • آنها را با استفاده از Thread T = new Thread(...) ایجاد می‌کردیم
  • و سپس آن‌ها را با استفاده از T.Start() اجرا کردیم

در فصل «پایگاه‌های داده» دیدیم که با برخی SGBD می‌توانستیم استخرهای اتصالات باز داشته باشیم:

  • اتصالات n هنگام راه‌اندازی استخر باز می‌شوند
  • وقتی یک نخ درخواست اتصال می‌کند، یکی از اتصالات باز موجود در استخر به آن اختصاص داده می‌شود
  • وقتی تِرد اتصال را می‌بندد، اتصال بسته نمی‌شود بلکه به استخر بازگردانده می‌شود

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

سیستم مشابهی برای نخ‌ها وجود دارد:

  • رسانه‌های min هنگام راه‌اندازی استخر ایجاد می‌شوند. مقدار min با استفاده از متد ThreadPool.SetMinThreads(min1,min2) تنظیم می‌شود. می‌توان از یک استخر نخ برای اجرای وظایف مسدودکننده یا غیرمسدودکننده، که به عنوان وظایف ناهمزمان شناخته می‌شوند، استفاده کرد. پارامتر اول، min1، تعداد نخ‌های مسدودکننده را تعیین می‌کند، در حالی که پارامتر دوم، min2، تعداد نخ‌های ناهمزمان را تعیین می‌کند. مقادیر فعلی این دو پارامتر را می‌توان از طریق ThreadPool.GetMinThreads(out min1,out min2) به‌دست آورد.
  • اگر این تعداد ناکافی باشد، استخر برای رسیدگی به درخواست‌ها تا محدوده‌ای که توسط max تعیین شده است، رشته‌های بیشتری ایجاد خواهد کرد. مقدار max با استفاده از متد ThreadPool.SetMaxThreads(max1, max2) تنظیم می‌شود. این دو پارامتر همان معنایی را دارند که در متد SetMinThreads دارند. مقادیر فعلی این دو پارامتر را می‌توان با استفاده از ThreadPool.GetMaxThreads(out max1, out max2) به دست آورد. پس از رسیدن به تعداد رشته‌های max1، درخواست‌ها برای رشته‌های مربوط به وظایف مسدودکننده تا زمانی که یک رشته آزاد در استخر در دسترس شود، صف‌بندی خواهند شد.

استخر نخ‌ها مزایای مختلفی را ارائه می‌دهد:

  • مانند استخر اتصالات، در زمان ایجاد نخ صرفه‌جویی می‌کند: ۱۰ نخ می‌توانند صدها درخواست را مدیریت کنند.
  • امنیت برنامه را افزایش می‌دهد: با تعیین حداکثر تعداد رشته‌ها، برنامه در برابر غرق شدن در درخواست‌های زیاد محافظت می‌شود. این درخواست‌ها در صف قرار می‌گیرند.

برای اختصاص یک وظیفه به یک تِرد در استخر، از یکی از دو روش زیر استفاده می‌شود:

  1. ThreadPool.QueueWorkItem(WaitCallBack)
  2. ThreadPool.QueueWorkItem(WaitCallBack, object)

که در آن WaitCallBack هر متدی با امضای void WaitCallBack(object) است. متد ۱ به یک نخ دستور می‌دهد که متد WaitCallBack را بدون ارسال هیچ پارامتری اجرا کند. متد ۲ همین کار را انجام می‌دهد اما یک پارامتر از نوع object را به متد WaitCallBack ارسال می‌کند.

در اینجا برنامه‌ای برای تشریح این مفاهیم آورده شده است:


using System;
using System.Threading;

namespace Chap8 {
    class Program {
        public static void Main() {
            // ابتدایی‌سازی نخ جاری
            Thread main = Thread.CurrentThread;
            // یک نام به نخ اختصاص دهید
            main.Name = "Main";

            // استفاده از استخر نخ‌ها
            int min1, min2;
            // تنظیم حداقل تعداد رشته‌های مسدودکننده
            ThreadPool.GetMinThreads(out min1, out min2);
            Console.WriteLine("Nombre minimum de tâches bloquantes dans le pool : {0}", min1);
            Console.WriteLine("Nombre minimum de tâches asynchrones dans le pool : {0}", min2);
            ThreadPool.SetMinThreads(3, min2);
            ThreadPool.GetMinThreads(out min1, out min2);
            Console.WriteLine("Nombre minimum de tâches bloquantes dans le pool après changement : {0}", min1);
            // حداکثر تعداد رشته‌های مسدودکننده را تنظیم کنید
            int max1, max2;
            ThreadPool.GetMaxThreads(out max1, out max2);
            Console.WriteLine("Nombre maximum de tâches bloquantes dans le pool : {0}", max1);
            Console.WriteLine("Nombre maximum de tâches asynchrones dans le pool : {0}", max2);
            ThreadPool.SetMaxThreads(5, max2);
            ThreadPool.GetMaxThreads(out max1, out max2);
            Console.WriteLine("Nombre maximum de tâches bloquantes dans le pool après changement : {0}", max1);
            // اجرای ۷ نخ
            for (int i = 0; i < 7; i++) {
                // رشته i از یک استخر راه‌اندازی می‌شود
                ThreadPool.QueueUserWorkItem(Sleep, new Data2 { Numéro = i.ToString(), Début = DateTime.Now, Durée = i + 10 });
            }
            // پایان برنامه اصلی
            Console.Write("Tapez [entrée] pour terminer le thread {0} à {1:hh:mm:ss:FF}", main.Name, DateTime.Now);
            // انتظار
            Console.ReadLine();
        }

        public static void Sleep(object infos) {
            // در حال بازیابی پارامتر
            Data2 data = infos as Data2;
            Console.WriteLine("A {2:hh:mm:ss:FF}, le thread n° {0} va dormir pendant {1} seconde(s)", data.Numéro, data.Durée,DateTime.Now);
            //وضعیت پول
            int cpt1, cpt2;
            ThreadPool.GetAvailableThreads(out cpt1, out cpt2);
            Console.WriteLine("Nombre de threads pour tâches bloquantes disponibles dans le pool : {0}", cpt1);
            // خواب‌آلود کردن برای مدت Duration ثانیه
            Thread.Sleep(data.Durée * 1000);
            //پایان اجرا
            data.Fin = DateTime.Now;
            Console.WriteLine("A {3:hh:mm:ss:FF}, le thread n° {0} se termine. Il était programmé pour durer {1} seconde(s). Il a duré {2} seconde(s)", data.Numéro, data.Durée, data.Fin - data.Début,DateTime.Now);
        }
    }

    internal class Data2 {
        // اطلاعات متفرقه
        public string Numéro { get; set; }
        public DateTime Début { get; set; }
        public int Durée { get; set; }
        public DateTime Fin { get; set; }
    }
}
  • خطوط ۱۵–۱۷: حداقل تعداد فعلی هر دو نوع نخ در استخر نخ‌ها استعلام و نمایش داده می‌شود
  • خط ۱۸: حداقل تعداد رشته‌ها برای وظایف مسدودکننده به ۲ تغییر داده می‌شود
  • خطوط ۱۹–۲۱: مقادیر حداقل جدید نمایش داده می‌شوند
  • خطوط ۲۲–۲۸: ما همین کار را برای تنظیم حداکثر تعداد رشته‌ها برای وظایف مسدودکننده انجام می‌دهیم: ۵
  • خطوط ۳۰–۳۳: ۷ وظیفه در یک استخر ۵ رشته‌ای اجرا می‌شوند. باید به هر یک از ۵ وظیفه اول یک رشته اختصاص داده شود؛ دو وظیفه اول باید فوراً اختصاص داده شوند زیرا همیشه ۲ رشته در دسترس هستند، در حالی که ۳ وظیفه دیگر باید ۰.۵ ثانیه زمان انتظار داشته باشند. ۲ وظیفه باید منتظر بمانند تا یک رشته در دسترس شود.
  • خط ۳۲: وظایف در خطوط ۴۰–۵۴ متد Sleep را فراخوانی می‌کنند و پارامتری از نوع Data2 را که در خطوط ۵۶–۶۲ تعریف شده، به آن ارسال می‌کنند.
  • خط ۴۰: متد Sleep که توسط وظایف اجرا می‌شود
  • خط ۴۲: پارامتر ارسال‌شده به متد Sleep بازیابی می‌شود.
  • خط ۴۳: وظیفه خود را در کنسول شناسایی می‌کند
  • خطوط ۴۵–۴۷: تعداد رشته‌های در حال حاضر موجود نمایش داده می‌شود. ما می‌خواهیم ببینیم این عدد چگونه تغییر می‌کند.
  • خط ۴۹: وظیفه برای چند ثانیه متوقف می‌شود (وظیفه مسدودکننده).
  • خط ۵۲: وقتی از حالت تعلیق خارج می‌شود، برخی اطلاعات درباره حساب کاربری آن نمایش داده می‌شود.

نتایج به‌دست‌آمده به شرح زیر است.

برای شماره‌های min و max از رشته‌ها در استخر:

1
2
3
4
5
6
Nombre minimum de tâches bloquantes dans le pool : 2
Nombre minimum de tâches asynchrones dans le pool : 2
Nombre minimum de tâches bloquantes dans le pool après changement : 3
Nombre maximum de tâches bloquantes dans le pool : 500
Nombre maximum de tâches asynchrones dans le pool : 1000
Nombre maximum de tâches bloquantes dans le pool après changement : 5

برای اجرای ۷ نخ:

A 03:07:37:04, le thread n° 0 va dormir pendant 10 seconde(s)
Nombre de threads pour tâches bloquantes disponibles dans le pool : 3
A 03:07:37:04, le thread n° 2 va dormir pendant 12 seconde(s)
Nombre de threads pour tâches bloquantes disponibles dans le pool : 2
A 03:07:37:04, le thread n° 1 va dormir pendant 11 seconde(s)
Nombre de threads pour tâches bloquantes disponibles dans le pool : 2
A 03:07:38:04, le thread n° 3 va dormir pendant 13 seconde(s)
Nombre de threads pour tâches bloquantes disponibles dans le pool : 1
A 03:07:38:54, le thread n° 4 va dormir pendant 14 seconde(s)
Nombre de threads pour tâches bloquantes disponibles dans le pool : 0
A 03:07:47:04, le thread n° 0 se termine. Il était programmé pour durer 10 seconde(s). Il a duré 00:00:10 seconde(s)
A 03:07:47:04, le thread n° 5 va dormir pendant 15 seconde(s)
Nombre de threads pour tâches bloquantes disponibles dans le pool : 0
A 03:07:48:04, le thread n° 1 se termine. Il était programmé pour durer 11 seconde(s). Il a duré 00:00:11 seconde(s)
A 03:07:48:04, le thread n° 6 va dormir pendant 16 seconde(s)
Nombre de threads pour tâches bloquantes disponibles dans le pool : 0
A 03:07:49:04, le thread n° 2 se termine. Il était programmé pour durer 12 seconde(s). Il a duré 00:00:12 seconde(s)
A 03:07:51:04, le thread n° 3 se termine. Il était programmé pour durer 13 seconde(s). Il a duré 00:00:14 seconde(s)
A 03:07:52:54, le thread n° 4 se termine. Il était programmé pour durer 14 seconde(s). Il a duré 00:00:15.5000000 seconde(s)
A 03:08:02:04, le thread n° 5 se termine. Il était programmé pour durer 15 seconde(s). Il a duré 00:00:25 seconde(s)
A 03:08:04:04, le thread n° 6 se termine. Il était programmé pour durer 16 seconde(s). Il a duré 00:00:27 seconde(s)
  • خطوط ۱–۶: سه وظیفه اول به ترتیب اجرا می‌شوند. آنها بلافاصله یک نخ آزاد (MinThreads=3) پیدا می‌کنند و سپس وارد حالت معلق می‌شوند.
  • خطوط ۷–۹: برای وظایف ۳ و ۴، کمی بیشتر طول می‌کشد. برای هر یک از آنها، هیچ نخ آزاد وجود نداشت. لازم بود یک نخ جدید ایجاد شود. این مکانیزم تا ۵ نخ (MaxThreads=5) امکان‌پذیر است.
  • خط ۱۰: دیگر هیچ رشته‌ای در دسترس نیست: وظایف ۵ و ۶ باید منتظر بمانند.
  • خطوط ۱۱–۱۲: وظیفه ۰ پایان می‌یابد. وظیفه ۵ نخ آن را برمی‌دارد.
  • خطوط ۱۳–۱۴: وظیفه ۱ پایان می‌یابد. وظیفه ۶ نخ آن را تصاحب می‌کند.
  • خطوط 17–21: وظایف یکی پس از دیگری به پایان می‌رسند.

10.8. کلاس BackgroundWorker

10.8.1. مثال 1

کلاس BackgroundWorker به فضای نام [System.ComponentModel] تعلق دارد. این کلاس مانند یک نخ استفاده می‌شود اما ویژگی‌های خاصی دارد که در برخی موارد ممکن است آن را نسبت به کلاس [Thread] مفیدتر سازد:

  • رویدادهای زیر را صادر می‌کند:
  • DoWork: یک نخ اجرای BackgroundWorker را درخواست کرد
  • ProgressChanged: شیء BackgroundWorker متد ReportProgress را اجرا کرد. این متد برای ارائه درصد اجرای عملیات استفاده می‌شود.
  • RunWorkerCompleted: شیء BackgroundWorker وظیفه خود را به پایان رسانده است. ممکن است این وظیفه به صورت عادی، یا به دلیل لغو یا استثنا به پایان رسیده باشد.

این رویدادها، BackgroundWorker را در رابط‌های کاربری گرافیکی مفید می‌سازند: یک وظیفه زمان‌بر به BackgroundWorker محول خواهد شد، که می‌تواند پیشرفت خود را از طریق رویداد ProgressChanged و اتمام آن را از طریق رویداد RunWorkerCompleted گزارش دهد. کاری که باید توسط BackgroundWorker انجام شود، توسط متدی که با رویداد DoWork مرتبط است، اجرا خواهد شد.

  • امکان درخواست لغو آن وجود دارد. در یک رابط کاربری گرافیکی، کاربر می‌تواند یک وظیفه زمان‌بر را بدین ترتیب لغو کند.
  • ابژه‌های BackgroundWorker به یک استخر تعلق دارند و در صورت نیاز بازیافت می‌شوند. یک برنامه کاربردی که به یک شیء BackgroundWorker نیاز دارد، آن را از استخر دریافت می‌کند که یک نخ موجود اما بلااستفاده را فراهم می‌کند. بازیافت نخ‌ها به این روش، به جای ایجاد یک نخ جدید در هر بار، عملکرد را بهبود می‌بخشد.

ما از این ابزار در برنامه قبلی که دسترسی به شمارنده در آن کنترل‌نشده است، استفاده می‌کنیم:


using System;
using System.Threading;
using System.ComponentModel;

namespace Chap8 {
    class Program2 {
        // استفاده از نخ‌های خواندن و نوشتن
        // استفاده همزمان از منابع مشترک و همگام‌سازی را نشان می‌دهد

        // متغیرهای کلاس
        const int nbThreads = 2;                    // تعداد کل نخ‌ها
        static int nbLecteursTerminés = 0;        // تعداد نخ‌های تکمیل‌شده
        static int[] data = new int[5];            // آرایه‌ای که بین رشته‌های خواندن و نوشتن مشترک است
        static object appli;                            // دسترسی به تعداد رشته‌های تکمیل‌شده را همگام‌سازی می‌کند
        static Random objRandom = new Random(DateTime.Now.Second);    // یک تولیدکننده عدد تصادفی
        static AutoResetEvent peutLire;        // نشان می‌دهد که محتویات آرایه قابل خواندن هستند
        static AutoResetEvent peutEcrire;        //نشان می‌دهد که آرایه قابل نوشتن است
        static AutoResetEvent finLecteurs;    //پایان خواننده‌ها را نشان می‌دهد

        //اصلی
        public static void Main(string[] args) {

            // به نخ یک نام داده می‌شود
            Thread.CurrentThread.Name = "Main";

            // ابتدایی‌سازی پرچم‌ها
            peutLire = new AutoResetEvent(false);        //هنوز قابل خواندن نیست
            peutEcrire = new AutoResetEvent(true);    //نوشتن قبلاً ممکن است
            finLecteurs = new AutoResetEvent(false);    // برنامه کامل نشده است

            // همگام‌سازی دسترسی به شمارشگر رشته‌های خاتمه‌یافته
            appli = new object();                

            // ایجاد نخ‌های خواننده
            MyBackgroundWorker[] lecteurs = new MyBackgroundWorker[nbThreads];
            for (int i = 0; i < nbThreads; i++) {
                // ایجاد
                lecteurs[i] = new MyBackgroundWorker();
                lecteurs[i].Numéro = "L" + i;
                lecteurs[i].DoWork += Lire;
                lecteurs[i].RunWorkerCompleted += EndLecteur;
                // راه‌اندازی
                lecteurs[i].RunWorkerAsync();
            }

            //ایجاد نخ‌های نویسنده
            MyBackgroundWorker[] écrivains = new MyBackgroundWorker[nbThreads];
            for (int i = 0; i < nbThreads; i++) {
                // ایجاد
                écrivains[i] = new MyBackgroundWorker();
                écrivains[i].Numéro = "E" + i;
                écrivains[i].DoWork += Ecrire;
                // راه‌اندازی
                écrivains[i].RunWorkerAsync();
            }

            // انتظار برای پایان تمام نخ‌ها
            finLecteurs.WaitOne();
            //پایان دست
            Console.WriteLine("Fin de Main...");
        }

        public static void EndLecteur(object sender, RunWorkerCompletedEventArgs infos) {
...
        }

        // خواندن محتویات آرایه
        public static void Lire(object sender, DoWorkEventArgs infos) {
...
        }

        // نوشتن در آرایه
        public static void Ecrire(object sender, DoWorkEventArgs infos) {
...
        }
    }

    //موضوع بحث
    internal class MyBackgroundWorker : BackgroundWorker {
        //اطلاعات متفرقه
        public string Numéro { get; set; }
    }

}

ما فقط تغییرات را توضیح می‌دهیم:

  • کلاس Thread در خطوط ۷۹–۸۲ با کلاس MyBackgroundWorker جایگزین شده است. کلاس BackgroundWorker مشتق شده است تا به نخ یک شماره اختصاص دهد. می‌توانستیم رویکرد متفاوتی در پیش بگیریم و یک شیء حاوی شماره نخ را در خطوط ۴۳ و ۵۴ به متد RunWorkerAsync پاس کنیم.
  • خط ۵۸: متد Main به محض اینکه تمام رشته‌های خواننده کار خود را به پایان رساندند، خاتمه می‌یابد. برای این کار، در خط ۱۲، شمارنده nbLecteursTerminés تعداد رشته‌های خواننده را که کارشان را به پایان رسانده‌اند، می‌شمارد. این شمارنده توسط متد EndLecteur در خطوط ۶۳–۶۵ افزایش می‌یابد، که هر بار یک رشته خواننده خاتمه می‌یابد، اجرا می‌شود. این رویه است که رویداد AutoResetEvent finLecteurs را در خط ۱۸ کنترل می‌کند، رویدادی که متد Main. در خط ۵۹ روی آن همگام‌سازی می‌کند
  • خط ۱۶: از آنجا که ممکن است چندین نخ خواننده بخواهند همزمان شمارنده nbLecteursTerminés را افزایش دهند، دسترسی انحصاری به آن توسط شیء همگام‌سازی appli تضمین می‌شود. این سناریو بعید است اما از نظر تئوری امکان‌پذیر است.
  • خطوط ۳۵–۴۴: ایجاد نخ‌های خواننده
  • خط ۳۸: ایجاد نخ از نوع MyBackgroundWorker
  • خط ۳۹: یک عدد به آن اختصاص داده می‌شود
  • خط ۴۰: متد Lire برای اجرا به آن اختصاص داده می‌شود
  • خط ۴۱: متد EndLecteur پس از پایان نخ اجرا خواهد شد
  • خط ۴۳: نخ آغاز می‌شود
  • خطوط ۴۷–۵۵: ایجاد نخ‌های نویسنده
  • خط ۵۰: ایجاد نخ از نوع MyBackgroundWorker
  • خط ۵۱: یک عدد به آن اختصاص داده می‌شود
  • خط ۵۲: متد Ecrire برای اجرا به آن اختصاص داده می‌شود
  • خط ۵۴: نخ اجرا می‌شود

متدهای Lire و Ecrire بدون تغییر باقی می‌مانند. متد EndLecteur در پایان هر نخ خواننده اجرا می‌شود. کد آن به شرح زیر است:


        public static void EndLecteur(object sender, RunWorkerCompletedEventArgs infos) {
            //افزایش در تعداد خوانندگان تکمیل‌شده
            lock (appli) {
                nbLecteursTerminés++;
                if (nbLecteursTerminés == nbThreads)
                    finLecteurs.Set();
            }
}

نقش متد EndLecteur این است که به متد Main اطلاع دهد که تمام خواننده‌ها کار خود را به پایان رسانده‌اند.

  • خط ۴: شمارنده nbLecteursTerminés افزایش می‌یابد.
  • خطوط ۵–۶: اگر همه خوانندگان کار خود را تکمیل کرده باشند، رویداد finLecteurs به مقدار true تنظیم می‌شود تا متد Main که در انتظار این رویداد است، مطلع شود.
  • از آنجا که رویه EndLecteur توسط چندین نخ اجرا می‌شود، بخش بحرانی قبلی توسط عبارت lock در خط ۳ محافظت می‌شود.

اجرا نتایجی مشابه نسخهٔ استفاده از رشته‌ها تولید می‌کند.

10.8.2. مثال ۲

کد زیر ویژگی‌های دیگر کلاس BackgroundWorker را نشان می‌دهد:

  • توانایی لغو وظیفه
  • انتشار استثنایی که در داخل وظیفه پرتاب می‌شود
  • ارسال یک پارامتر ورودی/خروجی به وظیفه

using System;
using System.Threading;
using System.ComponentModel;

namespace Chap8 {
    class Program3 {

        // رشته‌ها
        static BackgroundWorker[] tâches = new BackgroundWorker[5];

        public static void Main() {
            // ابتدایی‌سازی نخ جاری
            Thread main = Thread.CurrentThread;
            // اعطای نام به نخ
            main.Name = "Main";

            // ایجاد رشته‌ها
            for (int i = 0; i < tâches.Length; i++) {
                // ایجاد نخ شماره i
                tâches[i] = new BackgroundWorker();
                // ابتدایی‌سازی آن
                tâches[i].DoWork += Sleep;
                tâches[i].RunWorkerCompleted += End;
                tâches[i].WorkerSupportsCancellation = true;
                // راه‌اندازی آن
                tâches[i].RunWorkerAsync(new Data { Numéro = i, Début = DateTime.Now, Durée = i + 1 });
            }
            //لغو آخرین نخ
            tâches[4].CancelAsync();

            //پایان روال
            Console.WriteLine("Fin du thread {0}, tapez [entrée] pour terminer...", main.Name);
            Console.ReadLine();
            return;
        }

        public static void Sleep(object sender, DoWorkEventArgs infos) {
...
        }

        public static void End(object sender, RunWorkerCompletedEventArgs infos) {
...
        }

        internal class Data {
            // اطلاعات متفرقه
            public int Numéro { get; set; }
            public DateTime Début { get; set; }
            public int Durée { get; set; }
            public DateTime Fin { get; set; }
        }
    }
}
  • خط ۹: آرایه برای BackgroundWorker
  • خطوط ۱۸–۲۷: ایجاد رشته‌ها
  • خط ۲۰: ایجاد نخ
  • خط ۲۲: نخ متد Sleep را از خطوط ۳۹–۴۱ اجرا خواهد کرد
  • خط ۲۳: متد End در خطوط ۴۳–۴۵ در پایان نخ اجرا خواهد شد
  • خط ۲۴: ممکن است نخ لغو شود
  • خط ۲۶: نخ با پارامتری از نوع [Data] که در خطوط ۴۹–۵۲ تعریف شده است، راه‌اندازی می‌شود. این شیء دارای فیلدهای زیر است:
    • Numéro (ورودی): شمارهٔ نخ
    • Début (ورودی): زمان شروع اجرای نخ
    • Durée (ورودی): مدت زمان اجرای Sleep
    • Fin (خروجی): پایان اجرای نخ
  • خط ۲۹: رشته شماره ۴ لغو شد

تمام رشته‌ها متد زیر را اجرا می‌کنند، Sleep:


        public static void Sleep(object sender, DoWorkEventArgs infos) {
            //پارامتر 'infos' پردازش می‌شود
            Data data = (Data)infos.Argument;
            // استثناء برای وظیفه شماره ۳
            if (data.Numéro == 3) {
                throw new Exception("test....");
            }
            // متوقف برای مدت ثانیه با مکث هر ثانیه
            for (int i = 1; i <= data.Durée && !tâches[data.Numéro].CancellationPending; i++) {
                // انتظار ۱ ثانیه‌ای
                Thread.Sleep(1000);
            }
            //پایان اجرا
            data.Fin = DateTime.Now;
            // نتایج را مقداردهی اولیه می‌کند
            infos.Result = data;
            infos.Cancel = tâches[data.Numéro].CancellationPending;
}
  • خط ۱: متد Sleep دارای امضای استاندارد برای رسیدگی‌کننده‌های رویداد است. این متد دو پارامتر می‌گیرد:
    • sender: صادرکننده رویداد، در این مورد BackgroundWorker، که متد را اجرا می‌کند
    • infos: از نوع DoWorkEventArgs، که اطلاعاتی در مورد رویداد DoWork ارائه می‌دهد. این پارامتر هم برای ارسال اطلاعات به نخ (thread) و هم برای دریافت نتایج آن استفاده می‌شود.
  • خط ۳: پارامتر ارسال‌شده به متد RunWorkerAsync وظیفه از طریق خاصیت infos.Argument بازیابی می‌شود.
  • خطوط ۵–۷: یک استثنا برای وظیفه شماره ۳ پرتاب می‌شود
  • خطوط ۹–۱۲: نخ برای Durée ثانیه در بازه‌های یک ثانیه‌ای «خواب» می‌کند تا تست لغو در خط ۹ اجرا شود. این شبیه‌سازی یک وظیفه طولانی‌مدت است که در طول آن نخ به‌طور منظم درخواست لغو را بررسی می‌کند. برای نشان دادن اینکه لغو شده است، نخ باید خاصیت infos.Cancel را به true تنظیم کند (خط 17).
  • خط ۱۶: نخ می‌تواند نتیجه‌ای را به نخی که آن را راه‌اندازی کرده است بازگرداند. این نتیجه را در infos.Result قرار می‌دهد.

پس از اتمام، نخ‌ها متد زیر را اجرا می‌کنند: End:


public static void End(object sender, RunWorkerCompletedEventArgs infos) {
            //پارامتر 'infos' برای نمایش نتیجهٔ اجرا استفاده می‌شود
            // استثناء؟
            if (infos.Error != null) {
                Console.WriteLine("Le thread {1} a rencontré l'erreur suivante : {0}", infos.Error.Message, sender);
            } else
                if (!infos.Cancelled) {
                    Data data = (Data)infos.Result;
                    Console.WriteLine("Thread {0} terminé : début {1:hh:mm:ss}, durée programmée {2} s, fin {3:hh:mm:ss}, durée effective {4}",
                    data.Numéro, data.Début, data.Durée, data.Fin, (data.Fin - data.Début));
                } else {
                    Console.WriteLine("Thread {0} annulé", sender);
                }
        }
  • خط ۱: متد End دارای امضای استاندارد برای دستگیرکننده‌های رویداد است. این متد دو پارامتر می‌گیرد:
    • sender: صادرکننده رویداد، در این مورد BackgroundWorker که متد را اجرا می‌کند
    • infos: از نوع RunWorkerCompletedEventArgs، که اطلاعاتی درباره رویداد RunWorkerCompleted ارائه می‌دهد.
  • خط ۴: فیلد infos.Error، از نوع Exception، تنها در صورتی پر می‌شود که یک استثنا رخ داده باشد.
  • خط ۷: فیلد infos.Cancelled از نوع بولین، در صورتی که نخ لغو شده باشد، مقدار true را دارد.
  • خط ۸: اگر هیچ استثنا یا لغوشی رخ نداده باشد، آنگاه infos.Result نتیجهٔ نخ اجراشده است. استفاده از این نتیجه در صورتی که نخ لغو شده باشد یا استثنا پرتاب کرده باشد، منجر به خطا می‌شود. بنابراین، در خطوط ۵ و ۱۳، قادر به نمایش شماره نخی که لغو شده یا استثنا پرتاب کرده نیستیم، زیرا این شماره در infos.Result قرار دارد. این مشکل را می‌توان با مشتق کردن کلاس BackgroundWorker برای ذخیره اطلاعاتی که باید بین تارک فراخوانی‌کننده و تارک فراخوانی‌شونده مبادله شود، دور زد، همانطور که در مثال قبلی انجام شد. سپس به جای آرگومان infos، از آرگومان sender که نماینده BackgroundWorker است، استفاده می‌کنیم.

نتایج اجرا به شرح زیر است:

1
2
3
4
5
6
Fin du thread Main. Laissez les autres threads se terminer puis tapez [entrée] pour terminer...
Thread 0 terminé : début 05:19:46, durée programmée 1 s, fin 05:19:47, durée effective 00:00:01
Le thread System.ComponentModel.BackgroundWorker a rencontré l'erreur suivante : test....
Thread System.ComponentModel.BackgroundWorker annulé
Thread 1 terminé : début 05:19:46, durée programmée 2 s, fin 05:19:49, durée effective 00:00:03
Thread 2 terminé : début 05:19:46, durée programmée 3 s, fin 05:19:50, durée effective 00:00:04

10.9. داده‌های محلی نخ

10.9.1. اصل

بیایید یک برنامهٔ سه‌لایه را در نظر بگیریم:

فرض کنید برنامه چندکاربره است، مانند یک برنامه وب. هر کاربر توسط یک نخ اختصاصی سرویس‌دهی می‌شود. چرخه عمر نخ به شرح زیر است:

  1. رشته از یک استخر رشته‌ها برای انجام درخواست کاربر ایجاد یا درخواست می‌شود
  2. اگر این درخواست به داده‌ای نیاز داشته باشد، تِرد یک متد را در لایه [ui] اجرا می‌کند که آن متد، متدی را در لایه [metier] فراخوانی می‌کند، و آن متد نیز به نوبه خود متدی را در لایه [dao] فراخوانی می‌کند.
  3. رشته پاسخ را به کاربر بازمی‌گرداند. سپس یا از بین می‌رود یا به استخر رشته‌ها بازمی‌گردد.

در عملیات ۲، ممکن است مفید باشد که نخ داده‌های خاص خود، c.a.d، را داشته باشد که با نخ‌های دیگر مشترک نیست. این داده‌ها می‌توانند، برای مثال، متعلق به کاربر خاصی باشند که نخ در حال خدمت‌رسانی به او است. سپس این داده‌ها می‌توانند در لایه‌های مختلف، [ui, metier, dao]، استفاده شوند.

کلاس Thread این سناریو را از طریق نوعی فرهنگ لغت خصوصی که کلیدهای آن از نوع LocalDataStoreSlot هستند، امکان‌پذیر می‌سازد:

یک مدخل در فرهنگ لغت خصوصی نخ برای کلید name ایجاد می‌کند.
مقدار data را با کلید name در فرهنگ لغت خصوصی رشته مرتبط می‌سازد
مقدار مرتبط با کلید name را از فرهنگ لغت خصوصی نخ بازیابی می‌کند

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

  • برای ایجاد یک جفت (clé,valeur) مرتبط با نخ فعلی:
Thread.SetData(Thread.GetNamedDataSlot("clé"),valeur);
  • برای بازیابی مقدار مرتبط با clé:
Thread.GetData(Thread.GetNamedDataSlot("clé"));

10.9.2. کاربرد اصل

اپلیکیشن سه‌لایه زیر را در نظر بگیرید:

فرض کنیم که لایه [dao] یک مستر مواد را مدیریت می‌کند و رابط کاربری آن در ابتدا به شرح زیر است:


using System.Collections.Generic;

namespace Chap8 {
    public interface IDao {
        int InsertArticle(Article article);
        List<Article> GetAllArticles();
        void DeleteAllArticles();
    }
}
  • خط ۵: برای درج یک آیتم در پایگاه داده
  • خط ۶: برای بازیابی همه آیتم‌ها از پایگاه داده
  • خط ۷: حذف همه آیتم‌ها از پایگاه داده

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


using System.Collections.Generic;

namespace Chap8 {
    public interface IDao {
        int InsertArticle(Article article);
        void insertArticles(Article[] articles);
        List<Article> GetAllArticles();
        void DeleteAllArticles();
    }
}
  • خط ۶: افزودن یک آرایه از آیتم‌ها به پایگاه داده

بعداً، برای یک برنامه کاربردی دیگر، نیاز به حذف یک لیست از آیتم‌ها که در یک لیست ذخیره شده است، مجدداً در داخل یک تراکنش، پیش می‌آید. واضح می‌شود که برای برآورده کردن نیازهای تجاری مختلف، لایه [dao] باید گسترش یابد. ما می‌توانیم رویکرد متفاوتی در پیش بگیریم:

  • تا در لایه [dao] تنها عملیات پایه‌ای InsertArticle، DeleteArticle، UpdateArticle، SelectArticle, SelectArticles
  • برای انتقال عملیات به‌روزرسانی همزمان چندین آیتم به لایه [métier]. این عملیات از عملیات پایه‌ای لایه [dao] استفاده می‌کنند.

مزیت این راه‌حل این است که می‌توان از همان لایه [dao] بدون تغییر با لایه‌های مختلف [metier] استفاده کرد. با این حال، این موضوع چالشی در مدیریت تراکنش که به‌روزرسانی‌ها را به‌صورت اتمیک در پایگاه‌داده گروه‌بندی می‌کند، ایجاد می‌کند:

  • مبادله باید توسط لایه [metier] آغاز شود قبل از اینکه متدهای لایه [dao] را فراخوانی کند
  • متدهای لایه [dao] باید از وجود تراکنش مطلع باشند تا در صورت وجود، در آن شرکت کنند
  • معامله باید توسط لایه [métier] خاتمه یابد.

برای اطمینان از اینکه متدهای لایه [dao] از هر تراکنش در حال اجرا مطلع باشند، می‌توان تراکنش را به‌عنوان پارامتر به هر متد در لایه [dao] اضافه کرد. این پارامتر سپس در امضاهای متدهای اینفراساخت ظاهر می‌شود و بدین ترتیب اینفراساخت را به یک منبع دادهٔ خاص، یعنی پایگاه داده، متصل می‌سازد. داده‌های محلی نخ راه‌حل ظریف‌تری ارائه می‌دهد: لایه [métier] تراکنش را در داده‌های محلی نخ ذخیره می‌کند و لایه [dao] آن را از آنجا بازیابی خواهد کرد. بنابراین، امضاهای متد لایه [dao] نیازی به تغییر ندارند.

ما این راه‌حل را با استفاده از پروژهٔ زیر در ویژوال استودیو پیاده‌سازی می‌کنیم:

  • در [1]: راه‌حل به‌طور کلی
  • در [2]: مراجع مورد استفاده. از آنجایی که پایگاه داده [4] یک پایگاه داده SQL Server Compact است، به مرجع [System.Data.SqlServerCe] نیاز است.
  • در [3]: لایه‌های مختلف برنامه.

پایگاه داده [4] همان پایگاه داده SQL Server Compact است که قبلاً در فصل قبلی، به‌ویژه در بخش 9.3.1، استفاده شده است.

 

کلاس Article

یک ردیف از جدول قبلی [articles] در یک شی از نوع Article محصور شده است:


namespace Chap8 {
    public class Article {
        // ویژگی‌ها
        public int Id { get; set; }
        public string Nom { get; set; }
        public decimal Prix { get; set; }
        public int StockActuel { get; set; }
        public int StockMinimum { get; set; }

        // سازنده‌ها
        public Article() { 
        }

        public Article(int id, string nom, decimal prix, int stockActuel, int stockMinimum) {
            Id = id;
            Nom = nom;
            Prix = prix;
            StockActuel = stockActuel;
            StockMinimum = stockMinimum;
        }

        // هویت
        public override string ToString() {
            return string.Format("[{0},{1},{2},{3},{4}]", Id, Nom, Prix, StockActuel, StockMinimum);
        }
    }
}

رابط لایه [dao]

رابط IDao لایه [dao] به شرح زیر خواهد بود:


using System.Collections.Generic;

namespace Chap8 {
    public interface IDao {
        int InsertArticle(Article article);
        List<Article> GetAllArticles();
        void DeleteAllArticles();
    }
}
  • خط ۵: برای درج یک رکورد در جدول [articles]
  • خط ۶: برای قرار دادن تمام سطرها از جدول [articles] در یک لیست اشیاء Article
  • خط ۷: برای حذف تمام سطرهای جدول [articles]

رابط لایه [metier]

رابط IMetier برای لایه [metier] به شرح زیر خواهد بود:


using System.Collections.Generic;

namespace Chap8 {
    interface IMetier {
        void InsertArticlesInTransaction(Article[] articles);
        void InsertArticlesOutOfTransaction(Article[] articles);
        List<Article> GetAllArticles();
        void DeleteAllArticles();
    }
}
  • خط ۵: برای درج مجموعه‌ای از آیتم‌ها در یک تراکنش
  • خط ۶: مشابه مورد بالا، اما بدون تراکنش
  • خط ۷: بازیابی فهرست تمام آیتم‌ها
  • خط ۸: برای حذف همه آیتم‌ها

پیاده‌سازی لایه [metier]

پیاده‌سازی تجاری رابط IMetier به شرح زیر خواهد بود:


using System.Collections.Generic;
using System.Data;
using System.Data.SqlServerCe;
using System.Threading;

namespace Chap8 {
    public class Metier : IMetier {
        //لایه [dao]
        public IDao Dao { get; set; }
        // رشته اتصال
        public string ConnectionString { get; set; }

        // وارد کردن یک جدول از اقلام در داخل یک تراکنش
        public void InsertArticlesInTransaction(Article[] articles) {
            // ایجاد اتصال به پایگاه داده
            using (SqlCeConnection connexion = new SqlCeConnection(ConnectionString)) {
                // باز کردن اتصال
                connexion.Open();
                // معامله
                SqlCeTransaction transaction = null;
                try {
                    // شروع تراکنش
                    transaction = connexion.BeginTransaction(IsolationLevel.ReadCommitted);
                    // معامله در نخ ثبت می‌شود
                    Thread.SetData(Thread.GetNamedDataSlot("transaction"), transaction);
                    // افزودن آیتم‌ها
                    foreach (Article article in articles) {
                        Dao.InsertArticle(article);
                    }
                    // تثبیت تراکنش
                    transaction.Commit();
                } catch {
                    // برگشت تراکنش
                    if (transaction != null)
                        transaction.Rollback();
                }
            }
        }

        // افزودن یک جدول از اقلام بدون تراکنش
        public void InsertArticlesOutOfTransaction(Article[] articles) {
            // افزودن اقلام
            foreach (Article article in articles) {
                Dao.InsertArticle(article);
            }
        }

        // فهرست اقلام
        public List<Article> GetAllArticles() {
            return Dao.GetAllArticles();
        }
        // حذف همه اقلام
        public void DeleteAllArticles() {
            Dao.DeleteAllArticles();
        }
    }
}

کلاس دارای ویژگی‌های زیر است:

  • خط ۹: مرجعی به لایه [dao]
  • خط ۱۱: رشته اتصال مورد استفاده برای اتصال به پایگاه داده محصول

ما فقط در مورد متد InsertArticlesInTransaction توضیح می‌دهیم، زیرا این تنها متدی است که مشکلاتی را ایجاد می‌کند:

  • خط ۱۶: یک اتصال به پایگاه داده برقرار می‌شود
  • خط ۱۸: اتصال باز می‌شود
  • خط ۲۳: یک تراکنش ایجاد می‌شود
  • خط ۲۵: در داده‌های محلی نخ ذخیره می‌شود، مرتبط با کلید «transaction»
  • خطوط ۲۷–۲۹: متد درج یک‌عنصری لایه [dao] برای هر آیتم ورودی فراخوانی می‌شود
  • خطوط ۲۱ و ۳۲: کل عملیات درج آرایه توسط یک بلوک try/catch کنترل می‌شود
  • خط ۳۱: اگر به این نقطه برسیم، یعنی هیچ استثنایی رخ نداده است. سپس تراکنش را commit می‌کنیم.
  • خطوط ۳۴–۳۵: یک استثنا رخ داده است؛ تراکنش لغو می‌شود
  • خط ۳۷: از عبارت using خارج می‌شویم. اتصال بازشده در خط ۱۸ به‌طور خودکار بسته می‌شود.

پیاده‌سازی لایه [dao]

پیاده‌سازی DAO برای رابط IDao به شرح زیر خواهد بود:


using System.Collections.Generic;
using System.Data;
using System.Data.SqlServerCe;
using System.Threading;

namespace Chap8 {
    public class Dao : IDao {
        // رشته اتصال
        public string ConnectionString { get; set; }
        // پرس‌وجوها
        public string InsertText { get; set; }
        public string DeleteAllText { get; set; }
        public string GetAllText { get; set; }

        //پیاده‌سازی رابط

        // افزودن آیتم
        public int InsertArticle(Article article) {
            //آیا تراکنشی در حال انجام است؟
            SqlCeTransaction transaction = Thread.GetData(Thread.GetNamedDataSlot("transaction")) as SqlCeTransaction;
            //بازیابی یا ایجاد اتصال
            SqlCeConnection connexion = null;
            if (transaction != null) {
                //بازیابی اتصال
                connexion = transaction.Connection as SqlCeConnection;
            } else {
                // ایجاد آن
                connexion = new SqlCeConnection(ConnectionString);
                connexion.Open();
            }
            try {
                // آماده‌سازی فرمان درج
                SqlCeCommand sqlCommand = new SqlCeCommand();
                sqlCommand.Transaction = transaction;
                sqlCommand.Connection = connexion;
                sqlCommand.CommandText = InsertText;
                sqlCommand.Parameters.Add("@nom", SqlDbType.NVarChar, 30);
                sqlCommand.Parameters.Add("@prix", SqlDbType.Money);
                sqlCommand.Parameters.Add("@sa", SqlDbType.Int);
                sqlCommand.Parameters.Add("@sm", SqlDbType.Int);
                sqlCommand.Parameters["@nom"].Value = article.Nom;
                sqlCommand.Parameters["@prix"].Value = article.Prix;
                sqlCommand.Parameters["@sa"].Value = article.StockActuel;
                sqlCommand.Parameters["@sm"].Value = article.StockMinimum;
                // اجرا
                return sqlCommand.ExecuteNonQuery();
            } finally {
                //اگر در یک تراکنش نیست، اتصال را ببند
                if (transaction == null) {
                    connexion.Close();
                }
            }
        }

        // فهرست اقلام
        public List<Article> GetAllArticles() {
...
        }

        // حذف اقلام
        public void DeleteAllArticles() {
...
        }
    }
}

کلاس دارای ویژگی‌های زیر است:

  • خط ۹: رشته اتصال مورد استفاده برای اتصال به پایگاه داده محصول
  • خط ۱۱: دستور SQL برای درج یک آیتم
  • خط ۱۲: دستور SQL برای حذف همه آیتم‌ها
  • خط ۱۳: دستور SQL برای بازیابی تمام آیتم‌ها

این ویژگی‌ها از فایل پیکربندی زیر، [App.config]، مقداردهی اولیه می‌شوند:


<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <connectionStrings>
        <add name="dbArticlesSqlServerCe" connectionString="Data Source=|DataDirectory|\dbarticles.sdf;Password=dbarticles;" />
    </connectionStrings>
    <appSettings>
        <add key="insertText" value="insert into articles(nom,prix,stockactuel,stockminimum) values(@nom,@prix,@sa,@sm)"/>
        <add key="getAllText" value="select id,nom,prix,stockactuel,stockminimum from articles"/>
        <add key="deleteAllText" value="delete from articles"/>
    </appSettings>
</configuration>

ما متد InsertArticle را غیرفعال می‌کنیم:

  • خط ۲۰: هر تراکنشی را که لایه [metier] ممکن است در نخ قرار داده باشد، بازیابی می‌کنیم
  • خطوط ۲۳–۲۵: اگر یک تراکنش موجود باشد، اتصال مرتبط با آن را بازیابی می‌کنیم.
  • خطوط 26–30: در غیر این صورت، یک اتصال جدید ایجاد و باز می‌شود.
  • خطوط ۳۳–۴۴: دستور درج آماده می‌شود. این پیکربندی شده است (به خط g از App.config مراجعه کنید).
  • خط ۳۳: شیء Command ایجاد می‌شود.
  • خط ۳۴: این با تراکنش جاری مرتبط است. اگر تراکنش وجود نداشته باشد (transaction=null)، این معادل اجرای دستور SQL بدون تراکنش صریح است. باید توجه داشت که در این حالت همچنان یک تراکنش ضمنی وجود دارد. با سرور SQL CE، این تراکنش ضمنی به‌طور پیش‌فرض در حالت autocommit قرار دارد: دستور SQL پس از اجرا به committé تبدیل می‌شود.
  • خط ۳۵: شیء Command با اتصال جاری مرتبط است
  • خط ۳۶: متن SQl که باید اجرا شود، تعیین می‌شود. این پرس‌وجوی پارامتریک از خط g در App.config است.
  • خطوط ۳۷–۴۴: چهار پارامتر پرس‌وجو مقداردهی اولیه می‌شوند
  • خط ۴۶: پرس‌وجو اجرا می‌شود.
  • خطوط ۴۹–۵۱: مهم است به خاطر داشته باشید که اگر تراکنشی وجود نداشت، یک اتصال جدید به پایگاه داده در خطوط ۲۶–۳۰ باز شده است. در این صورت، باید بسته شود. اگر تراکنشی وجود داشت، اتصال نباید بسته شود زیرا توسط لایه [metier] مدیریت می‌شود.

دو روش دیگر پی‌گیری آنچه در فصل «پایگاه‌های داده» پوشش داده شد، هستند:


        // فهرست اقلام
        public List<Article> GetAllArticles() {
            // فهرست اقلام – در ابتدا خالی
            List<Article> articles = new List<Article>();
            //پردازش اتصال
            using (SqlCeConnection connexion = new SqlCeConnection(ConnectionString)) {
                // گشودن اتصال
                connexion.Open();
                // اجرای sqlCommand با یک پرس‌وجوی SELECT
                SqlCeCommand sqlCommand = new SqlCeCommand(GetAllText, connexion);
                using (SqlCeDataReader reader = sqlCommand.ExecuteReader()) {
                    // نتیجهٔ پردازش
                    while (reader.Read()) {
                        //پردازش سطر فعلی
                        articles.Add(new Article(reader.GetInt32(0), reader.GetString(1), reader.GetDecimal(2), reader.GetInt32(3), reader.GetInt32(4)));
                    }
                }
            }
            // بازگرداندن نتیجه
            return articles;
        }

        // حذف آیتم‌ها
        public void DeleteAllArticles() {
            using (SqlCeConnection connexion = new SqlCeConnection(ConnectionString)) {
                // ایجاد اتصال
                connexion.Open();
                // sqlCommand را با یک درخواست به‌روزرسانی اجرا می‌کند
                new SqlCeCommand(DeleteAllText, connexion).ExecuteNonQuery();
            }
}

برنامه آزمایشی [console]

برنامه آزمایشی [console] به شرح زیر است:


using System;
using System.Configuration;

namespace Chap8 {
    class Program {
        static void Main(string[] args) {
            //پردازش فایل پیکربندی
            string connectionString = null;
            string insertText;
            string getAllText;
            string deleteAllText;
            try {
                // رشته اتصال
                connectionString = ConfigurationManager.ConnectionStrings["dbArticlesSqlServerCe"].ConnectionString;
                // پارامترهای دیگر
                insertText = ConfigurationManager.AppSettings["insertText"];
                getAllText = ConfigurationManager.AppSettings["getAllText"];
                deleteAllText = ConfigurationManager.AppSettings["deleteAllText"];
            } catch (Exception e) {
                Console.WriteLine("Erreur de configuration : {0}", e.Message);
                return;
            }
            //ایجاد لایه [dao]
            Dao dao = new Dao();
            dao.ConnectionString = connectionString;
            dao.DeleteAllText = deleteAllText;
            dao.GetAllText = getAllText;
            dao.InsertText = insertText;
            // ایجاد لایه [métier]
            Metier metier = new Metier();
            metier.Dao = dao;
            metier.ConnectionString = connectionString;
            // ایجاد یک لیست از آیتم‌ها
            Article[] articles = new Article[2];
            for (int i = 0; i < articles.Length; i++) {
                articles[i] = new Article(0, "article", 100, 10, 1);
            }
            // تمام آیتم‌ها حذف می‌شوند
            Console.WriteLine("Suppression de tous les articles...");
            metier.DeleteAllArticles();
            // وارد کردن جدول خارج از تراکنش
            Console.WriteLine("Insertion des articles hors transaction...");
            try {
                metier.InsertArticlesOutOfTransaction(articles);
            } catch (Exception e){
                Console.WriteLine("Exception : {0}", e.Message);
            }
            //نمایش آیتم‌ها
            Console.WriteLine("Liste des articles");
            AfficheArticles(metier);
            // حذف همه آیتم‌ها
            Console.WriteLine("Suppression de tous les articles...");
            metier.DeleteAllArticles();
            // جدول را در یک تراکنش درج کنید
            Console.WriteLine("Insertion des articles dans une transaction...");
            metier.InsertArticlesInTransaction(articles);
            // نمایش اقلام
            Console.WriteLine("Liste des articles");
            AfficheArticles(metier);
        }

        private static void AfficheArticles(IMetier metier) {
            // موارد را نمایش می‌دهد
            foreach(Article article in metier.GetAllArticles()){
                Console.WriteLine(article);
            }
        }

    }
}
  • خطوط ۱۲–۲۲: فایل [App.config] پردازش می‌شود.
  • خطوط ۲۴–۲۸: لایه [dao] نمونه برداری و اولیه می‌شود
  • خطوط ۳۰–۳۲: همین کار برای لایه [metier] انجام می‌شود
  • خطوط ۳۴–۳۷: یک آرایه از دو آیتم با نام یکسان ایجاد می‌شود. جدول [articles] در پایگاه داده SQL روی سرور [dbarticles.sdf] دارای محدودیت یکتایی برای نام است. بنابراین درج مورد دوم رد خواهد شد. اگر جدول خارج از یک تراکنش درج شود، مورد اول ابتدا درج شده و در جدول باقی می‌ماند. اگر جدول در داخل یک تراکنش درج شود، مورد اول ابتدا درج شده و سپس هنگام commit تراکنش حذف خواهد شد.
  • خطوط ۳۹–۵۰: درج جدول حاوی دو آیتم خارج از تراکنش و اعتبارسنجی.
  • خطوط ۵۲–۵۹: مشابه مورد بالا، اما درون یک تراکنش

نتایج پس از اجرا به شرح زیر است:

1
2
3
4
5
6
7
8
9
Suppression de tous les articles...
Insertion des articles hors transaction...
Exception : A duplicate value cannot be inserted into a unique index. [ Table na
me = ARTICLES,Constraint name = UQ__ARTICLES__0000000000000010 ]
Liste des articles
[7,article,100,10,1]
Suppression de tous les articles...
Insertion des articles dans une transaction...
Liste des articles
  • خطوط ۵–۶: درج خارج از تراکنش، اولین آیتم را در پایگاه داده باقی گذاشت
  • خط ۹: درج انجام‌شده درون تراکنش هیچ رکودی در پایگاه داده باقی نگذاشت

10.9.3. نتیجه‌گیری

مثال قبلی ارزش داده‌های محلی-ریسه‌ای را برای مدیریت تراکنش نشان داد. نباید همان‌طور که هست تکرار شود. فریم‌ورک‌هایی مانند Spring، NHibernate و غیره از این تکنیک استفاده می‌کنند اما آن را حتی شفاف‌تر می‌سازند: لایه [metier] می‌تواند از تراکنش‌ها استفاده کند بدون اینکه لایه [dao] نیازی به آگاهی از آن داشته باشد. در نتیجه، هیچ شیء Transaction در کد لایه [dao] وجود ندارد. این امر با استفاده از یک تکنیک پروکسی به نام AOP (برنامه‌نویسی جنبه‌گرا) محقق می‌شود. بار دیگر، ما فقط می‌توانیم خواننده را به استفاده از این فریم‌ورک‌ها تشویق کنیم.

10.10. مطالعه بیشتر...

برای بررسی عمیق‌تر حوزه چالش‌برانگیز همگام‌سازی نخ‌ها، خوانندگان می‌توانند به فصل Threading کتاب *C# 3.0* که در مقدمه این سند به آن ارجاع شده است، مراجعه کنند. این فصل تکنیک‌های متعددی برای انواع مختلف موقعیت‌ها ارائه می‌دهد.