تعدد المهام في السي بلاس بلاس | C++ Multithreading

تعدد المهام في السي بلاس بلاس 

 تعدد المهام في C++

عندما تستخدم هاتفك أو حاسوبك ترى أنه يمكنك تشغيل عدة برامج مع بعض في وقت واحد, كل برنامج شغال في الذاكرة يعتبر Process فمثلاً إذا قمت بتشغيل خمسة برامج مع بعض فهذا يعني أن نظام التشغيل ينظم عمل خمسة Processes مع بعض. آلية تشغيل عدة برامج مع بعض تسمى Multiprocessing.

من جهة اخرى, في البرنامج الواحد يمكنك تنفيذ عدة أوامر مع بعض و جعل المستخدم يشعر كأنها تتنفذ في وقت واحد, فمثلاً في حال كنت تلعب لعبة مثل لعبة كرة القدم, تجد أنه هناك عدة أشياء تحدث في وقت واحد, فمثلاُ عند تشغيل اللعبة تسمع عدة أصوات ( مثل أغنية حماسية, صوت المعلق, صوت المشجعين, صوت صفارة الحكم في حال وقع خطأ إلخ.. ), بالإضافة إلى أنه يمكنك تحريك اللاعب و مشاهدة توقيت المبارة و الكثير من التفاصيل الأخرى التي تحدث كلها في نفس الوقت لتصنع لك لعبة رائعة. هذه الآلية تسمى تعدد المهام ( Multithreading ) لأن كل جزء شغال في البرنامج يكون عبارة عن مجموعة أوامر موضوعة بداخل ثريد ( Thread ) خاص.

إذاً نستخدم آلية تعدد المهام لجعل البرنامج قادر على تنفيذ عدة أوامر مع بعض و كأنها تتنفذ في وقت واحد, و هذا ما سنتعلمه في هذا الدرس.


أهمية تعدد المهام في السي بلاس بلاس

  • جعل المستخدم قادر على تنفيذ عدة عمليات مع بعض في نفس الوقت.

  • جعل تصميم التطبيقات أجمل و إضافة مؤثرات فيه.

  • كل ثريد تقوم بتشغيله, يعمل بشكل منعزل عن باقي الأوامر الموجودة في البرنامج, و بالتالي فإنه في حال وقوع أي خطأ في الثريد فإنه لن يؤثر على باقي الأوامر الموجود في البرنامج, كما أنه لا يؤثر على أي ثريد آخر شغال في البرنامج.


معلومة حول تعدد المهام في السي بلاس بلاس

في السابق كان لا بد لك من الإعتماد على مكتبة موجودة في لغة C إسمها POSI حتى تتمكن من إنشاء ثريد و التعامل معه لأن لغة C++ وقتها لم تكن توفر لك ذلك.
إبتداءاً من الإصدار C++11 تم توفير كل ما يلزم لإنشاء ثريد و التحكم به و بالتالي ستتعلم في هذا الدرس كيف تستخدم الكلاسات و الدوال الموجودة في هذا الإصدار و التي تعمل على الإصدارات الجديدة أيضاً.

مفهوم الثريد الرئيسي في C++

في C++ كل كود يتنفذ في البرنامج, فإنه حتماً يتنفذ بداخل ثريد واحد على الأقل.
أي حتى لو لم تقم بوضع الكود بداخل ثريد فإنه سيتم وضعه في واحد.

لتوضيح هذه الفكرة أكثر, عندما يتنفذ الكود الموضوع بداخل الدالة main() يقوم المترجم بوضعه في ثريد خاص يقال له Main Thread.


إذا عندما يبدأ المترجم بتنفيذ الكود التالي فإنه سيضعه بداخل ثريد خاص.

المثال الأول في الثريد الرئيسي في السي بلاس بلاس

main.cpp
#include <iostream>

using namespace std;

// سيقوم المترجم بوضعه بداخل ثريد خاص عندما يبدأ بتنفيذه main() كل الكود الموضوع بداخل الدالة
int main()
{
    for(int i=1; i<=5; i++)
    {
        cout << i << endl;
    }
	
	return 0;
}
		

سنحصل على نتيجة تشبه النتيجة التالية عند التشغيل.

