في نافذة الطباعة اختر «حفظ كملف PDF» كوجهة.
نظم التشغيل 1، الدرس العملي 6
عندما تتشارك الخيوط البيانات: التحديثات المفقودة، وlock، وMonitor، وخيط الواجهة
locklock: Monitor.Enter وMonitor.Exit وtry/finallyMonitor: الدالة TryEnter مع مهلة (timeout)، والدالتان Wait/Pulse لجعل الخيوط تتناوبSemaphoreInvoke أو BackgroundWorkerالجزء 1
الشيفرة نفسها، وإجابات مختلفة
تبدأ ثلاثة خيوط الدالة SomeMethod؛ وتنتظرها Main وتطبع الزمن. توقع المخرجات.
static void SomeMethod()
{
Console.Write("[Welcome To The ");
Thread.Sleep(1000);
Console.WriteLine("World of Dotnet!]");
}[Welcome To The [Welcome To The [Welcome To The World of Dotnet!]
World of Dotnet!]
World of Dotnet!]
Total: 1.1 sيحجز T1 عدد 3، ويحجز T2 عدد 2. يقرأ كل خيط العدد، ويعمل 100 ms، ثم يكتب العدد الجديد. توقع Tickets left.
static int availableTickets = 10;
static void Book(int wanted)
{
string name = Thread.CurrentThread.Name;
int seen = availableTickets; // 1) read
Console.WriteLine(name + " read " + seen);
Thread.Sleep(100); // a slow step
availableTickets = seen - wanted; // 2) compute, 3) write
Console.WriteLine(name + " wrote " + availableTickets);
}T1 read 10
T2 read 10
T1 wrote 7
T2 wrote 8
Tickets left: 8count++ ثلاث خطواتاللغة الوسيطة (IL) التي أنتجها المترجم للسطر count++ في برنامج العداد (قرأناها بأداة تفكيك)
count++;ldsfld int32 Program::count // 1) read count
ldc.i4.1
add // 2) add 1
stsfld int32 Program::count // 3) write count5 سيكتب كلاهما 6: فتكون إحدى الزيادتين مفقودةينفذ كل خيط count++ عدد 1,000,000 مرة. وعند استدعاء الدالة بالتتابع يطبع البرنامج count = 3000000. توقع النتيجة مع الخيوط.
static int count = 0;
static void IncrementCount()
{
for (int i = 1; i <= 1000000; i++)
{
count++;
}
}
// Main: start t1, t2, t3 on IncrementCount,
// Join all three, print count| مرة التشغيل | count |
|---|---|
| 1 | 2433729 |
| 2 | 1646435 |
| 3 | 1076791 |
| 4 | 1508029 |
| 5 | 1179164 |
يطلب Thread1 عدد 1 من التذاكر، وThread2 عدد 2، وThread3 عدد 3، والثلاثة على كائن BookMyShow واحد. توقع: كم تذكرة ستحجز؟
public int AvailableTickets = 3;
public void BookTicket(string name, int wantedTickets)
{
if (wantedTickets <= AvailableTickets)
{
Console.WriteLine(wantedTickets + " booked to " + name);
AvailableTickets = AvailableTickets - wantedTickets;
}
else
{
Console.WriteLine("No tickets available to book for " + name);
}
}2 booked to Thread2
1 booked to Thread1
3 booked to Thread3
Tickets left: -3if التي تفحص AvailableTickets مع السطر الذي يحدثهاcount++، بخطواته الثلاث كلها| الهدف | المعنى | مثال |
|---|---|---|
| الذرية (atomicity) | ينفذ المقطع الحرج كوحدة واحدة، بخيط واحد في كل مرة | حجز التذاكر، والعداد |
| الترتيب (ordering) | تعمل الخيوط بترتيب نختاره، أو لا يدخل إلا عدد محدود منها | رسالة كاملة واحدة في كل مرة؛ والأعداد الزوجية والفردية بالتناوب |
الجزء 2
خيط واحد في المقطع الحرج في كل مرة
private static readonly object lockObject = new object();
static void SomeMethod()
{
lock (lockObject)
{
// critical section:
// one thread at a time
}
}lock يحصل على القفل ويدخلlock حتى يغادر المالك الكتلةprivate: لا تستطيع الشيفرة الخارجية أن تقفل عليه؛ readonly: لا يمكن استبداله أبداstatic: كائن قفل static؛ ويمكن لدالة النسخة أن تستخدم حقل نسخةالخيوط الثلاثة نفسها؛ وجسم SomeMethod الآن داخل lock، مع رسالة قبله وداخله وبعده. توقع الترتيب والزمن.
Console.WriteLine(name + " trying to acquire the lock");
lock (lockObject)
{
Console.WriteLine(name + " got the lock");
Console.Write("[Welcome To The ");
Thread.Sleep(1000);
Console.WriteLine("World of Dotnet!]");
Console.WriteLine(name + " releases the lock");
}Thread 2 trying to acquire the lock
Thread 3 trying to acquire the lock
Thread 3 got the lock
[Welcome To The Thread 1 trying to acquire the lock
World of Dotnet!]
Thread 3 releases the lock
Thread 2 got the lock
[Welcome To The World of Dotnet!]
Thread 2 releases the lock
Thread 1 got the lock
[Welcome To The World of Dotnet!]
Thread 1 releases the lock
Total: 3.0 sضع count++ وحدها داخل القفل. توقع count.
private static readonly object lockCount = new object();
static void IncrementCount()
{
for (int i = 1; i <= 1000000; i++)
{
lock (lockCount)
{
count++;
}
}
}count = 30000003000000i متغير محلي: لكل خيط نسخته الخاصة منه، لذا تبقى الحلقة خارج القفل3 x 1000000 مرة: النتيجة صحيحة، لكن البرنامج أبطأ منه بدون قفلضع الفحص والتحديث معا داخل lock واحد. توقع المخرجات الممكنة.
lock (lockObject)
{
if (wantedTickets <= AvailableTickets)
{
Console.WriteLine(wantedTickets + " booked to " + name);
AvailableTickets = AvailableTickets - wantedTickets;
}
else
{
Console.WriteLine("No tickets available to book for " + name);
}
}| مرات التشغيل | المخرجات، في 60 مرة تشغيل |
|---|---|
| 51 | 1 إلى Thread1، و2 إلى Thread2، ولا شيء لـThread3 |
| 7 | 1 إلى Thread1، ولا شيء لـThread3، و2 إلى Thread2 |
| 1 | 2 إلى Thread2، و1 إلى Thread1، ولا شيء لـThread3 |
| 1 | 2 إلى Thread2، ولا شيء لـThread3، و1 إلى Thread1 |
يحول المترجم lock (lockObject) { body } إلى هذه الشيفرة
object temp = lockObject;
bool lockTaken = false;
try
{
Monitor.Enter(temp, ref lockTaken);
// body
}
finally
{
if (lockTaken)
{
Monitor.Exit(temp);
}
}Monitor.Enter القفل لخيط واحد وتضبط قيمة lockTaken لديه على trueref: تكتب Enter في متغير المستدعي نفسهfinally دائما، حتى بعد استثناء، لذا يحرر القفل دائماcount = 3000000 في 10 مرات من أصل 10اللغة الوسيطة للدالة IncrementCount في نسخة القفل المترجمة، مختصرة. bool& تعني ref bool.
.try
{
call void System.Threading.Monitor::Enter(object, bool&)
ldsfld int32 Program::count
ldc.i4.1
add
stsfld int32 Program::count
leave.s IL_0032
}finally
{
ldloc.2 // lockTaken
brfalse.s IL_0031 // false: skip Exit
call void System.Threading.Monitor::Exit(object)
}يرمي الخيط A استثناء داخل القفل ويلتقطه خارجه. ثم يطلب B القفل نفسه؛ وتعطيه Main مدة 2 s: تعيد b.Join(TimeSpan.FromSeconds(2)) القيمة true إذا انتهى B. توقع السطر الأخير.
static void Faulty() // thread A
{
try
{
lock (lockObject)
{
Console.WriteLine("A has the lock");
int tickets = int.Parse("three"); // throws FormatException
}
} catch (FormatException ex)
{
Console.WriteLine("A caught: " + ex.Message);
}
}A has the lock
A caught: The input string 'three' was not in a correct format.
B has the lock
B finished within 2 s: Trueالبرنامج نفسه، لكن الخيط A يستخدم Monitor.Enter وMonitor.Exit بدون finally. توقع السطر الأخير.
try
{
Monitor.Enter(lockObject);
Console.WriteLine("A has the lock");
int tickets = int.Parse("three"); // throws FormatException
Monitor.Exit(lockObject); // skipped by the exception
}
catch (FormatException ex)
{
Console.WriteLine("A caught: " + ex.Message);
}A has the lock
A caught: The input string 'three' was not in a correct format.
B finished within 2 s: Falseالجزء 3
كل ما يفعله lock، ومعه المهل والإشارات
| الدالة | ما تفعله |
|---|---|
Enter(obj), Enter(obj, ref taken) | الحصول على القفل؛ والانتظار في طابور الجاهزين (ready queue) ما دام خيط آخر يملكه |
Exit(obj) | تحرير القفل؛ فيستطيع خيط من طابور الجاهزين أن يحصل عليه |
TryEnter(obj, timeout, ref taken) | مثل Enter، لكنها تتخلى عن المحاولة بعد انقضاء المهلة (وتبقى taken بقيمة false) |
Wait(obj) | تحرير القفل والنوم في طابور الانتظار (waiting queue) حتى يأتي Pulse |
Pulse(obj) | نقل خيط واحد من طابور الانتظار إلى طابور الجاهزين |
تشغل ثلاثة خيوط الدالة PrintNumbers. توقع: هل يمكن أن يطبع خيطان أرقامهما في الوقت نفسه؟
Console.WriteLine(name + " trying to enter the critical section");
try
{
Monitor.Enter(lockPrintNumbers);
Console.WriteLine(name + " entered the critical section");
for (int i = 0; i < 5; i++)
{
Thread.Sleep(100);
Console.Write(i + ",");
}
Console.WriteLine();
}finally
{
Monitor.Exit(lockPrintNumbers);
Console.WriteLine(name + " exit from the critical section");
}Child Thread 2 trying to enter the critical section
Child Thread 1 trying to enter the critical section
Child Thread 2 entered the critical section
Child Thread 3 trying to enter the critical section
0,1,2,3,4,
Child Thread 2 exit from the critical section
Child Thread 1 entered the critical section
0,1,2,3,4,
Child Thread 1 exit from the critical section
Child Thread 3 entered the critical section
0,1,2,3,4,
Child Thread 3 exit from the critical sectionيملك كل خيط القفل مدة 5 x 100 ms، وينتظره 1 s على الأكثر. توقع: هل تطبع الخيوط الثلاثة كلها؟
TimeSpan timeout = TimeSpan.FromSeconds(1);
bool lockTaken = false;
try
{
Monitor.TryEnter(lockPrintNumbers, timeout, ref lockTaken);
if (lockTaken)
{
// "entered", the 5 numbers, a new line: as before
}
else
Console.WriteLine(name + ": lock was not acquired");
}finally
{
if (lockTaken)
{
Monitor.Exit(lockPrintNumbers); // then prints "exit"
}
}Child Thread 2 trying to enter the critical section
Child Thread 3 trying to enter the critical section
Child Thread 3 entered the critical section
Child Thread 1 trying to enter the critical section
0,1,2,3,4,
Child Thread 3 exit from the critical section
Child Thread 2 entered the critical section
0,1,2,3,Child Thread 1: lock was not acquired
4,
Child Thread 2 exit from the critical sectionالهدف: 0 1 2 3 ... 20، الأعداد الزوجية من خيط والأعداد الفردية من خيط آخر
0، ثم يستدعي Pulse: يمكن لخيط منتظر أن يستعدWait: فيحرر القفل وينام في طابور الانتظار1، ويستدعي Pulse (فيستعد evenThread)، ثم WaitWait ويطبع 2، وهكذاWait: إذ لن يبقى خيط يستدعي Pulse من أجلهnumberLimit = 20. توقع المخرجات.
static void PrintEvenNumbers()
{
try
{
Monitor.Enter(lockMonitor); for (int i = 0; i <= numberLimit; i = i + 2)
{
Console.Write(i + " ");
Monitor.Pulse(lockMonitor);
bool isLast = (i == numberLimit);
if (!isLast)
Monitor.Wait(lockMonitor);
}
}
finally { Monitor.Exit(lockMonitor); }
}PrintOddNumbers: الدالة نفسها مع i = 1 وisLast = (i == numberLimit - 1)lockMonitorMain: تبدأ evenThread، ثم Thread.Sleep(100) كي يعمل أولا، ثم تبدأ oddThread، ثم Join للخيطين0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
Main method completedيستدعي الخيطان Wait دائما. وهما خيطان خلفيان، وتنتظر Main كلا منهما 2 s على الأكثر: تعيد Join(TimeSpan) القيمة true إذا انتهى الخيط. توقع السطر الأخير.
Console.Write(i + " ");
Monitor.Pulse(lockMonitor);
Monitor.Wait(lockMonitor); // no isLast check
// in Main
bool oddDone = oddThread.Join(TimeSpan.FromSeconds(2));
bool evenDone = evenThread.Join(TimeSpan.FromSeconds(2));
Console.WriteLine("odd finished: " + oddDone + ", even finished: " + evenDone);0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
odd finished: True, even finished: Falselock | Monitor | |
|---|---|---|
| الحصول على القفل وتحريره | تلقائيا، عند { و} | Enter وExit، يدويا |
| بعد استثناء | يحرر دائما | فقط مع try/finally تكتبها بنفسك |
| التخلي بعد انقضاء مهلة | لا | TryEnter |
| إرسال إشارة إلى خيط آخر | لا | Wait، Pulse، PulseAll |
| نوع القفل | من يمكنه الدخول | أدوات C# |
|---|---|---|
| حصري | خيط واحد في كل مرة | lock، Monitor، Mutex، SpinLock |
| غير حصري | عدد محدود من الخيوط | Semaphore، SemaphoreSlim، ReaderWriterLockSlim |
يملك كل خيط من خمسة خيوط خانة مدة 1 s. توقع الزمن الكلي.
static Semaphore sm = new Semaphore(2, 2); // 2 free slots, at most 2
static void Work()
{
string name = Thread.CurrentThread.Name;
sm.WaitOne(); // take a slot, or wait for one
Console.WriteLine(Now() + " " + name + " entered");
Thread.Sleep(1000);
Console.WriteLine(Now() + " " + name + " is leaving");
sm.Release(); // give the slot back
}0.0 s Thread 2 entered
0.0 s Thread 1 entered
1.0 s Thread 2 is leaving
1.0 s Thread 3 entered
1.0 s Thread 1 is leaving
1.0 s Thread 4 entered
2.0 s Thread 3 is leaving
2.0 s Thread 5 entered
2.0 s Thread 4 is leaving
3.0 s Thread 5 is leaving
All done at 3.0 sالجزء 4
لتطبيق Windows Forms خيط واجهة واحد
تشغل Application.Run(new Form1()) هذه الحلقة على خيط الواجهة
ويندوز
الفأرة، والمفاتيح، وإعادة الرسم
طابور الرسائل
الداخل أولا يخرج أولا
مضخة الرسائل
رسالة واحدة في كل مرة
معالج الحدث الخاص بك
button1_Click
يضبط معالج حدث النقر النص، ثم يعمل مدة 20 s. توقع ما تعرضه النافذة بعد 8 s.
private void button1_Click(object sender, EventArgs e)
{
textBox1.Text = "Working";
Thread.Sleep(20 * 1000); // 20 s of work on the UI thread
textBox1.Text = "Done";
}

