> ## Content Index
> Fetch the complete content index at: https://arabroot.io/llms.txt
> Use this file to discover other available public pages before exploring further.

# التزامن Concurrency والخيوط: ماذا يعني لسيرفرك وتطبيقاتك
- URL: https://arabroot.io/articles/المعالج-والخيوط-والتزامن-للمبتدئين/
- Published: 2026-10-05T10:30:00.000Z
- Updated: 2026-10-09T09:42:05.000Z
- Description: لماذا يكون المعالج عند 100% والتطبيق بطيئاً، ولا تسرع الخيوط كود Python، وكيف تضبط عدد العمال وحدود ال Containers.
- Author: فريق عرب رووت
- Tags: الأدلة التقنية, أساسيات الحاسب والبرمجة, لينكس والسيرفرات, DevOps وCI/CD, الاستضافة الذاتية

تشتري خادماً افتراضياً VPS فيه أربعة معالجات افتراضية vCPU، وتنشر عليه تطبيقك، ثم تفتح `top` في يوم مزدحم فتجد المعالج عند 100% والتطبيق بطيء، أو تجد متوسط الحمل Load Average يقول 8 على سيرفر فيه أربع أنوية فقط ولا تعرف هل هذه كارثة أم أمر عادي. وفي يوم آخر يشكو تطبيقك في Container من البطء والسيرفر نفسه شبه فارغ، أو تكتشف أن تطبيق Node.js يستخدم نواة واحدة من ثمانية، أو أن الخيوط Threads التي أضفتها لكود Python لم تسرع الحساب أبداً، أو تقرأ في السجلات Logs أن قاعدة البيانات Database ترفض الاتصالات لأن عددها تجاوز الحد.

وكل هذه الأعراض لها سبب واحد في الأصل، وهو أن أغلبنا يتعامل مع المعالج CPU كرقم في عرض المزود، بينما الذي يحدث فعلاً أن نظام التشغيل يوزع الوقت على عمليات Processes وخيوط Threads تتنافس على أنوية محدودة، وأن طريقة كتابة التطبيق تحدد هل يستفيد من هذه الأنوية أم يبقى ينتظر. وهذا المقال يشرح ما يحدث فعلاً من العتاد حتى إعدادات تطبيقاتك، مع تجارب حقيقية في Docker تقيس كل فكرة بالأرقام.

وسوف نناقش في هذا المقال ما يلي:

- ما الذي يوجد داخل المعالج: المقبس Socket والنواة Core والخيط العتادي Hardware Thread، وما هو ال vCPU الذي تشتريه، وكيف تقرأ `lscpu` و`nproc`.
- كيف يوزع Linux الوقت: العملية والخيط، والمجدول Scheduler، وتبديل السياق Context Switch، وما الذي يحسبه متوسط الحمل فعلاً.
- الفرق بين التزامن Concurrency والتوازي Parallelism، وطرق البرمجة المختلفة: الخيوط المتعددة Multithreading، والعمليات المتعددة Multiprocessing، وحلقة الأحداث Event Loop، والخيوط الافتراضية Virtual Threads، وقفل GIL في Python.
- تجارب بالأرقام على Python وNode.js وJava تبين متى يفيد كل نموذج ومتى لا يفيد.
- ما يعنيه ذلك لتطبيقاتك: عدد العمال Workers في Gunicorn وPHP-FPM وNginx وNode.js، واتصالات قاعدة البيانات، والمهام الخلفية.
- حدود المعالج في ال Containers، والخنق Throttling الذي يبطئ تطبيقك والسيرفر فارغ، وكيف تكتشف كل لغة عدد المعالجات المتاحة لها.
- أدوات القياس، وكيف تختار عدد ال vCPU لسيرفرك، ثم الأخطاء الشائعة وقائمة تحقق.

## أعراض تراها على سيرفرك ولا تفهم سببها

قبل الشرح لنضع الأعراض في جدول واحد، والسبب أن كل عرض منها له تفسير في جزء محدد من المقال، فإذا كنت جئت من أجل مشكلة معينة فابدأ من الجزء الذي يخصها:

| ما تراه                                              | السبب الغالب                                                                       | أين تجد الشرح                                            |
| ---------------------------------------------------- | ---------------------------------------------------------------------------------- | -------------------------------------------------------- |
| المعالج عند 100% والتطبيق بطيء على VPS بأربعة vCPU   | عمل حسابي أكثر من الأنوية، أو وقت سرقة Steal Time من جيرانك على نفس السيرفر الفعلي | [ال vCPU في الخادم الافتراضي](#vcpu) و[القياس](#measure) |
| متوسط الحمل 8 على أربع أنوية                         | قد يكون طابوراً على المعالج، وقد يكون عمليات تنتظر القرص                           | [متوسط الحمل](#load-average)                             |
| Container بطيء والسيرفر شبه فارغ                     | الخنق Throttling بسبب حد \--cpus                                                   | [ال Containers وحدود المعالج](#containers)               |
| تطبيق Node.js يستخدم نواة واحدة من ثمانية            | حلقة الأحداث تعمل في خيط واحد                                                      | [حلقة الأحداث](#event-loop) و[عمال الويب](#web-workers)  |
| خيوط Python لا تسرع العمل الحسابي                    | قفل GIL                                                                            | [Python وقفل GIL](#python-gil)                           |
| FATAL: sorry, too many clients already من PostgreSQL | عدد العمال والخيوط في التطبيق أكبر من اتصالات قاعدة البيانات                       | [اتصالات قاعدة البيانات](#db-connections)                |

## ما الذي يوجد داخل المعالج؟

### المقبس والنواة والخيط العتادي

المعالج CPU هو الشريحة التي تنفذ أوامر البرامج، وتركب في السيرفر في مقبس Socket على اللوحة الأم، وسيرفرات مراكز البيانات فيها غالباً مقبس أو اثنان. وداخل الشريحة عدة أنوية Cores، وكل نواة معالج كامل بذاته يستطيع أن ينفذ برنامجاً مستقلاً عن بقية الأنوية، وبالتالي فالمعالج الذي فيه أربع أنوية يستطيع أن ينفذ أربعة أشياء في نفس اللحظة فعلاً.

ثم جاءت تقنية تعدد الخيوط المتزامن Simultaneous Multithreading واختصاراً SMT، وتسميها Intel باسم Hyper-Threading، وفكرتها أن النواة الواحدة تظهر لنظام التشغيل كمعالجين منطقيين Logical CPUs، أي خيطين عتاديين Hardware Threads. والسبب أن وحدات التنفيذ داخل النواة تبقى معطلة جزءاً من الوقت وهي تنتظر بيانات من الذاكرة، فإذا كان هناك خيط ثان جاهز استغل هذه الفراغات. ولكن لاحظ أن الخيطين يتقاسمان نفس وحدات التنفيذ، لذلك فالخيط العتادي الثاني لا يساوي نواة ثانية، والمكسب الحقيقي منه يختلف بحسب نوع العمل، فقد يكون كبيراً في عمل كثير الانتظار للذاكرة، وقد يكون قريباً من الصفر في حساب مكثف يشغل وحدات التنفيذ كلها.

وقد يتساءل البعض: لماذا لا نقارن المعالجات بالسرعة Clock Speed فقط، أي 4.2 GHz مقابل 3.0 GHz؟ والإجابة أن السرعة هي عدد الدورات في الثانية، أما كمية العمل في كل دورة Instructions Per Cycle واختصاراً IPC فتختلف من جيل لآخر، لذلك قد يكون معالج حديث بسرعة 3 GHz أسرع من معالج قديم بسرعة 4 GHz، وأفضل مقارنة هي قياس تطبيقك نفسه على الخطتين. ويبقى عنصر أخير هو الذاكرة المؤقتة Cache داخل المعالج: L1 وL2 لكل نواة وهي صغيرة وسريعة جداً، وL3 مشتركة بين الأنوية وهي أكبر وأبطأ قليلاً، وكلها أسرع بكثير من الذاكرة RAM، ولهذا السبب سوف ترى لاحقاً أن نقل خيط من نواة لأخرى ليس مجانياً.

![مخطط سيرفر فعلي فيه مقبس واحد يحتوي أربع أنوية، في كل نواة خيطان عتاديان مرقمان CPU 0 إلى CPU 7 وذاكرة L1 وL2، وتحتها ذاكرة L3 مشتركة، وبجانبه ثلاثة خوادم افتراضية مجموع معالجاتها الافتراضية عشرة تمر عبر Hypervisor إلى ثمانية خيوط عتادية، مع توضيح أن انتظار vCPU لخيط مشغول هو وقت السرقة st](https://arabroot.io/content/images/2026/10/cpu-concurrency-01-cpu-layers.webp)

المقبس فيه أنوية، والنواة فيها خيطان عتاديان، وال vCPU خيط يجدوله ال Hypervisor على أحدها

### ال vCPU في الخادم الافتراضي

عندما تشتري VPS بأربعة vCPU فأنت لا تحصل على أربع أنوية فعلية، وإنما على أربعة معالجات افتراضية، وكل واحد منها في الغالب يعمل كخيط عادي على سيرفر المزود، ويجدوله ال Hypervisor مثل KVM على أي خيط عتادي متاح، وبالتالي فال vCPU الواحد يقابل غالباً خيطاً عتادياً واحداً وليس نواة كاملة. وكما شرحنا في دليل [مصطلحات الاستضافة الذاتية](https://arabroot.io/articles/%D9%85%D8%B5%D8%B7%D9%84%D8%AD%D8%A7%D8%AA-%D8%A7%D9%84%D8%A7%D8%B3%D8%AA%D8%B6%D8%A7%D9%81%D8%A9-%D8%A7%D9%84%D8%B0%D8%A7%D8%AA%D9%8A%D8%A9-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/) فالآلة الافتراضية Virtual Machine تحمل نظام تشغيل كاملاً فوق ال Hypervisor، ولذلك يرى Linux داخلها هذه المعالجات الافتراضية وكأنها معالجات حقيقية.

والمزود يبيع عادة ال vCPU بطريقتين: المشترك Shared، وفيه يبيع على السيرفر الفعلي معالجات افتراضية أكثر من خيوطه العتادية لأن أغلب العملاء لا يستخدمون المعالج طوال الوقت، والمخصص Dedicated، وفيه يحجز لك خيوطاً عتادية لا يشاركك فيها أحد بسعر أعلى. وفي الخطة المشتركة إذا احتاج جيرانك المعالج في نفس الوقت الذي تحتاجه أنت انتظر ال vCPU الخاص بك حتى يفرغ خيط عتادي، وهذا الانتظار يسجله Linux داخل سيرفرك باسم وقت السرقة Steal Time، ويظهر في عمود `st` في `top` و`vmstat`. وقد شرحنا في دليل [كيف تختار خادماً افتراضياً](https://arabroot.io/articles/%D9%83%D9%8A%D9%81-%D8%AA%D8%AE%D8%AA%D8%A7%D8%B1-%D8%AE%D8%A7%D8%AF%D9%85%D8%A7-%D8%A7%D9%81%D8%AA%D8%B1%D8%A7%D8%B6%D9%8A%D8%A7-vps/) متى يكفيك المشترك ومتى تحتاج المخصص، وسوف نعود لقراءة هذا العمود في [جزء القياس](#measure).

### كيف تقرأ lscpu وnproc؟

الأمر [lscpu](https://man7.org/linux/man-pages/man1/lscpu.1.html?ref=arabroot.io) يعرض ما يراه Linux من المعالج، والأمر [nproc](https://man7.org/linux/man-pages/man1/nproc.1.html?ref=arabroot.io) يعرض عدد المعالجات المتاحة للعملية الحالية. والمثال التالي يعرض الأسطر المهمة فقط من مخرج `lscpu`:

```bash
lscpu | grep -E '^(CPU\(s\)|Model name|Thread|Core|Socket|Hypervisor|L1d|L2|L3)'
nproc
```

والمخرج سوف يكون كما يلي:

```text
CPU(s):                               8
Model name:                           Intel(R) Core(TM) i7-7700K CPU @ 4.20GHz
Thread(s) per core:                   2
Core(s) per socket:                   4
Socket(s):                            1
Hypervisor vendor:                    Microsoft
L1d cache:                            128 KiB (4 instances)
L2 cache:                             1 MiB (4 instances)
L3 cache:                             8 MiB (1 instance)
8
```

في المخرج أعلاه لاحظ التالي:

- عدد المعالجات المنطقية `CPU(s)` هو حاصل ضرب الأسطر الثلاثة: مقبس واحد في أربع أنوية في خيطين لكل نواة، أي ثمانية.
- السطر `Hypervisor vendor` يعني أن هذا النظام نفسه آلة افتراضية، وعلى VPS سوف تجد فيه غالباً `KVM`، وعندها يكون `Thread(s) per core` و`Core(s) per socket` ما يعرضه المزود على الآلة الافتراضية، وقد لا يطابق العتاد الحقيقي تحته.
- الذاكرة L1 وL2 أربع نسخ أي واحدة لكل نواة، وL3 نسخة واحدة مشتركة، تماماً كما في المخطط أعلاه.
- `nproc` أعاد 8، ولكن تذكر هذا الرقم، فسوف ترى في [جزء ال Containers](#containers) أنه يبقى 8 حتى داخل Container مسموح له بمعالج واحد فقط.

## كيف يوزع نظام التشغيل العمل على المعالج؟

### العملية Process والخيط Thread

العملية Process هي برنامج يعمل، ولها مساحة ذاكرة خاصة بها لا تراها العمليات الأخرى، وفيها الكود والبيانات والملفات المفتوحة، أما الخيط Thread فهو مسار تنفيذ داخل العملية، وكل عملية تبدأ بخيط واحد ويمكنها أن تنشئ خيوطاً أخرى. والفرق المهم أن خيوط العملية الواحدة تشترك في نفس الذاكرة، ولكل خيط مكدس Stack وسجلات Registers خاصة به فقط، وبالتالي فالخيوط تتبادل البيانات بسرعة لأنها تقرأ نفس المتغيرات، ولكنها تحتاج أقفالاً Locks حتى لا يكتب خيطان على نفس الكائن في نفس اللحظة، وإذا انهار خيط بسبب خطأ في الذاكرة سقطت العملية كلها. أما العمليات المنفصلة فكل واحدة معزولة، فإذا انهارت واحدة بقيت الأخرى، ولكن كل واحدة تحمل نسختها من البيانات في الذاكرة، وتتواصل مع غيرها عبر Pipe أو Socket أو طابور Queue.

![مخطط عمليتين يفصل بينهما جدار، العملية A فيها ذاكرة مشتركة تشمل الكود وال heap عليه قفل والملفات المفتوحة واتصالات قاعدة البيانات، وتحتها ثلاثة خيوط لكل منها مكدس وسجلات خاصة، والعملية B لها ذاكرة خاصة لا تراها A وخيط واحد، وبينهما سهم يمثل التواصل عبر pipe أو socket أو queue، وتحت المخطط مقارنة بين مزايا الخيوط والعمليات وعيوبها](https://arabroot.io/content/images/2026/10/cpu-concurrency-02-process-thread.webp)

خيوط العملية الواحدة تشترك في ذاكرتها، والعمليات المنفصلة لا ترى ذاكرة بعضها

### المجدول وتبديل السياق

على سيرفرك عادة مئات الخيوط وعدد قليل من المعالجات المنطقية، والذي يقرر أي خيط يعمل على أي معالج وإلى متى هو المجدول Scheduler في نواة Linux. وأغلب هذه الخيوط نائمة تنتظر شيئاً، مثل طلب من الشبكة أو رد من القرص، أما الخيوط الجاهزة للعمل فتقف في طابور التشغيل Run Queue، والمجدول يعطي كل واحد منها شريحة زمنية Time Slice ثم ينقل المعالج لغيره. وقد كان المجدول الافتراضي في Linux لسنوات هو [CFS](https://docs.kernel.org/scheduler/sched-design-CFS.html?ref=arabroot.io)، ومنذ الإصدار 6.6 بدأ الانتقال إلى [EEVDF](https://docs.kernel.org/scheduler/sched-eevdf.html?ref=arabroot.io)، والفكرة العامة واحدة في الحالتين وهي توزيع الوقت بعدل بين الخيوط الجاهزة.

وكل انتقال من خيط لآخر اسمه تبديل السياق Context Switch، وفيه يحفظ النظام سجلات الخيط الأول ويحمل سجلات الثاني، وقد يجد الخيط الجديد أن بياناته ليست في ال Cache فيجلبها من الذاكرة. وهذا التبديل سريع جداً، ولكنه يصبح مكلفاً عندما يتكرر عشرات الآلاف من المرات في الثانية لأنك أنشأت خيوطاً أكثر بكثير من المعالجات، وسوف تراه في عمود `cs` من مخرج `vmstat`.

### ما الذي يحسبه متوسط الحمل Load Average فعلاً؟

الأمر `uptime` يعرض ثلاثة أرقام هي متوسط الحمل في آخر دقيقة وخمس دقائق وخمس عشرة دقيقة، والتفسير الشائع أنه عدد العمليات التي تستخدم المعالج أو تنتظره، وبالتالي فإذا تجاوز عدد المعالجات فالسيرفر مشغول. وهذا التفسير ناقص في Linux تحديداً، والسبب أن Linux يحسب في المتوسط أيضاً الخيوط في حالة الانتظار غير القابل للمقاطعة Uninterruptible Sleep، وتظهر بالحرف `D` في `ps`، وهي غالباً خيوط تنتظر القرص أو نظام ملفات عبر الشبكة، كما يشرح [Brendan Gregg في مقاله عن متوسط الحمل في Linux](https://www.brendangregg.com/blog/2017-08-08/linux-load-averages.html?ref=arabroot.io) وصفحة [/proc/loadavg](https://man7.org/linux/man-pages/man5/proc%5Floadavg.5.html?ref=arabroot.io). لذلك فالحمل المرتفع قد يعني معالجاً مزدحماً، وقد يعني قرصاً بطيئاً والمعالج نصف فارغ.

والمثال التالي من سيرفر فيه ثمانية معالجات منطقية في وقت كان القرص فيه تحت ضغط كبير:

```bash
nproc
uptime
vmstat 1 5
```

والمخرج سوف يكون كما يلي:

```text
8
 22:29:22 up  1:30,  0 user,  load average: 14.78, 15.55, 16.50

procs -----------memory---------- ---swap-- -----io---- -system-- -------cpu-------
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st gu
12  2  45588 468364 1371660 26726772    0    8  1204 10286 2836   22 29 16 37 18  0  0
 5  3  45588 453568 1371732 26732188    0    0  3400 51388 2972 10492 28 37 17 18  0  0
 4  7  45588 439992 1371772 26741268    0    0  4080 60112 1525 6539 33 21 15 30  0  0
13  4  45588 453380 1371852 26756084    0    0  3920 31036  984 6948 41 32  8 20  0  0
 3  3  45588 490408 1371928 26765376    0    0  3480 35760 1787 8213 11 16 37 36  0  0
```

في المخرج أعلاه لاحظ التالي:

- متوسط الحمل قرابة 15 على ثمانية معالجات، ومن ينظر إليه وحده يظن أن المعالج مختنق.
- العمود `r` هو عدد الخيوط الجاهزة للعمل، والعمود `b` هو عدد الخيوط المحجوزة Blocked في انتظار القرص، ومتوسط الحمل يجمع الاثنين.
- العمود `id` يقول إن المعالج كان فارغاً بين 8% و37% من الوقت، والعمود `wa` يقول إن ما بين 18% و36% كان انتظاراً للقرص I/O Wait، والعمود `bo` يعرض كتابة عشرات الآلاف من الكتل في الثانية، لذلك فجزء كبير من هذا الحمل قرص وليس معالجاً.
- العمود `st` صفر، أي لا يوجد وقت سرقة، ولو كان مرتفعاً لكان السبب جيرانك على السيرفر الفعلي.

والقاعدة العملية أن تقارن متوسط الحمل بعدد المعالجات من `nproc`، ثم لا تحكم قبل أن تنظر في `us` و`sy` و`wa` و`st` وفي العمودين `r` و`b`، والسبب أن الرقم الواحد يجمع مشكلتين مختلفتين تماماً في العلاج.

### مهمة تحسب ومهمة تنتظر

قبل أن ننتقل لطرق البرمجة نحتاج تقسيماً واحداً سوف نعود إليه في كل قسم من المقال: المهمة المحدودة بالمعالج CPU-bound هي التي تقضي وقتها في الحساب، مثل تغيير حجم الصور وتوليد ملفات PDF وتشفير كلمات المرور بخوارزمية bcrypt والضغط Compression، وهذه لا يسرعها إلا معالج أكثر. أما المهمة المحدودة بالإدخال والإخراج I/O-bound فتقضي أغلب وقتها في الانتظار، مثل استعلام قاعدة البيانات وطلب API خارجي وإرسال البريد وقراءة الملفات، وفي وقت الانتظار هذا لا تستخدم المعالج أصلاً، لذلك يسرعها أن تتداخل فترات الانتظار، وليس أن تضيف أنوية. وأغلب تطبيقات الويب من النوع الثاني، فالطلب يصرف بضعة أجزاء من الثانية في الكود، ثم ينتظر قاعدة البيانات أضعاف ذلك.

## التزامن Concurrency والتوازي Parallelism: ما الفرق؟

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

![مخطط مطبخ، في جهة طباخ واحد وثلاثة طلبات A وB وC ينتقل بينها في شرائح زمنية متتالية فتنتهي بعد تسع شرائح، وفي الجهة الأخرى ثلاثة طباخين كل واحد يطبخ طلباً في نفس الوقت فتنتهي الطلبات بعد ثلاث شرائح، مع تعريف التزامن بأنه مهام قيد التنفيذ تتناوب، والتوازي بأنه مهام تعمل في نفس اللحظة وتحتاج أنوية](https://arabroot.io/content/images/2026/10/cpu-concurrency-03-concurrency-parallelism.webp)

طباخ واحد يتنقل بين الطلبات هو التزامن، وثلاثة طباخين في نفس الوقت هو التوازي

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

## طرق كتابة البرامج التي تعمل على أكثر من مهمة

### الخيوط المتعددة Multithreading

أول طريقة يفكر فيها المطور أن ينشئ خيطاً لكل مهمة، وفي السيرفرات خيطاً لكل طلب Thread per Request، وهذه هي الطريقة التقليدية في Java وفي Apache وكثير من الأطر. وهي بسيطة لأن الكود يكتب بالترتيب الطبيعي، فالخيط يطلب من قاعدة البيانات وينتظر الرد ثم يكمل، وأثناء انتظاره يعمل خيط آخر. ولكن مشكلتها أن كل خيط من خيوط نظام التشغيل يحجز ذاكرة لمكدسه ويكلف المجدول تبديلاً للسياق، لذلك لا تستطيع أن تنشئ مئة ألف خيط لمئة ألف اتصال مفتوح، وتنتهي في الواقع بمجمع خيوط Thread Pool بحجم ثابت، وإذا امتلأ وقفت الطلبات الجديدة في الطابور حتى لو كان المعالج فارغاً.

### العمليات المتعددة Multiprocessing

الطريقة الثانية أن تشغل عدة نسخ من البرنامج كعمليات منفصلة، وهذا ما يفعله Gunicorn مع عماله Workers وPHP-FPM مع عملياته الفرعية وNginx مع `worker_processes`. ونقاط قوتها أن كل عملية تعمل على نواة مستقلة دون أقفال مشتركة، وأن انهيار عامل لا يسقط البقية، أما نقطة ضعفها فهي الذاكرة، فكل عامل يحمل نسخته من التطبيق ومكتباته، وعشرون عاملاً لتطبيق Django أو Laravel قد تعني عدة جيجابايت من الذاكرة.

### حلقة الأحداث Event Loop في Node.js

الطريقة الثالثة لا تنتظر أصلاً، فبدلاً من أن يقف الخيط حتى يصل رد قاعدة البيانات، يسجل البرنامج دالة Callback تعمل عندما يصل الرد ثم ينتقل فوراً لطلب آخر، ويتولى النظام مراقبة آلاف الاتصالات المفتوحة معاً عبر `epoll`، وحلقة الأحداث تأخذ كل رد يصل وتنفذ الدالة الخاصة به. وبهذا الشكل يخدم خيط واحد آلاف الطلبات التي تنتظر بذاكرة قليلة، وهذا هو نموذج [Node.js](https://nodejs.org/en/learn/asynchronous-work/event-loop-timers-and-nexttick?ref=arabroot.io) وNginx و[asyncio في Python](https://docs.python.org/3/library/asyncio.html?ref=arabroot.io).

ولكن هناك مشكلة رئيسية في هذا النموذج، وهي أن الكود نفسه يعمل في خيط واحد، فإذا قام أحد الطلبات بعمل حسابي طويل توقفت الحلقة كلها ولم يخدم أي طلب آخر حتى ينتهي. والمثال التالي فيه مؤقت Timer يفترض أن يعمل كل 100 ms، وبعد 350 ms تبدأ حلقة حسابية مدتها ثانية ونصف في نفس الخيط، احفظه باسم `block.mjs`:

```javascript
// A timer that should fire every 100 ms, and a CPU-bound loop on the same thread
let last = performance.now();
const timer = setInterval(() => {
  const now = performance.now();
  console.log(`tick after ${Math.round(now - last)} ms`);
  last = now;
}, 100);

setTimeout(() => {
  console.log('CPU work starts');
  const end = Date.now() + 1500;
  while (Date.now() < end) {}          // 1.5 s of pure computation
  console.log('CPU work ends');
}, 350);

setTimeout(() => clearInterval(timer), 2400);
```

ثم شغله في Container من Node.js 24:

```bash
docker run --rm --name lab-cpu-node -v "$PWD":/lab -w /lab node:24-alpine node block.mjs
```

والمخرج سوف يكون كما يلي:

```text
tick after 101 ms
tick after 109 ms
tick after 104 ms
CPU work starts
CPU work ends
tick after 1574 ms
tick after 101 ms
tick after 105 ms
tick after 100 ms
tick after 101 ms
tick after 113 ms
```

لاحظ السطر `tick after 1574 ms`، فالمؤقت الذي يعمل كل 100 ms انتظر أكثر من ثانية ونصف، ولو كان هذا سيرفر ويب لانتظر كل زائر في تلك اللحظة نفس المدة، حتى من يطلب صفحة لا تحتاج أي حساب. لذلك القاعدة في Node.js أن العمل الحسابي الطويل لا يكون في حلقة الأحداث، وإنما في [Worker Threads](https://nodejs.org/api/worker%5Fthreads.html?ref=arabroot.io) أو في عامل خلفي منفصل. والنقطة الثانية أن عملية Node.js الواحدة تستخدم نواة واحدة لتنفيذ الكود، لذلك إذا أردت أن تستفيد من كل الأنوية فشغل عدة عمليات كما سيأتي في [جزء عمال الويب](#web-workers).

### الخيوط الافتراضية Virtual Threads وGoroutines

الطريقة الرابعة تجمع بساطة الخيوط وخفة حلقة الأحداث، وفكرتها أن الخيط الذي يراه المطور ليس خيطاً من نظام التشغيل، وإنما خيط خفيف تديره بيئة التشغيل Runtime، وعندما ينتظر الشبكة أو قاعدة البيانات تنزله البيئة عن خيط النظام وتضع مكانه خيطاً آخر جاهزاً. وبالتالي يكتب المطور كوداً عادياً ينتظر بالترتيب الطبيعي، وتعمل خلفه بضعة خيوط حقيقية تسمى Carrier Threads بعدد المعالجات. وهذا هو نموذج [Goroutines في Go](https://go.dev/doc/effective%5Fgo?ref=arabroot.io#goroutines) منذ بدايتها، وهو أيضاً [الخيوط الافتراضية في Java](https://openjdk.org/jeps/444?ref=arabroot.io) التي أصبحت ميزة نهائية في JDK 21 عبر JEP 444، وتقول وثيقتها إن الفائدة الأكبر تكون عندما يكون عدد المهام المتزامنة كبيراً، أي أكثر من بضعة آلاف، ولا يكون العمل محدوداً بالمعالج.

والمثال التالي ينفذ 10,000 مهمة، كل واحدة تنتظر ثانية كأنها تنتظر قاعدة البيانات، مرة على مجمع من 200 خيط من خيوط النظام، ومرة على خيوط افتراضية، احفظه باسم `Threads.java`:

```java
import java.time.Duration;
import java.util.concurrent.Executors;

public class Threads {
    static void run(String label, java.util.concurrent.ExecutorService pool) {
        long start = System.nanoTime();
        try (pool) {
            for (int i = 0; i < 10_000; i++) {
                pool.submit(() -> { Thread.sleep(Duration.ofSeconds(1)); return null; });  // waiting for I/O
            }
        }
        System.out.printf("%-22s %5.2f s%n", label, (System.nanoTime() - start) / 1e9);
    }

    public static void main(String[] args) {
        System.out.println("Java " + Runtime.version() + ", availableProcessors: " + Runtime.getRuntime().availableProcessors());
        run("200 platform threads", Executors.newFixedThreadPool(200));
        run("virtual threads", Executors.newVirtualThreadPerTaskExecutor());
    }
}
```

ثم شغله في Container حده معالجان:

```bash
docker run --rm --name lab-cpu-java --cpus=2 -v "$PWD":/lab -w /lab eclipse-temurin:25-jdk-alpine java Threads.java
```

والمخرج سوف يكون كما يلي:

```text
Java 25.0.4.1+1-LTS, availableProcessors: 2
200 platform threads   52.54 s
virtual threads         1.75 s
```

والنتيجة منطقية، فمجمع ال 200 خيط ينهي 200 مهمة في كل ثانية، أي يحتاج خمسين ثانية للعشرة آلاف، أما الخيوط الافتراضية فتنتظر كلها معاً على خيطين حقيقيين فقط. ولكن تذكر أن هذا المكسب في الانتظار وحده، فلو كانت المهام حساباً لما أسرعها شيء غير الأنوية، والخيوط الافتراضية في هذه الحالة لا تضيف شيئاً. ولاحظ أيضاً السطر الأول، فقد قرأت Java حد ال Container وأعادت `availableProcessors: 2`، وسوف نعود لذلك في [جزء اكتشاف عدد المعالجات](#runtime-detection).

![مخطط بثلاثة أعمدة، الأول خيط لكل طلب حيث يحجز كل طلب خيطاً وهو ينتظر PostgreSQL والطلب الخامس يقف في الطابور، والثاني حلقة أحداث بخيط واحد تأخذ الردود الجاهزة وتترك الانتظار للنواة عبر epoll مع حلقة حسابية حمراء أخرت مؤقتاً من 100 ms إلى 1574 ms، والثالث عشرة آلاف خيط افتراضي تعمل على خيطين حقيقيين مع نتيجة 52.54 ثانية لمئتي خيط مقابل 1.75 ثانية للخيوط الافتراضية](https://arabroot.io/content/images/2026/10/cpu-concurrency-04-server-models.webp)

خيط لكل طلب، وحلقة أحداث، وخيوط افتراضية فوق خيوط قليلة

### Python وقفل GIL

في التنفيذ المعتاد للغة Python المسمى CPython يوجد قفل عام اسمه [Global Interpreter Lock](https://docs.python.org/3/glossary.html?ref=arabroot.io#term-global-interpreter-lock) واختصاراً GIL، ولا يسمح إلا لخيط واحد بتنفيذ كود Python في أي لحظة. لذلك فالخيوط في Python تفيد في الانتظار، لأن الخيط يترك القفل وهو ينتظر الشبكة أو القرص، ولا تفيد في الحساب، لأن الخيوط الحسابية تتناوب على قفل واحد. والحل التقليدي للحساب هو [multiprocessing](https://docs.python.org/3/library/multiprocessing.html?ref=arabroot.io) أو `ProcessPoolExecutor`، أي عمليات منفصلة لكل منها مفسر وقفل خاص بها.

وقد اقترحت [PEP 703](https://peps.python.org/pep-0703/?ref=arabroot.io) نسخة من CPython دون هذا القفل تسمى Free-threaded، فظهرت نسخة تجريبية منها في Python 3.13، ثم أصبحت في Python 3.14 [مدعومة رسمياً عبر PEP 779](https://peps.python.org/pep-0779/?ref=arabroot.io)، ولكنها ما زالت نسخة اختيارية تنصبها بنفسك وليست الافتراضية، ويذكر [دليلها الرسمي](https://docs.python.org/3/howto/free-threading-python.html?ref=arabroot.io) أن الكود ذا الخيط الواحد يصبح فيها أبطأ بنسبة تتراوح بين 1% و8% بحسب النظام. ويجب أن تتأكد أن المكتبات التي تعتمد عليها، خاصة التي فيها كود C، تدعم هذه النسخة قبل أن تنقل إليها تطبيقاً حقيقياً، ونتيجة القياس في [التجربة العملية](#lab) أدناه.

## تجربة عملية: نقيس الفرق بأنفسنا

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

### مهمة حسابية: خيوط أم عمليات؟

البرنامج التالي ينفذ نفس الحساب أربع مرات بثلاث طرق: بالتتابع، ثم في أربعة خيوط، ثم في أربع عمليات، احفظه باسم `cpu_bound.py`:

```python
import os, sys, time
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor

def work(n):
    total = 0
    for i in range(n):
        total += i * i
    return total

def once(pool, tasks, n):
    start = time.perf_counter()
    if pool is None:
        for _ in range(tasks):
            work(n)
    else:
        with pool(max_workers=tasks) as ex:
            list(ex.map(work, [n] * tasks))
    return time.perf_counter() - start

def run(label, pool, tasks=4, n=5_000_000):
    best = min(once(pool, tasks, n) for _ in range(3))   # best of 3 runs
    print(f"{label:<14}{best:6.2f} s")

if __name__ == "__main__":
    print(sys.version.split()[0], "GIL:", sys._is_gil_enabled(), "cpu_count:", os.cpu_count())
    run("sequential", None)
    run("4 threads", ThreadPoolExecutor)
    run("4 processes", ProcessPoolExecutor)
```

```bash
docker run --rm --name lab-cpu-py -v "$PWD":/lab -w /lab python:3.13-slim python cpu_bound.py
```

والمخرج سوف يكون كما يلي:

```text
3.13.16 GIL: True cpu_count: 8
sequential      6.97 s
4 threads       6.52 s
4 processes     2.48 s
```

والخيوط الأربعة لم توفر شيئاً تقريباً بسبب قفل GIL، أما العمليات الأربع فأنهت العمل في قرابة ثلث الوقت، ولم تصل إلى الربع لأن إنشاء العمليات ونقل البيانات إليها له ثمن، ولأن الأنوية الأربع تشارك الجهاز مع برامج أخرى. والآن نكرر نفس البرنامج على Python 3.14 العادية والنسخة الحرة منها 3.14t، ونستخدم [uv](https://docs.astral.sh/uv/concepts/python-versions/?ref=arabroot.io) لتنزيل النسختين:

```bash
docker run --rm --name lab-cpu-ft -v "$PWD":/lab -w /lab python:3.13-slim sh -c '
  pip install -q uv
  uv python install -q 3.14 3.14t
  for v in 3.14 3.14t; do $(uv python find $v) cpu_bound.py; done'
```

والمخرج سوف يكون كما يلي:

```text
3.14.8 GIL: True cpu_count: 8
sequential      2.63 s
4 threads       2.33 s
4 processes     0.75 s
3.14.8 GIL: False cpu_count: 8
sequential      3.10 s
4 threads       1.38 s
4 processes     1.90 s
```

في المخرج أعلاه لاحظ التالي:

- مع القفل بقيت الخيوط قريبة من التتابع، والعمليات هي التي أسرعت، تماماً كما في 3.13.
- في النسخة الحرة `GIL: False` أصبحت الخيوط الأربعة أسرع من التتابع بأكثر من الضعف، وهذا هو الهدف من PEP 703.
- التتابع في النسخة الحرة أبطأ من النسخة العادية، وهذا ثمن إزالة القفل الذي ذكره الدليل الرسمي، وأرقام العمليات هنا تتأثر بالحمل على الجهاز أكثر من غيرها، فلا تبن عليها قراراً من محاولة واحدة.
- الفرق الكبير بين أزمنة 3.13 و3.14 يرجع إلى اختلاف الإصدار وطريقة بنائه، ولا علاقة له بموضوعنا، لذلك قارن دائماً داخل الإصدار الواحد.

### مهمة تنتظر: التتابع أم الخيوط أم asyncio؟

البرنامج التالي ينفذ عشرين انتظاراً مدة كل واحد نصف ثانية كأنها استعلامات لقاعدة البيانات، احفظه باسم `io_bound.py`:

```python
import asyncio, time
from concurrent.futures import ThreadPoolExecutor

def fake_query():
    time.sleep(0.5)          # waiting for a database or an API

async def fake_query_async():
    await asyncio.sleep(0.5)

def timed(label, fn):
    start = time.perf_counter()
    fn()
    print(f"{label:<14}{time.perf_counter() - start:6.2f} s")

def sequential():
    for _ in range(20):
        fake_query()

def threads():
    with ThreadPoolExecutor(max_workers=20) as ex:
        for _ in range(20):
            ex.submit(fake_query)

async def gather():
    await asyncio.gather(*(fake_query_async() for _ in range(20)))

timed("sequential", sequential)
timed("20 threads", threads)
timed("asyncio", lambda: asyncio.run(gather()))
```

```bash
docker run --rm --name lab-cpu-py -v "$PWD":/lab -w /lab python:3.13-slim python io_bound.py
```

والمخرج سوف يكون كما يلي:

```text
sequential     10.01 s
20 threads      0.56 s
asyncio         0.50 s
```

وهنا انعكست الصورة، فالخيوط العشرون أسرعت العمل عشرين مرة تقريباً رغم وجود القفل، والسبب أن الخيط يترك القفل وهو ينتظر، أما asyncio فوصلت لنفس النتيجة بخيط واحد. ونفس الفكرة في Node.js، احفظ الملف التالي باسم `io.mjs`:

```javascript
const wait = (ms) => new Promise((r) => setTimeout(r, ms));   // a database or API call

let start = performance.now();
for (let i = 0; i < 20; i++) await wait(500);
console.log(`sequential     ${((performance.now() - start) / 1000).toFixed(2)} s`);

start = performance.now();
await Promise.all(Array.from({ length: 20 }, () => wait(500)));
console.log(`Promise.all    ${((performance.now() - start) / 1000).toFixed(2)} s`);
```

```bash
docker run --rm --name lab-cpu-node -v "$PWD":/lab -w /lab node:24-alpine node io.mjs
```

والمخرج سوف يكون كما يلي:

```text
sequential     10.04 s
Promise.all    0.50 s
```

وهذا من أشهر الأخطاء في كود Node.js، أي أن يكتب المطور `await` داخل حلقة `for` لطلبات لا يعتمد بعضها على بعض، فيحول التزامن الذي يقدمه له Node.js إلى تتابع. أما العمل الحسابي في Node.js فيحتاج خيوطاً حقيقية، والملف التالي يقارن أربع مهام حسابية بالتتابع مع أربعة Worker Threads، احفظه باسم `workers.mjs`:

```javascript
import { Worker, isMainThread, parentPort } from 'node:worker_threads';
import os from 'node:os';

const work = (n) => { let t = 0; for (let i = 0; i < n; i++) t += i % 7; return t; };
const N = 400_000_000;

if (!isMainThread) {
  parentPort.postMessage(work(N));
} else {
  console.log('availableParallelism:', os.availableParallelism());
  let start = performance.now();
  for (let i = 0; i < 4; i++) work(N);
  console.log(`sequential     ${((performance.now() - start) / 1000).toFixed(2)} s`);

  start = performance.now();
  await Promise.all(Array.from({ length: 4 }, () => new Promise((resolve) => {
    new Worker(new URL(import.meta.url)).on('message', resolve);
  })));
  console.log(`4 workers      ${((performance.now() - start) / 1000).toFixed(2)} s`);
}
```

```bash
docker run --rm --name lab-cpu-node -v "$PWD":/lab -w /lab node:24-alpine node workers.mjs
```

والمخرج سوف يكون كما يلي:

```text
availableParallelism: 8
sequential     13.48 s
4 workers      5.01 s
```

والمخطط التالي يجمع النتائج كلها في قاعدة واحدة تختار بها النموذج المناسب لعملك:

![مخطط يقارن المهمة المحدودة بالمعالج التي تحسب طوال الوقت ولا يسرعها إلا أنوية أكثر، مع نتائج Python بالتتابع 6.97 ثانية والخيوط مع GIL 6.52 والعمليات 2.48 والخيوط في 3.14t دون GIL 1.38، والمهمة المحدودة بالانتظار التي تنتظر قاعدة البيانات، مع نتائج التتابع 10.01 ثانية والخيوط 0.56 وasyncio وPromise.all نصف ثانية، وتحت كل منهما النماذج المناسبة لها](https://arabroot.io/content/images/2026/10/cpu-concurrency-06-cpu-io-bound.webp)

المهمة التي تحسب تحتاج أنوية، والمهمة التي تنتظر تحتاج تداخل الانتظار

## ماذا يعني هذا لتطبيقاتك على السيرفر؟

### عمال الويب Workers والخيوط

كل خادم تطبيقات تقريباً يسألك سؤالاً واحداً بصيغ مختلفة: كم عاملاً تريد؟ والإجابة تبدأ من نوع العمل، ثم من الذاكرة المتاحة، ثم من الأرقام الرسمية لكل أداة:

- **Gunicorn**: يقترح [دليل تصميمه](https://gunicorn.org/design/?ref=arabroot.io) البدء بالمعادلة `(2 × CPU cores) + 1`، ثم التعديل بحسب اختبار الحمل، ويذكر أن 4 إلى 12 عاملاً تكفي غالباً لحمل كبير. والضرب في اثنين مبني على أن العامل يقضي جزءاً من وقته في الانتظار، فإذا كان التطبيق يحسب طوال الوقت فلن يفيدك أن يزيد العمال عن الأنوية. ويمكنك مع العامل `gthread` أن تجمع عمالاً وخيوطاً، فالخيوط أخف على الذاكرة والعمال أفضل في العزل.
- **PHP-FPM**: الإعداد [pm.max\_children](https://www.php.net/manual/en/install.fpm.configuration.php?ref=arabroot.io) هو الحد الأعلى للعمليات، أي الحد الأعلى للطلبات التي تخدم في نفس الوقت، و`pm` يحدد الطريقة: `static` عدد ثابت، و`dynamic` عدد يتغير بين حدين، و`ondemand` ينشئ العمليات عند الطلب. وطريقة الحساب العملية أن تقسم الذاكرة التي تخصصها ل PHP على استهلاك العملية الواحدة كما تراه في `ps` أو `docker stats`، والسبب أن الذاكرة تنفد هنا قبل المعالج غالباً.
- **Nginx**: القيمة [worker\_processes auto](https://nginx.org/en/docs/ngx%5Fcore%5Fmodule.html?ref=arabroot.io#worker%5Fprocesses) تجعله يكتشف عدد الأنوية ويشغل عاملاً لكل واحدة، وكل عامل حلقة أحداث تخدم آلاف الاتصالات، ولاحظ أن القيمة الافتراضية إذا لم تكتب شيئاً هي `1`.
- **Node.js**: العملية الواحدة تستخدم نواة واحدة للكود كما رأيت، فإذا أردت الأنوية كلها فشغل عدة عمليات عبر [وحدة cluster](https://nodejs.org/api/cluster.html?ref=arabroot.io) أو عبر [وضع cluster في PM2](https://pm2.keymetrics.io/docs/usage/cluster-mode/?ref=arabroot.io)، أو شغل عدة Containers خلف ال Reverse Proxy. واحسب العدد من [os.availableParallelism()](https://nodejs.org/api/os.html?ref=arabroot.io#osavailableparallelism) وليس من `os.cpus().length`، مع الانتباه لما سوف تراه في جزء ال Containers.

### اتصالات قاعدة البيانات

لنفرض أن لديك تطبيق Python فيه 9 عمال Gunicorn، وكل عامل فيه 4 خيوط، وكل خيط يفتح اتصالاً بقاعدة البيانات، ثم أضفت عاملين للمهام الخلفية في كل واحد 10 خيوط، وأنت الآن تحتاج 56 اتصالاً، وعند النشر تعمل النسخة الجديدة بجانب القديمة دقيقة، فيصبح الرقم 112\. وقيمة [max\_connections](https://www.postgresql.org/docs/current/runtime-config-connection.html?ref=arabroot.io) الافتراضية في PostgreSQL هي 100 عادة، لذلك تظهر فجأة الرسالة التالية في سجلات التطبيق، وهي من تجربة على PostgreSQL 17 بحد خمسة اتصالات:

```text
psql: error: connection to server on socket "/var/run/postgresql/.s.PGSQL.5432" failed: FATAL:  sorry, too many clients already
```

والحل البسيط أن ترفع `max_connections` إلى 1000، وهو حل غير مناسب، والسبب أن كل اتصال في PostgreSQL عملية كاملة على سيرفر قاعدة البيانات لها ذاكرتها، وأن ألف اتصال يعمل منها مئة في نفس الوقت على أربع أنوية لن تنهي العمل أسرع، وإنما تزيد التنافس والأقفال وتبديل السياق. والحل الأنسب أن تحسب الاتصالات كما فعلنا أعلاه، وأن تحدد حجم مجمع الاتصالات Connection Pool في التطبيق بحيث يبقى المجموع أقل من الحد، ثم إذا كثرت التطبيقات والعمال فضع أمام قاعدة البيانات [PgBouncer](https://www.pgbouncer.org/features.html?ref=arabroot.io)، فهو يستقبل اتصالات كثيرة من التطبيقات ويمررها على عدد قليل من الاتصالات الحقيقية، وفي [وضع transaction](https://www.pgbouncer.org/config.html?ref=arabroot.io) يعيد الاتصال للمجمع بعد كل معاملة بدلاً من أن يحجزه طوال جلسة العميل، وقد شرحنا حساب حجم المجمع وإعداد PgBouncer بالتفصيل في مقال [تجميع الاتصالات Connection Pooling للمبتدئين](https://arabroot.io/articles/%D8%AA%D8%AC%D9%85%D9%8A%D8%B9-%D8%A7%D9%84%D8%A7%D8%AA%D8%B5%D8%A7%D9%84%D8%A7%D8%AA-connection-pooling-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/).

### المهام الخلفية Background Jobs والطوابير

إذا كان في تطبيقك عمل حسابي ثقيل، مثل تصدير تقرير كبير أو تحويل فيديو أو توليد ملفات PDF، فلا تتركه داخل الطلب، والسبب أنه يحجز عامل الويب طوال مدته، وقد يتجاوز مهلة ال Reverse Proxy فيعود للمستخدم خطأ `504` كما شرحنا في دليل [تطبيقك خلف Reverse Proxy](https://arabroot.io/articles/%D8%AA%D8%B7%D8%A8%D9%8A%D9%82%D9%83-%D8%AE%D9%84%D9%81-reverse-proxy/). والحل أن يكتب الطلب المهمة في طابور Queue ويعود فوراً، ثم يأخذها عامل خلفي Worker مستقل، وقد شرحنا هذا النمط بالتفصيل مع إعادة المحاولة في دليل [البريد التفاعلي](https://arabroot.io/articles/%D8%A7%D9%84%D8%A8%D8%B1%D9%8A%D8%AF-%D8%A7%D9%84%D8%AA%D9%81%D8%A7%D8%B9%D9%84%D9%8A-transactional-email/). والفائدة هنا أنك تحدد عدد العمال الخلفيين بحسب نوع العمل: عامل لكل نواة تقريباً للمهام الحسابية، وعدد أكبر للمهام التي تنتظر خدمات خارجية، دون أن يؤثر ذلك على سرعة صفحات الموقع.

### لماذا لا تكون الخيوط الأكثر أسرع دائماً؟

قد تبدو الفكرة من الوهلة الأولى بسيطة: إذا كانت أربعة خيوط أسرع من خيط واحد، فأربعون أسرع من أربعة. ولكن في الواقع فهي غير كذلك، والأسباب أربعة:

- **الأنوية محدودة**: في العمل الحسابي لا يعمل في نفس اللحظة أكثر من عدد المعالجات المنطقية، والبقية تنتظر في الطابور وتضيف تبديلاً للسياق.
- **التنافس على الأقفال Lock Contention**: إذا كانت الخيوط تكتب على نفس البيانات، فهي تقف في طابور أمام القفل، وقد تصبح أبطأ من خيط واحد.
- **الذاكرة**: كل عامل يحمل نسخة من التطبيق، وكل خيط يحجز مكدساً، وعندما تمتلئ الذاكرة يبدأ النظام في استخدام ال swap أو ينهي عمليات، وهذا أسوأ من أي بطء في المعالج.
- **الطرف الآخر**: إذا كانت الخيوط تنتظر قاعدة البيانات، فإن زيادتها تنقل الطابور من التطبيق إلى قاعدة البيانات، ولا تزيد عدد الاستعلامات التي تنتهي في الثانية.

## ال Containers وحدود المعالج

### الخيار --cpus والخيار --cpuset-cpus

يعطيك Docker طريقتين لتحديد المعالج، كما في [صفحة قيود الموارد](https://docs.docker.com/engine/containers/resource%5Fconstraints/?ref=arabroot.io). الأولى `--cpus=1.5`، وهي حد للوقت وليست حداً للأنوية، فال Container يستطيع أن يعمل على كل أنوية السيرفر، ولكن مجموع ما يستخدمه في كل فترة Period مدتها 100 ms لا يتجاوز حصة Quota قدرها 150 ms. وهذا ما يسميه Linux [التحكم في عرض النطاق للمجدول CFS Bandwidth Control](https://docs.kernel.org/scheduler/sched-bwc.html?ref=arabroot.io)، وعندما تنفد الحصة يوقف النظام خيوط ال Container حتى تبدأ الفترة التالية، وهذا هو الخنق Throttling. أما الطريقة الثانية `--cpuset-cpus=0,1` فتحدد معالجات منطقية بعينها، فلا يعمل ال Container إلا عليها.

والمثال التالي يقارن ما يراه ال Container في الحالتين:

```bash
docker run --rm --name lab-cpu-limit --cpus=1.5 ubuntu:24.04 sh -c "nproc; cat /sys/fs/cgroup/cpu/cpu.cfs_quota_us /sys/fs/cgroup/cpu/cpu.cfs_period_us"
docker run --rm --name lab-cpu-set --cpuset-cpus=0,1 ubuntu:24.04 nproc
```

والمخرج سوف يكون كما يلي:

```text
8
150000
100000
2
```

لاحظ أن `nproc` أعاد 8 مع `--cpus=1.5`، لأنه يقرأ المعالجات المسموح للعملية أن تعمل عليها وليس حصة الوقت، بينما أعاد 2 مع `--cpuset-cpus`. والسطران بعده هما الحصة والفترة بالميكروثانية، أي 150 ms من وقت المعالج في كل 100 ms. وهذه الملفات من الإصدار الأول cgroup v1، أما أغلب السيرفرات الحديثة فتستخدم الإصدار الثاني cgroup v2، وهو الافتراضي منذ Ubuntu 21.10 وDebian 11 كما تذكر [وثائق Kubernetes](https://kubernetes.io/docs/concepts/architecture/cgroups/?ref=arabroot.io)، وتعرف إصدار نظامك بالأمر:

```bash
stat -fc %T /sys/fs/cgroup/
```

فإذا كان المخرج `cgroup2fs` فنظامك يستخدم الإصدار الثاني، وتجد الحصة والفترة معاً في الملف `/sys/fs/cgroup/cpu.max` بالصيغة `150000 100000`، والعدادات في `/sys/fs/cgroup/cpu.stat` باسم `nr_periods` و`nr_throttled` و`throttled_usec`، كما في [وثيقة cgroup v2](https://docs.kernel.org/admin-guide/cgroup-v2.html?ref=arabroot.io). وإذا كان المخرج `tmpfs` فهو الإصدار الأول، والملفات كما في المثال أعلاه.

### تجربة الخنق: Container بطيء والسيرفر فارغ

سوف نقوم الآن بتشغيل اثنين من ال Containers لكل منهما حد معالج واحد، الأول فيه خيط حسابي واحد، والثاني فيه أربعة خيوط حسابية، ونستخدم [stress-ng](https://github.com/stress-ng/stress-ng?ref=arabroot.io) لتوليد الحمل، ونضيفه مع أدوات القياس في Image صغيرة عبر ملف `Dockerfile` التالي:

```dockerfile
FROM ubuntu:24.04
RUN apt-get update && apt-get install -y --no-install-recommends stress-ng sysstat procps python3 util-linux && rm -rf /var/lib/apt/lists/*
```

```bash
docker build -t lab-cpu-tools:1 .
docker run -d --name lab-cpu-one --cpus=1 lab-cpu-tools:1 stress-ng --cpu 1 --timeout 90s
docker run -d --name lab-cpu-four --cpus=1 lab-cpu-tools:1 stress-ng --cpu 4 --timeout 90s
```

وبعد عشرين ثانية تقريباً نقرأ الاستهلاك والعدادات:

```bash
docker stats --no-stream lab-cpu-one lab-cpu-four
docker exec lab-cpu-one cat /sys/fs/cgroup/cpu/cpu.stat
docker exec lab-cpu-four cat /sys/fs/cgroup/cpu/cpu.stat
docker exec lab-cpu-four top -b -n1 | head -14
```

والمخرج سوف يكون كما يلي:

```text
CONTAINER ID   NAME           CPU %     MEM USAGE / LIMIT     MEM %     NET I/O         BLOCK I/O   PIDS
8bad8f2df1ef   lab-cpu-one    110.34%   13.72MiB / 31.29GiB   0.04%     1.25kB / 126B   0B / 0B     2
07d6347ed8e4   lab-cpu-four   96.75%    23.03MiB / 31.29GiB   0.07%     1.04kB / 126B   0B / 0B     5

nr_periods 369
nr_throttled 128
throttled_time 786834600

nr_periods 323
nr_throttled 320
throttled_time 92745814400

top - 21:45:58 up 47 min,  0 user,  load average: 5.82, 7.84, 7.73
Tasks:   6 total,   5 running,   1 sleeping,   0 stopped,   0 zombie
%Cpu(s): 45.6 us,  1.8 sy,  0.0 ni, 50.0 id,  0.0 wa,  0.0 hi,  2.7 si,  0.0 st
MiB Mem :  32036.0 total,  13948.6 free,   6445.4 used,  13188.3 buff/cache
MiB Swap:   8192.0 total,   8192.0 free,      0.0 used.  25590.6 avail Mem

  PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND
   10 root      20   0   52600   6728   2520 R  26.3   0.0   0:08.67 stress-ng+
    8 root      20   0   52600   6728   2520 R  21.1   0.0   0:08.75 stress-ng+
    9 root      20   0   52600   6728   2520 R  21.1   0.0   0:08.85 stress-ng+
   11 root      20   0   52600   6728   2520 R  21.1   0.0   0:08.81 stress-ng+
    1 root      20   0   52600  19204  17840 S   0.0   0.1   0:00.15 stress-ng
```

في المخرج أعلاه لاحظ التالي:

- كلا ال Containers عند 100% تقريباً في `docker stats`، أي معالج واحد كامل، والرقم وحده لا يكشف أن أحدهما مخنوق.
- ال Container ذو الخيوط الأربعة خنق في 320 فترة من 323، أي في كل فترة تقريباً، ومجموع وقت الخنق `throttled_time` بالنانوثانية أكبر بأكثر من مئة مرة من ال Container ذي الخيط الواحد.
- في `top` كل خيط من الأربعة يأخذ ربع معالج تقريباً، بينما السطر `%Cpu(s)` يقول إن نصف معالجات الجهاز فارغة `50.0 id`، وهذا هو العرض الذي تراه: Container بطيء والسيرفر فارغ.

وما يحدث داخل كل فترة يبينه المخطط التالي، فالخيوط الأربعة تعمل معاً على أربع أنوية فتستهلك الحصة كلها في أول 25 ms، ثم تتوقف 75 ms حتى بداية الفترة التالية، وأي طلب يصل في هذه المدة ينتظر حتى لو كان صغيراً:

![مخطط زمني لثلاث فترات مدة كل منها 100 ms، في الأعلى Container حده معالج واحد بخيط واحد يعمل طوال الوقت دون خنق، وفي الأسفل Container بنفس الحد وأربعة خيوط تعمل 25 ms ثم تخنق 75 ms في كل فترة، وطلب يصل عند 40 ms ينتظر 60 ms، مع العداد nr_throttled 320 من 323 فترة، وتحته شرح أن --cpus=1 تعني cpu.max بقيمة 100000 100000 في cgroup v2](https://arabroot.io/content/images/2026/10/cpu-concurrency-05-cpu-quota.webp)

الحصة تنفد في أول ربع الفترة، فتتوقف الخيوط الأربعة حتى الفترة التالية

وقد يتساءل البعض: هل الحل أن أزيل الحد؟ والإجابة لا، فالحد يحمي بقية الخدمات على السيرفر من تطبيق واحد يستهلك كل شيء، وإنما الحل أن يتوافق عدد الخيوط والعمال في التطبيق مع الحد، أو أن ترفع الحد إذا كانت الأرقام تقول إن التطبيق يحتاج أكثر. وتأخير الطلبات بسبب الخنق يظهر في زمن الاستجابة Latency وليس في متوسط استهلاك المعالج، لذلك راقب `nr_throttled` لأي خدمة تشكو من البطء وعليها حد.

وفي Kubernetes نفس الآلية، فحد المعالج Limit في ال Pod [ينفذ بالخنق](https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/?ref=arabroot.io) كحد صارم، أما الطلب Request فيستخدمه المجدول ليختار العقدة Node التي تتسع لل Pod، ووحدته أن 1 CPU يساوي `1000m`.

### كيف تعرف كل لغة عدد المعالجات المتاحة لها؟

هذه نقطة مهمة جداً، والسبب أن أغلب البيئات تحدد عدد الخيوط وأحجام المجمعات تلقائياً من عدد المعالجات، فإذا رأى التطبيق 8 معالجات وحده الحقيقي معالجان، أنشأ خيوطاً أكثر من حصته فزاد الخنق. والمثال التالي يقرأ عدد المعالجات من Go وNode.js وPython تحت الحدين، وبرنامج Go هو الملف `procs.go`:

```go
package main

import (
	"fmt"
	"runtime"
)

func main() {
	fmt.Println(runtime.Version(), "NumCPU:", runtime.NumCPU(), "GOMAXPROCS:", runtime.GOMAXPROCS(0))
}
```

```bash
for opt in "--cpus=2" "--cpuset-cpus=0,1"; do
  echo "### $opt"
  docker run --rm --name lab-cpu-go $opt -v "$PWD":/lab -w /lab golang:1.27.1-alpine3.24 go run procs.go
  docker run --rm --name lab-cpu-node $opt node:22-alpine node -e 'const os=require("os");console.log(process.version,"availableParallelism:",os.availableParallelism(),"cpus:",os.cpus().length)'
  docker run --rm --name lab-cpu-pyd $opt python:3.13-slim python -c 'import os,sys;print(sys.version.split()[0],"cpu_count:",os.cpu_count(),"process_cpu_count:",os.process_cpu_count())'
done
```

والمخرج سوف يكون كما يلي:

```text
### --cpus=2
go1.27.1 NumCPU: 8 GOMAXPROCS: 2
v22.23.3 availableParallelism: 8 cpus: 8
3.13.16 cpu_count: 8 process_cpu_count: 8
### --cpuset-cpus=0,1
go1.27.1 NumCPU: 2 GOMAXPROCS: 2
v22.23.3 availableParallelism: 2 cpus: 8
3.13.16 cpu_count: 8 process_cpu_count: 2
```

والجدول التالي يلخص النتيجة مع ما وجدناه في Node.js 24 وJava 25 بنفس الطريقة:

| البيئة           | مع \--cpus=2                              | مع \--cpuset-cpus=0,1                                                                                             | ما يجب أن تفعله                                                                                                                                                                                  |
| ---------------- | ----------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Go 1.25 وما بعده | GOMAXPROCS يساوي 2                        | 2                                                                                                                 | لا شيء، فمنذ [Go 1.25](https://go.dev/doc/go1.25?ref=arabroot.io) يقرأ ال Runtime حد cgroup ويحدث القيمة دورياً، ولا يأخذ ال Request في Kubernetes في الحساب                                     |
| Java 10 وما بعده | availableProcessors يساوي 2               | 2                                                                                                                 | لا شيء غالباً، فدعم ال Containers [موجود منذ JDK 10](https://www.oracle.com/java/technologies/javase/10-relnote-issues.html?ref=arabroot.io)، ويمكنك فرض الرقم بالخيار \-XX:ActiveProcessorCount |
| Node.js 22 و24   | availableParallelism يساوي 8              | 2                                                                                                                 | حدد عدد العمليات في cluster أو PM2 بنفسك عبر متغير بيئة                                                                                                                                          |
| Python 3.13      | cpu\_count وprocess\_cpu\_count يساويان 8 | [process\_cpu\_count](https://docs.python.org/3/library/os.html?ref=arabroot.io#os.process%5Fcpu%5Fcount) يساوي 2 | حدد عدد العمال في Gunicorn وProcessPoolExecutor بنفسك                                                                                                                                            |

والخلاصة العملية أن `--cpuset-cpus` تراه كل البيئات، أما `--cpus` فلا تراه إلا Go وJava، لذلك إذا استخدمت `--cpus` مع تطبيق Node.js أو Python فاكتب عدد العمال صراحة في الإعداد، مثل `--workers 3` في Gunicorn، ولا تعتمد على الاكتشاف التلقائي.

وبعد أن تنتهي من التجارب احذف ما أنشأته بأسمائه فقط:

```bash
docker rm -f lab-cpu-one lab-cpu-four
docker rmi lab-cpu-tools:1
```

## كيف تقيس استهلاك المعالج؟

أول أداة هي [top](https://man7.org/linux/man-pages/man1/top.1.html?ref=arabroot.io) أو نسختها الأوضح `htop`، وأهم سطر فيها هو `%Cpu(s)` الذي رأيته في التجربة السابقة، ومعنى أعمدته كما يلي:

| العمود | معناه                  | إذا كان مرتفعاً                                               |
| ------ | ---------------------- | ------------------------------------------------------------- |
| us     | وقت كود التطبيقات User | تطبيقك يحسب، ابحث عن العملية في القائمة                       |
| sy     | وقت نواة النظام System | شبكة كثيفة، أو تبديل سياق كثير، أو خيوط أكثر من اللازم        |
| id     | وقت الفراغ Idle        | المعالج ليس مشكلتك                                            |
| wa     | انتظار القرص I/O Wait  | القرص بطيء، وهذا يرفع متوسط الحمل دون أن يكون المعالج مشغولاً |
| st     | وقت السرقة Steal       | جيرانك على السيرفر الفعلي، والحل خطة مخصصة أو مزود آخر        |

والأداتان [mpstat](https://man7.org/linux/man-pages/man1/mpstat.1.html?ref=arabroot.io) و[pidstat](https://man7.org/linux/man-pages/man1/pidstat.1.html?ref=arabroot.io) من الحزمة `sysstat` تفصلان ما تراه في top، فالأولى تعرض كل معالج منطقي وحده، والثانية تعرض كل عملية. والمثال التالي يعرض كل معالج على حدة:

```bash
mpstat -P ALL 1 1
```

والمخرج سوف يكون كما يلي:

```text
22:29:27     CPU    %usr   %nice    %sys %iowait    %irq   %soft  %steal  %guest  %gnice   %idle
22:29:28     all   20.94    0.00   19.28   27.27    0.00    4.68    0.00    0.00    0.00   27.82
22:29:28       0   17.00    0.00    6.00   29.00    0.00   18.00    0.00    0.00    0.00   30.00
22:29:28       1   13.95    0.00   17.44   13.95    0.00   17.44    0.00    0.00    0.00   37.21
22:29:28       2   20.99    0.00   37.04   22.22    0.00    0.00    0.00    0.00    0.00   19.75
22:29:28       3   38.14    0.00   12.37   20.62    0.00    1.03    0.00    0.00    0.00   27.84
22:29:28       4   12.50    0.00   26.14   38.64    0.00    0.00    0.00    0.00    0.00   22.73
22:29:28       5   21.88    0.00   18.75   43.75    0.00    0.00    0.00    0.00    0.00   15.62
22:29:28       6   27.59    0.00   20.69   27.59    0.00    0.00    0.00    0.00    0.00   24.14
22:29:28       7   14.29    0.00   19.78   20.88    0.00    0.00    0.00    0.00    0.00   45.05
```

وفائدة هذا العرض أنه يكشف تطبيقاً عالقاً على نواة واحدة، فإذا رأيت معالجاً واحداً عند 100% والبقية فارغة فالتطبيق يعمل في خيط واحد، مثل عملية Node.js واحدة أو تطبيق Python لا يستخدم إلا خيطاً، والحل هنا عمال أكثر وليس سيرفراً أكبر. أما `pidstat` فيعرض عموداً مفيداً جداً هو `%wait`، أي نسبة الوقت التي كانت فيها العملية جاهزة وتنتظر المعالج، والمثال التالي من Container حده معالج واحد وفيه خيطان حسابيان:

```bash
docker run -d --name lab-cpu-pid --cpus=1 lab-cpu-tools:1 stress-ng --cpu 2 --timeout 60s
docker exec lab-cpu-pid pidstat -u 5 1
```

والمخرج سوف يكون كما يلي:

```text
22:32:15      UID       PID    %usr %system  %guest   %wait    %CPU   CPU  Command
22:32:20        0         7   46.31    3.59    0.00   49.90   49.90     4  stress-ng-cpu
22:32:20        0         8   50.70    0.20    0.00   49.50   50.90     6  stress-ng-cpu
```

وكل خيط هنا عمل نصف الوقت وانتظر النصف الآخر، وهذا ما تتوقعه من خيطين على حصة معالج واحد. وإذا رأيت `%wait` مرتفعاً لخدمة على سيرفر لا يوجد عليه حد، فالمعالج نفسه لا يكفي، أما مع حد ال Container فارجع إلى `nr_throttled`. واحذف ال Container بعد التجربة بالأمر `docker rm -f lab-cpu-pid`.

## كم vCPU تحتاج لسيرفرك؟

ابدأ من الأرقام الرسمية لكل تطبيق تنوي تشغيله، ثم من نوع العمل، وقد وضعنا في دليل [كيف تختار خادماً افتراضياً](https://arabroot.io/articles/%D9%83%D9%8A%D9%81-%D8%AA%D8%AE%D8%AA%D8%A7%D8%B1-%D8%AE%D8%A7%D8%AF%D9%85%D8%A7-%D8%A7%D9%81%D8%AA%D8%B1%D8%A7%D8%B6%D9%8A%D8%A7-vps/) جدول نقاط بداية للسيناريوهات الشائعة، وهنا ما يضيفه هذا المقال على ذلك الجدول:

- **اقرأ متطلبات التطبيق الرسمية كما هي**: فمثلاً يطلب [Mailcow](https://docs.mailcow.email/getstarted/prerequisite-system/?ref=arabroot.io) معالجاً بسرعة 1 GHz فقط مقابل 6 GiB من الذاكرة على الأقل، أما [GitLab](https://docs.gitlab.com/install/requirements/?ref=arabroot.io) فيجعل 8 vCPU و16 GB نقطة البداية لتثبيت على سيرفر واحد، ولا ينصح بالأنواع التي تعتمد على رصيد المعالج Burstable.
- **الذاكرة قبل المعالج في أغلب الحالات**: أغلب الخدمات المستضافة ذاتياً تنتظر أكثر مما تحسب، لذلك تنفد الذاكرة قبل المعالج، وكل عامل إضافي يحتاج ذاكرة قبل أن يحتاج نواة، وإذا كان المعالج فارغاً والسيرفر بطيئاً فانظر في الذاكرة وال swap والقرص أولاً.
- **المشترك للأحمال المتقطعة والمخصص للمستمرة**: موقع وأدوات داخلية تعمل بشكل متقطع يكفيها vCPU مشترك، أما بناء الكود Build في CI وتحويل الفيديو وقواعد البيانات المزدحمة والنماذج المحلية فتحتاج vCPU مخصصاً، والسبب أن الحمل المستمر هو أول ما يتأثر بوقت السرقة.
- **اختبر قبل أن تقرر**: شغل حملك الحقيقي أسبوعاً وراقب `st` و`%wait` ومتوسط الحمل مقارنة ب `nproc`، ثم قرر، فالانتقال لخطة أكبر يأخذ دقائق عند أغلب المزودين.

## أخطاء شائعة

- **الحكم من متوسط الحمل وحده**: الحمل في Linux يحسب انتظار القرص أيضاً، فانظر في `wa` والعمود `b` قبل أن تشتري معالجاً أكبر.
- **عدد عمال أكبر من الذاكرة**: رفع `pm.max_children` أو عمال Gunicorn دون حساب استهلاك العامل ينتهي بإنهاء العمليات Out of Memory، وهو أسوأ من البطء.
- **الاعتماد على الاكتشاف التلقائي داخل Container له `--cpus`**: Node.js وPython يريان كل معالجات السيرفر، فيحسبان عدد عمال أكبر من الحصة ويزداد الخنق.
- **الخيوط في Python للعمل الحسابي**: مع قفل GIL لن تسرع الحساب، فاستخدم العمليات، أو جرب النسخة الحرة بعد التأكد من دعم مكتباتك.
- **عمل حسابي داخل حلقة الأحداث**: حلقة واحدة طويلة في Node.js تؤخر كل الطلبات، فانقلها إلى Worker Threads أو عامل خلفي.
- **`await` داخل حلقة لطلبات مستقلة**: تحول التزامن إلى تتابع، كما رأيت 10.04 ثانية مقابل نصف ثانية.
- **رفع `max_connections` بدلاً من حساب الاتصالات**: الحل حجم مجمع مناسب في التطبيق، وPgBouncer عندما تكثر التطبيقات.
- **عمل ثقيل داخل الطلب**: يحجز عامل الويب وينتهي بخطأ `504`، ومكانه طابور وعامل خلفي.

## قائمة تحقق قبل أن تضبط تطبيقك

1. اعرف عدد المعالجات المنطقية من `nproc` و`lscpu`، وهل هي مشتركة أم مخصصة في خطتك.
2. حدد نوع العمل في كل خدمة: هل تحسب أم تنتظر؟
3. اضبط عدد العمال صراحة: `worker_processes auto` في Nginx، والمعادلة `(2 × cores) + 1` نقطة بداية ل Gunicorn، و`pm.max_children` من حساب الذاكرة في PHP-FPM، وعملية لكل نواة ل Node.js.
4. اجمع اتصالات قاعدة البيانات من كل العمال والخيوط والمهام الخلفية، وتأكد أنها أقل من `max_connections` مع هامش للنشر.
5. انقل العمل الحسابي الطويل إلى طابور وعمال خلفيين.
6. إذا وضعت `--cpus` على Container فاجعل عدد العمال يتوافق معه، وراقب `nr_throttled`.
7. راقب `us` و`wa` و`st` ومتوسط الحمل مقارنة بعدد المعالجات أسبوعاً قبل أن تغير الخطة.

## الخلاصة

وصلنا لنهاية الموضوع، وأهم ما فيه:

- المقبس فيه أنوية، والنواة فيها خيطان عتاديان مع SMT، وال vCPU الذي تشتريه هو غالباً خيط عتادي يجدوله ال Hypervisor، وانتظاره لخيط مشغول يظهر في عمود `st`.
- متوسط الحمل في Linux يجمع الخيوط التي تنتظر المعالج والخيوط التي تنتظر القرص، فلا تحكم منه وحده.
- التزامن أن تتقدم عدة مهام بالتناوب، والتوازي أن تعمل معاً على أنوية مختلفة، والعمل الذي ينتظر يستفيد من التزامن، والعمل الذي يحسب لا يستفيد إلا من الأنوية.
- الخيوط في Python لا تسرع الحساب بسبب GIL إلا في النسخة الحرة 3.14t، وحلقة الأحداث في Node.js تتوقف كلها عند أي حساب طويل، والخيوط الافتراضية في Java وGo تجعل الانتظار رخيصاً ولا تسرع الحساب.
- عدد العمال يحدده نوع العمل والذاكرة واتصالات قاعدة البيانات، والعمل الثقيل مكانه طابور وعامل خلفي.
- `--cpus` حصة وقت في كل 100 ms، وتجاوزها يعني خنقاً يبطئ التطبيق والسيرفر فارغ، وGo وJava يقرآن هذه الحصة، أما Node.js وPython فحدد لهما عدد العمال بنفسك.

[![](https://arabroot.io/content/images/2026/10/choose-vps-cover-cover-2.webp)دليلكيف تختار خادماً افتراضياً VPSكم تحتاج من المعالج والذاكرة والقرص، وما الذي يميز مزوداً عن آخر غير السعر، حتى تختار VPS يناسب احتياجك.](https://arabroot.io/articles/%D9%83%D9%8A%D9%81-%D8%AA%D8%AE%D8%AA%D8%A7%D8%B1-%D8%AE%D8%A7%D8%AF%D9%85%D8%A7-%D8%A7%D9%81%D8%AA%D8%B1%D8%A7%D8%B6%D9%8A%D8%A7-vps/)[![](https://arabroot.io/content/images/2026/10/self-hosting-terms-cover-cover-1.webp)دليلمصطلحات الاستضافة الذاتية Self-hosting للمبتدئينمرجع مبسط لأهم المصطلحات التي سوف تقابلها في أدلة الاستضافة الذاتية، بأسمائها الإنجليزية كما تظهر في لوحات التحكم، مع شرح قصير لكل منها.](https://arabroot.io/articles/%D9%85%D8%B5%D8%B7%D9%84%D8%AD%D8%A7%D8%AA-%D8%A7%D9%84%D8%A7%D8%B3%D8%AA%D8%B6%D8%A7%D9%81%D8%A9-%D8%A7%D9%84%D8%B0%D8%A7%D8%AA%D9%8A%D8%A9-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/)[![](https://arabroot.io/content/images/2026/10/docker-cover-cover-1.webp)دليلتثبيت Docker Engine وCompose على Ubuntuكيف تثبت Docker على Ubuntu من المستودع الرسمي، وتضبطه من البداية حتى لا يمتلئ القرص أو تنكشف منافذك للإنترنت.](https://arabroot.io/articles/%D8%AA%D8%AB%D8%A8%D9%8A%D8%AA-docker-%D8%B9%D9%84%D9%89-ubuntu/)[![](https://arabroot.io/content/images/2026/10/transactional-email-cover-cover.webp)دليلTransactional Email: كيف تصل رسائل تطبيقك في وقتهالماذا تفصل رسائل تطبيقك مثل استعادة كلمة المرور عن النشرات البريدية، وكيف ترسلها بحيث تصل في وقتها ولا تذهب إلى Spam.](https://arabroot.io/articles/%D8%A7%D9%84%D8%A8%D8%B1%D9%8A%D8%AF-%D8%A7%D9%84%D8%AA%D9%81%D8%A7%D8%B9%D9%84%D9%8A-transactional-email/)[![](https://arabroot.io/content/images/2026/10/dev-checklist-cover-cover.webp)دليلقبل أن تنشر تطبيقك على خادمك: قائمة تحقق للمطورالأخطاء التي لا تظهر إلا بعد نشر تطبيقك في Docker خلف Reverse Proxy، وكيف تعالج كل واحد منها قبل أن يصل إلى المستخدمين.](https://arabroot.io/articles/%D9%82%D8%A7%D8%A6%D9%85%D8%A9-%D8%AA%D8%AD%D9%82%D9%82-%D8%A7%D9%84%D9%85%D8%B7%D9%88%D8%B1-%D9%82%D8%A8%D9%84-%D9%86%D8%B4%D8%B1-%D8%AA%D8%B7%D8%A8%D9%8A%D9%82%D9%87/)

## سجل التحديثات

- أكتوبر 2026: كتابة المقال، واختبار التجارب على Docker Engine 29.8.0 مع Python 3.13.16 و3.14.8 والنسخة الحرة 3.14t، وNode.js 22.23.3 و24.21.0، وJava 25.0.4.1، وGo 1.27.1، وstress-ng 0.17.06، وPostgreSQL 17.