1
2
3
4
5
		

الآن حتى تتأكد أن الكود الموضوع في الدالة main() يقوم المترجم بوضعه بداخل ثريد سنقوم في المثال التالي بالتعامل معه كثريد.
للدقة أكثر سنقوم بالتحكم بوقت تنفيذ الكود باستخدام الكلاس this_thread المخصص للتعامل مع الثريد الذي يعمل في الوقت الحالي في البرنامج.


معلومة تقنية حول الثريد الرئيسي في السي بلاس بلاس

الكلاس this_thread يحتوي على دالة ثابتة إسمها sleep_for() يمكن استخدامها لجعل الثريد الحالي ينتظر لمدة محددة قبل أن يتابع عمله, و لتحديد هذه المدة نستخدم إحدى الدوال الثابتة الموجودة في الكلاس chrono المخصصة لذلك مثال الدالة minutes() إذا أردنا تحديد المدة بالدقائق, أو الدالة seconds() إذا أردنا تحديد المدة بالثواني, أو الدالة milliseconds() إذا أردنا تحديد المدة بأجزاء من الثانية.

لاستخدام الكلاس this_thread و الكلاس chrono يجب تضمين الملفين <thread> و <chrono>.


في المثال التالي قمنا بإنشاء حلقة, في كل دورة تقوم بطباعة قيمة العداد i و من ثم تتوقف مدة ثانية واحدة قبل أن تنتقل للدورة التالية.
ملاحظة: لجعل المترجم يتوقف استخدمنا الدالة sleep_for() و لتحديد مدة التوقف استخدمنا الدالة seconds().

المثال الثاني في الثريد الرئيسي في السي بلاس

main.cpp
#include <iostream>
#include <thread>
#include <chrono>

using namespace std;

int main()
{
	// ثم تتوقف ثانية واحدة قبل الإنتقال للدورة التالية i هنا قمنا بإنشاء حلقة تقوم في كل دورة بطباعة قيمة العداد
    for(int i=1; i<=5; i++)
    {
        cout << i << endl;
        this_thread::sleep_for(chrono::seconds(1));
    }
	
	return 0;
}
		

سنحصل على النتيجة التالية عند التشغيل مع الإشارة إلى أنه سيتم طباعة عدد واحد في كل ثانية.

1
2
3
4
5
		

كيف يستطيع المترجم التمييز بين ثريد و آخر؟

يقوم المترجم بإعطاء رقم تعرفة ( ID ) لكل ثريد يتم إنشاؤه حتى يستطيع التمييز فيما بينهم.
كما أنك تستطيع التعامل مع كل ثريد على حدا من خلال رقم التعرفة الخاص به.

مبادئ إنشاء ثريد و تشغيله في C++

بشكل عام, لإنشاء ثريد و تشغيله يجب اتباع الخطوات التالية.

// thread أولاً يجب تضمين الكلاس
import<thread> 

// callable بعدها يجب إنشاء كائن منه و تمرير الدالة التي نريده أن ينفذها مكان الباراميتر
std::thread thread_object(callable);

// في الأخير أو قبل نهاية البرنامج يجب إيقاف الكائن عن التنفيذ حتى لا يؤدي ذلك لحدوث مشكلة
thread_object.join();
	

  1. يجب تضمين الملف <thread>.

  2. يجب إنشاء كائن من الكلاس thread و تمرير دالة له مكان الباراميتر callable تحتوي على الأوامر التي نريد تنفيذها بواسطة هذا الكائن.

  3. مكان الباراميتر callable يمكنك تمرير إسم الدالة التي تريده أنه ينفذها, أو تعريف الدالة التي سينفذها بشكل مباشر بأسلوب Lambda أو تمرير إسم دالة معرّفة بأسلوب Function Object حتى ينفذها.

لا تقلق ستتعلم من الأمثلة كيف تنشئ ثريد بتفصيل ممل و لكن تذكر دائماً أن هذه الخطوات يجب تطبيقها عند التعامل مع ثريد.



معلومة عند إنشاء ثريد في السي بلاس بلاس