يبدأ النقر خيطا خلفيا يكتب الأعداد من 0 إلى 9 في textBox1، عددا كل ثانية. توقع ما يحدث تحت مصحح الأخطاء (F5).
private void button1_Click(object sender, EventArgs e)
{
var thread = new Thread(StartCounting);
thread.IsBackground = true;
thread.Start();
}private void StartCounting()
{
for (var i = 0; i < 10; i++)
{
textBox1.Text = i.ToString();
Thread.Sleep(1000);
}
}
Control.CheckForIllegalCrossThreadCalls هي False: فعرضت الشيفرة نفسها 3 بعد 3.5 sControl.CheckForIllegalCrossThreadCalls = true; في Main ليتصرف البرنامج كما مع F5تسلم Invoke مفوضا إلى خيط الواجهة، فينفذه عبر مضخة الرسائل
private delegate void DisplayCountDelegate(int i);
private void StartCounting() // worker thread
{
for (var i = 0; i < 10; i++)
{
textBox1.Invoke(new DisplayCountDelegate(DisplayCount), i);
Thread.Sleep(1000);
}
button1.Invoke(new EnableButtonDelegate(EnableButton));
}private void DisplayCount(int i) // runs on the UI thread
{
textBox1.Text = i.ToString();
}
button1، فلا يعمل إلا خيط عد واحد؛ وتعيد EnableButton تفعيله عبر Invoke أيضا3، والنافذة تبقى مستجيبةتنفذ أحداثه على الخيط الذي أنشأه: خيط الواجهة
worker = new BackgroundWorker();
worker.WorkerReportsProgress = true;
worker.DoWork += StartCounting; // worker thread
worker.ProgressChanged += worker_ProgressChanged; // UI thread
worker.RunWorkerCompleted += worker_RunWorkerCompleted; // UI thread
// StartCounting calls worker.ReportProgress(i) once a second
private void worker_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
textBox1.Text = e.ProgressPercentage.ToString();
}
InvokeRunWorkerAsync() الحدث DoWork على خيط عاملRunWorkerCompleted تفعيل الزر وكتب Completedالجزء 5
نحو 30 دقيقة: توقع على الورق، ثم شغل البرنامج وتحقق
نحو 6 دقائق
static int balance = 500;ATM مبلغ 300، ويسحب الخيط App مبلغ 400؛ وتنتظر Main الخيطين وتطبع Balancelock. ما المخرجات الممكنة عندئذ؟static void Withdraw(int amount)
{
string name = Thread.CurrentThread.Name;
if (amount <= balance)
{
Console.WriteLine(name + " withdraws " + amount);
Thread.Sleep(100); // the bank's slow check
balance = balance - amount;
}
else
{
Console.WriteLine(name + ": not enough money");
}
}| النسخة | المخرجات، في 20 مرة تشغيل لكل نسخة |
|---|---|
| بدون قفل | يسحب الخيطان كلاهما، Balance: -200 (20 من أصل 20) |
lock حول if | ATM withdraws 300، App: not enough money، Balance: 200 (19 من أصل 20) |
lock حول if | App withdraws 400، ATM: not enough money، Balance: 100 (1 من أصل 20) |
نحو 6 دقائق
يشغل كل خيط من خيطين الدالة Increment؛ وتنتظر Main الخيطين وتطبع count. توقع count في A وB وC.
// A
lock (lockCount) { for (int i = 0; i < 500000; i++) count++; }
// B
for (int i = 0; i < 500000; i++) { lock (lockCount) { count++; } }
// C
for (int i = 0; i < 500000; i++)
{
int next = count + 1;
lock (lockCount) { count = next; }
}1000000 دائما؟| النسخة | count، في 12 مرة تشغيل لكل نسخة | السبب |
|---|---|---|
| A | 1000000 في 12 | الحلقة كلها مقطع حرج واحد: ينفذها خيط كلها، ثم الآخر |
| B | 1000000 في 12 | كل count++ محمية؛ ويحرر القفل بعد كل واحدة، لذا تتداخل الخيوط |
| C | 12 قيمة مختلفة، من 491699 إلى 874943 | القراءة (count + 1) خارج القفل |
نحو 6 دقائق
أربعة خيوط؛ يملك كل منها القفل مدة 3 صفحات، كل صفحة 200 ms، وينتظره 1 s على الأكثر. توقع عدد الخيوط التي تطبع.
Monitor.TryEnter(lockPrinter, TimeSpan.FromSeconds(1), ref lockTaken);
if (lockTaken)
{
for (int page = 1; page <= 3; page++)
{
Thread.Sleep(200); // one page takes 200 ms
}
Console.WriteLine(name + " printed 3 pages");
}
else
{
Console.WriteLine(name + " gave up");
}T1 printed 3 pages
T4 gave up
T2 gave up
T3 printed 3 pagesنحو 8 دقائق
Play(string word): تطبع word ومسافة، 5 مراتPlay("ping")، ثم بعد 100 ms الخيط Play("pong")ping pong ping pong ping pong ping pong ping pong، ثم Game overMonitor.Enter وPulse وWait وExit، كما في مثال الزوجي والفرديstatic void Play(string word)
{
try
{
Monitor.Enter(lockBall);
for (int i = 1; i <= rounds; i++) // rounds = 5
{
Console.Write(word + " ");
Monitor.Pulse(lockBall); // wake the other player
if (i != rounds)
Monitor.Wait(lockBall); // hand the ball over and wait
}
}ping pong ping pong ping pong ping pong ping pong
Game over finally
{
Monitor.Exit(lockBall);
}
}نحو 4 دقائق
مثال السيمافور مع new Semaphore(3, 3)، وستة خيوط، وعمل مدته 2 s لكل منها. توقع الزمن الكلي وأكبر عدد من الخيوط في الداخل في آن واحد.
0.0 s Thread 4 entered
0.0 s Thread 1 entered
0.0 s Thread 3 entered
2.0 s Thread 4 is leaving
2.0 s Thread 2 entered
...
4.0 s Thread 6 is leaving
All done at 4.0 scount++ قراءة ثم جمع ثم كتابةlock بدخول خيط واحد في كل مرة؛ وهو Monitor.Enter + Exit داخل try/finallyMonitor الدالة TryEnter (مهلة) والدالتين Wait/Pulse (التناوب)Semaphore بدخول عدد ثابت من الخيوط؛ ويعمل Mutex أيضا بين البرامجInvoke أو BackgroundWorkerMahmoud Abas|تزامن الخيوط بلغة C#