عند تشغيل أكثر من ثريد في وقت واحد, لا يمكنك ضمان أو تحديد أي ثريد سيتنفذ أو ينتهي قبل الآخر.
السبب في ذلك أن معالج الحاسوب ( CPU ) سيقوم بإرسال كل ثريد قمت بتشغيله إلى نواة ( Core ) حتى ينفذهم لك في وقت واحد.
و منطقياً, النواة التي عليها ضغط أقل ستنتهي من تنفيذ أوامر الثريد بشكل أسرع, و للعلم فإن النواة الواحدة قادرة على تنفيذ آلاف الأوامر في أقل من ثانية واحدة.



ملاحظة

سنقوم باستخدام الدالة sleep_for() في أغلب الأمثلة التي تمر معك في هذا الدرس لجعلك تلاحظ كيف أن المترجم سيقوم بتشغيل الثريدات ( Threads ) بشكل عشوائي بهدوء لأننا لو لم نفعل ذلك سيكون من الصعب عليك ملاحظة الفرق.

للإيضاح أكثر, بسبب سرعة الحاسوب الهائلة في تنفيذ الأوامر كنا سنضطر لجعل الحلقة تكرر تنفيذ أمر الطباعة الموضوع في كل ثريد آلاف المرات حتى تلاحظ كيف أن التنفيذ يحدث بشكل عشوائي لأنك سترى المترجم مرة مثلاً يطبع الكلمة "Thread-1" 700 مرة و مرة يطبع الكلمة "Thread-2" 300 مرة ثم يرجع و يطبع الكلمة "Thread-1" 200 مرة إلخ.. لذا كنت ستجد صعوبة كبيرة في ملاحظة ذلك.

نود الإشارة إلى أن الأرقام التي ذكرناها هي مجرد فرضيات و ليست حقيقية لأنه لا يمكن ضمان الترتيب الذي سيتم فيه تنفيذ أوامر الطباعة حيث أنها تتنفذ على حسب قدرة النواة التي يعمل عليها الثريد.

أمثلة شاملة حول التعامل مع الثريد في C++


 طريقة إنشاء ثريد و تمرير دالة له في السي بلاس بلاس

في المثال التالي ستتعلم طريقة إنشاء ثريد و تمرير دالة له.

في المثال التالي قمنا بتعريف دالة إسمها func() عند استدعائها تقوم بطباعة الجملة "func is executed.." 5 مرات بواسطة حلقة مع التوقف لمدة ثانية في كل مرة.
في الدالة main() قمنا بإنشاء كائن من الكلاس thread ينفذ الدالة func() بالإضافة إلى أننا قمنا بإنشاء حلقة تقوم بطباعة الجملة "main is executed.." 5 مرات مع التوقف لمدة ثانية في كل مرة أيضاً حتى تلاحظ كيف أنهما سيتنفذان في وقت واحد.

المثال الأول طريقة إنشاء ثريد  وتمرير داله له في السي بلاس بلاس

main.cpp
      #include <iostream>
      #include <thread>
      #include <chrono>

      using namespace std;

      // خمس مرات مع التوقف لمدة ثانية في كل مرة "func is executed.." عند استدعائها تقوم بطباعة النص func() هنا قمنا بتعريف دالة إسمها
      void func()
      {
      for(int i=1; i<=5; i++)
      {
      cout << "func is executed.. \n";
      this_thread::sleep_for(chrono::seconds(1));
      }
      }

      int main()
      {
      // مع الإنتقال إلى الكود الموجود بعدها بشكل مباشر func() حتى يقوم بتنفيذ الدالة thread هنا قمنا بإنشاء كائن من الكلاس 
      thread t(func);

      // خمس مرات مع التوقف لمدة ثانية في كل مرة "main is executed.." عندما يصل المترجم لهذه الحلقة سيقوم بطباعة الجملة
      for(int i=1; i<=5; i++)
      {
      cout << "main is executed.. \n";
      this_thread::sleep_for(chrono::seconds(1));
      }

      // لجعل المترجم ينتظر تنفيذ أوامره قبل أن يتوقف الثريد الرئيسي في البرنامج حتى لا يسبب مشكلة t من الكائن join() هنا قمنا باستدعاء الدالة
      t.join();

      return 0;
      }
    

سنحصل على نتيجة تشبه النتيجة التالية عند التشغيل.

      main is executed..
      func is executed..
      func is executed..
      main is executed..
      func is executed..
      main is executed..
      func is executed..
      main is executed..
      func is executed..
      main is executed..
    


في المثال التالي قمنا بإعادة المثال الأول و لكننا قمنا بتعريف الدالة التي سينفذها الثريد بأسلوب Lambda Expression.

المثال الثاني 

main.cpp
      #include <iostream>
      #include <thread>
      #include <chrono>

      using namespace std;

      int main()
      {
      // Lambda Expression هنا قمنا بتعريف و تمرير الدالة التي سينفذها الثريد مباشرةً فيه بأسلوب
      thread t( []{
      for(int i=1; i<=5; i++)
      {
      cout << "lambda is executed.. \n";
      this_thread::sleep_for(chrono::seconds(1));
      }
      });

      for(int i=1; i<=5; i++)
      {
      cout << "main is executed.. \n";
      this_thread::sleep_for(chrono::seconds(1));
      }

      t.join();

      return 0;
      }
    

سنحصل على نتيجة تشبه النتيجة التالية عند التشغيل.

      main is executed..
      lambda is executed..
      lambda is executed..
      main is executed..
      lambda is executed..
      main is executed..
      lambda is executed..
      main is executed..
      lambda is executed..
      main is executed..
    


في المثال التالي قمنا بإعادة المثال الأول و لكننا قمنا بتمرير دالة للثريد بأسلوب Function Object.
ملاحظة: هنا يجب جعل الدالة التي سينفذها الثريد تحتوي على باراميتر واحد على الأقل لهذا السبب وضعنا فيها الباراميتر n.

المثال الثالث

main.cpp
      #include <iostream>
      #include <thread>
      #include <chrono>

      using namespace std;

      // هنا قمنا بتعريف كلاس يحتوي على الدالة التي سنمررها لكائن الثريد
      class FnObject
      {
      public:
      void operator()(int n) {
      for(int i=1; i<=n; i++)
      {
      cout << "FnObject is executed.. \n";
      this_thread::sleep_for(chrono::seconds(1));
      }
      }
      };

      int main()
      {
      // هنا قمنا بتمرير كائن من الكلاس الذي قمنا بتعريفه لكائن الثريد
      // لأنه يتوقع أن نمرر له قيمة n كما أننا قمنا بتمرير القيمة 5 للبارميتر
      thread t(FnObject(), 5);

      for(int i=1; i<=5; i++)
      {
      cout << "main is executed.. \n";
      this_thread::sleep_for(chrono::seconds(1));
      }

      t.join();

      return 0;
      }
    

سنحصل على نتيجة تشبه النتيجة التالية عند التشغيل.

      main is executed..
      FnObject is executed..
      main is executed..
      FnObject is executed..
      main is executed..
      FnObject is executed..
      FnObject is executed..
      main is executed..
      FnObject is executed..
      main is executed..
    


 طريقة تشغيل أكثر من ثريد في وقت واحد في السي بلاس بلاس

في المثال التالي ستتعلم طريقة تشغيل أكثر من ثريد في وقت واحد.

في المثال التالي قمنا بتعريف دالة إسمها foo() عند استدعائها تقوم بطباعة الجملة "foo is executed.." 5 مرات.
بعدها قمنا بتعريف دالة أخرى إسمها bar() عند استدعائها تقوم بطباعة الجملة "bar is executed.." 5 مرات أيضاً.
في الأخير قمنا بإنشاء كائنين من الكلاس thread, الأول ينفذ الدالة foo() و الثاني ينفذ الدالة bar().

المثال الأول طريقة تشغيل أكثر من ثريد في وقت واحد

main.cpp
      #include <iostream>
      #include <thread>
      #include <chrono>

      using namespace std;

      // عند استدعائها تقوم بطباعة النص foo() هنا قمنا بتعريف دالة إسمها
      // خمس مرات مع التوقف لمدة ثانية في كل مرة "foo is executed.."
      void foo()
      {
      for(int i=1; i<=5; i++)
      {
      cout << "foo is executed.. \n";
      this_thread::sleep_for(chrono::seconds(1));
      }
      }

      // عند استدعائها تقوم بطباعة النص bar() هنا قمنا بتعريف دالة إسمها
      // خمس مرات مع التوقف لمدة ثانية في كل مرة "bar is executed.."
      void bar()
      {
      for(int i=0; i<5; i++)
      {
      cout << "bar is executed.. \n";
      this_thread::sleep_for(chrono::seconds(1));
      }
      }

      int main()
      {
      // bar() و الثاني يقوم بتشغيل الدالة foo() الأول يقوم بتشغيل الدالة thread هنا قمنا بإنشاء كائنين من الكلاس 
      thread t1(foo);
      thread t2(bar);

      // لجعل المترجم ينتظر تنفيذ أوامر t2 و t1 من الكائنين join() هنا قمنا باستدعاء الدالة
      // main() بالكامل قبل أن يتابع تنفيذ باقي الأوامر الموجودة في الدالة t2 و t1 الكائنين 
      t1.join();
      t2.join();

      // عند التنفيذ سيتم تنفيذ أمر الطباعة التالي و الذي يعني أنهما توقفا عن التنفيذ t2 و t1 إذاً بعد توقف الكائن 
      cout << "All threads are stopped!";

      return 0;
      }
    

سنحصل على نتيجة تشبه النتيجة التالية عند التشغيل.

      foo is executed..
      bar is executed..
      bar is executed..
      foo is executed..
      bar is executed..
      foo is executed..
      bar is executed..
      foo is executed..
      bar is executed..
      foo is executed..
      All threads are stopped!
    


في المثال التالي قمنا بإعادة المثال الأول و لكننا قمنا بتعريف الدالة التي سينفذها الثريد بأسلوب Lambda Expression.

المثال الثاني طريقة تشغيل أكثر من ثريد في وقت واحد

main.cpp
      #include <iostream>
      #include <thread>
      #include <chrono>

      using namespace std;

      int main()
      {
      // خمس مرات مع التوقف لمدة ثانية في كل مرة "t1 is executed.." يقوم بطباعة الجملة thread هنا قمنا بإنشاء كائن من الكلاس 
      thread t1( []{
      for(int i=1; i<=5; i++)
      {
      cout << "t1 is executed.. \n";
      this_thread::sleep_for(chrono::seconds(1));
      }
      });

      // خمس مرات مع التوقف لمدة ثانية في كل مرة "t2 is executed.." يقوم بطباعة الجملة thread هنا قمنا بإنشاء كائن من الكلاس 
      thread t2( []{
      for(int i=0; i<5; i++)
      {
      cout << "t2 is executed.. \n";
      this_thread::sleep_for(chrono::seconds(1));
      }
      });

      // لجعل المترجم ينتظر تنفيذ أوامر t2 و t1 من الكائنين join() هنا قمنا باستدعاء الدالة
      // main() بالكامل قبل أن يتابع تنفيذ باقي الأوامر الموجودة في الدالة t2 و t1 الكائنين 
      t1.join();
      t2.join();

      // عند التنفيذ سيتم تنفيذ أمر الطباعة التالي و الذي يعني أنهما توقفا عن التنفيذ t2 و t1 إذاً بعد توقف الكائن 
      cout << "All threads are stopped!";

      return 0;
      }
    

سنحصل على نتيجة تشبه النتيجة التالية عند التشغيل.

      t1 is executed..
      t2 is executed..
      t2 is executed..
      t1 is executed..
      t2 is executed..
      t1 is executed..
      t2 is executed..
      t1 is executed..
      t2 is executed..
      t1 is executed..
      All threads are stopped!
    


 طريقة تمرير قيم للدالة التي سينفذها الثريد في السي بلاس بلاس

في المثال التالي ستتعلم طريقة تمرير قيم للدالة التي سينفذها الثريد.

في المثال التالي قمنا بتعريف دالة إسمها func() عند استدعائها نمرر لها نص فتقوم بطباعته 5 مرات مع التوقف لثانية واحدة في كل مرة.
في الدالة main() قمنا بإنشاء كائنين من الكلاس thread, الأول ينفذ الدالة func() و يمرر لها النص "Thread-1 is executed.." و الثاني ينفذ الدالة func() و يمرر لها النص "Thread-2 is executed..".

ملاحظة: لتمرير قيمة لباراميتر الدالة التي ينفذها الثريد قم بوضع فاصلة بعد إسم الدالة و من ثم القيمة, و في حال أردت تمرير أكثر من قيمة يجب أن تضع فاصلة بين كل قيمتين.

مثال يوضح طريقة تمرير قيم للدالة التي سينفذها الثريد في السي بلاس

main.cpp
      #include <iostream>
      #include <thread>
      #include <chrono>

      using namespace std;

      void func(string txt)
      {
      for(int i=0; i<5; i++)
      {
      cout << txt << "\n";
      this_thread::sleep_for(chrono::seconds(1));
      }
      }

      int main()
      {
      // txt و كل واحد منهما يمرر لها نص مختلف مكان الباراميتر func() ينفذان الدالة thread هنا قمنا بإنشاء كائنين من الكلاس 
      thread t1(func, "t1 is executed..");
      thread t2(func, "t2 is executed..");

      // المترجم ينتظر t2 و t1 من الكائنين join() هنا وضعنا قمنا باستدعاء الدالة
      // قبل أن يتابع تنفيذ باقي الأوامر الموجودة في الملف t1 و t1 أن يتوقف الكائنين 
      t1.join();
      t2.join();

      cout << "All threads are stopped!";

      return 0;
      }
    

سنحصل على نتيجة تشبه النتيجة التالية عند التشغيل.

      t1 is executed..
      t2 is executed..
      t2 is executed..
      t1 is executed..
      t2 is executed..
      t1 is executed..
      t2 is executed..
      t1 is executed..
      t2 is executed..
      t1 is executed..
      All threads are stopped!
    

المزامنة في السي بلاس بلاس C++

في حال كنت تريد تشغيل أكثر من ثريد في نفس الوقت, يجب أن تنتبه جيداً إلى العمليات التي سيجريها كل ثريد تنوي تشغيله لأن هذا الأمر قد يسبب لك مشاكل منطقية أو يعطيك نتائج خاطئة كما سنوضح لك في السيناريوهات التالية.


السيناريو الأول المزامنة في السي بلاس بلاس

في حال قمت ببناء ثريد مهمته جلب علامات الطالب المخزنة في قاعدة بيانات و من ثم حساب معدله العام, و بعدما تم حساب المعدل و عرض المعدل للطالب, قام ثريد آخر بتعديل بعض العلامات في قاعدة البيانات لأنه وجد أن الطالب عنده غياب كثير.
إذاً, النتيجة التي أعطانا إياها الثريد الأول في هذه الحالة ليست صحيحة, حيث أنه كان يفترض حساب المعدل بعد أن تم إدخال أيام الغياب ضمن المعادلة التي تحسب له معدله النهائي و تعرض له إن كان ناجحاً بناءاً على معدله النهائي و عدد الأيام التي حضر فيها إلى الجامعة.
فعلى سبيل المثال, قد يكون من شروط الجامعة أنه في حال تغيّب الطالب مدة 30 يوم خلال الفصل الواحد, يعتبر راسباً في كل المواد.

إذاً, لحل المشكلة السابقة, كان يجب مزامنة عمل الثريد الأول و الثريد الثاني.
أي كان يجب تشغيل الثريد الذي يجلب أيام الغياب أولاً.
ثم بعد تخزين أيام الغياب و توقف الثريد عن العمل, يجب تشغيل الثريد الذي يجلب علامات الطالب و يعطيه النتيجة النهائية.


السيناريو الثاني المزامنة في السي بلاس بلاس

في حال كان يوجد ثريد يريد تعديل محتوى ملف, و كان يوجد ثريد آخر يقوم بقراءة محتوى نفس الملف.
في هذه الحالة, سيحدث أيضاً خطأ و هو أن الثريد الذي يقوم بالقراءة, سيقرأ محتوى الملف القديم, بدون معرفة أنه قد تم تحديث محتوى هذا الملف في الوقت الذي كان يقرأ منه و يجري عمليات ما بناءاً على المحتوى الذي قرأه وقتها.
لحل هذه المشكلة, كان يجب مزامنة عمل الثريد الذي يقرأ من الملف و الثريد الذي يعدل في الملف لضمان أن لا يتعاملا معه في وقت واحد.


السيناريو الثالث المزامنة في السي بلاس بلاس

في حال قمت بتشغيل إثنين ثريد, و في مرحلة ما أصبح الإثنين عالقين بسبب أن الثريد الأول بحاجة للوصول إلى شيء يستخدمه الثريد الثاني. و بنفس الوقت الثريد الثاني بحاجة للوصول إلى شيء يستخدمه الثريد الأول. هذه المعضلة تسمى Deadlock, و يمكنك تخليها كما في الصورة التالية.


لحل هذه المشكلة يمكنك تشغيل كل ثريد على حدا أو وضع قفل يقضي بجعل الثريد الذي يدخل أولاً يتم تنفيذه بالكامل و بعد أن ينتهي يبدأ الثريد الآخر بالتنفيذ.
لوضع قفل على الأوامر التي لا نريد لأكثر من ثريد أن ينفذوها في وقت واحد نستخدم دوال كلاس جاهز إسمه mutex.

ملاحظة: لاستخدام الكلاس mutex يجب تضمين الملف <mutex>.



في المثال التالي قمنا بإنشاء إثنين ثريد يعملات بطريقة متزامنة, أي الواحد تلو الآخر و ليس مع بعض.
ما فعلناه ببساطة هو إنشاء كائن من الكلاس mutex, و من ثم استدعاء الدالة lock() منه قبل الكود الذي سينفذه الثريد و استدعاء الدالة unlock() منه بعد الكود الذي سينفذه الثريد.

مثال على المزامنة في السي بلاس بلاس

main.cpp
#include <iostream>
#include <thread>
#include <chrono>
#include <mutex>

using namespace std;

// لأننا سنستخدمه لجعل الثريدات التي ننشئها تعمل بشكل متزامن mutex هنا قمنا بإنشاء كائن من الكلاس
mutex mtx;

// txt هنا قمنا بتعريف الدالة التي سنمررها للثريد مع الإشارة إلى أنه يجب تمرير نص لها مكان الباراميتر
void func(string txt)
{
	// حتى يجعل أي ثريد آخر يريد تنفيذ الكود ينتظر mtx من الكائن lock() هنا قمنا باستدعاء الدالة
    mtx.lock();

    cout << "Starting " << txt << "\n";

	// هنا قمنا بإنشاء حلقة تفوم بطباعة إسم الثريد الذي يتم تنفيذه حالياً 3 مرات مع التوقف لمدة ثانية في كل مرة
    for(int i=0; i<5; i++)
    {
        cout << txt << "\n";
        this_thread::sleep_for(chrono::seconds(1));
    }

    cout << "Ending " << txt << "\n";

	// حتى يجعل أي ثريد آخر موضوع في الإنتظار قادر على البدء بتنفيذ أوامر الدالة mtx من الكائن unlock() هنا قمنا باستدعاء الدالة
    mtx.unlock();
}

int main()
{
    // txt و كل واحد منهما يمرر لها نص مختلف مكان الباراميتر func() ينفذان الدالة thread هنا قمنا بإنشاء كائنين من الكلاس
    thread t1(func, "Thread-1");
    thread t2(func, "Thread-2");

    // المترجم ينتظر t2 و t1 من الكائنين join() هنا وضعنا قمنا باستدعاء الدالة
    // قبل أن يتابع تنفيذ باقي الأوامر الموجودة في الملف t1 و t1 أن يتوقف الكائنين
    t1.join();
    t2.join();

    cout << "All threads are end!";

    return 0;
}
		

سنحصل على النتيجة التالية عند التشغيل.

Starting Thread-1 &nbsp &nbsp &nbsp <-- هنا تم البدء بتنفيذ الثريد الأول
Thread-1
Thread-1
Thread-1
Ending Thread-1 &nbsp &nbsp &nbsp &nbsp <-- هنا إنتهى تنفيذ أوامر الثريد الأول
Starting Thread-2 &nbsp &nbsp &nbsp <-- هنا تم البدء بتنفيذ الثريد الثاني
Thread-2
Thread-2
Thread-2
Ending Thread-2 &nbsp &nbsp &nbsp &nbsp <-- هنا إنتهى تنفيذ أوامر الثريد الثاني
Both threads are end!