معمل نظام التشغيل - الأسبوع 7
الـ Threading والتزامن (Concurrency) في C#
جدول المحتويات
- مقدمة
- البرنامج والـ Process والـ Thread
- أساسيات الـ Threading
- التنفيذ المتزامن (Synchronous) مقابل غير المتزامن (Asynchronous)
- دورة حياة الـ Thread وإدارتها
- حالات التسابق والمزامنة
- الـ Thread Pool
- تحليل الأداء: أحادي مقابل متعدد الـ Threads
- أمثلة تطبيقية
مقدمة
يستكشف هذا المعمل مفاهيم الـ threading في C#، ويوضح كيفية إنشاء الـ threads وإدارتها ومزامنتها في تطبيقات .NET. يتيح الـ threading التنفيذ المتزامن لعدة عمليات في آن واحد، مما يُحسِّن أداء التطبيق واستجابته.
أهداف التعلم:
- فهم الفرق بين البرنامج والـ process والـ thread
- إنشاء الـ threads وإدارتها في C#
- تنفيذ آليات مزامنة الـ threads
- التعرف على حالات التسابق (Race Conditions) ومنعها
- مقارنة الأداء بين التنفيذ الأحادي والمتعدد الـ threads
- استخدام الـ ThreadPool لإدارة المهام بكفاءة
البرنامج والـ Process والـ Thread
التعريفات
graph TB
A[Program] -->|Loaded into memory| B[Process]
B -->|Contains one or more| C[Thread]
style A fill:#e1f5ff
style B fill:#fff4e1
style C fill:#e8f5e9البرنامج (Program): مجموعة ثابتة من التعليمات مُخزَّنة على القرص (ملف تنفيذي). يحتوي على الكود والبيانات لكنه لا يُنفَّذ من تلقاء نفسه.
الـ Process: نسخة قيد التنفيذ من برنامج ما. عند تشغيل برنامج، ينشئ نظام التشغيل process تتضمن:
- مساحة الذاكرة (الكود، البيانات، الـ heap، الـ stack)
- موارد النظام (مقابض الملفات، اتصالات الشبكة)
- thread واحدة على الأقل من التنفيذ
الـ Thread: أصغر وحدة تنفيذ داخل الـ process. تشترك عدة threads داخل نفس الـ process في:
- مساحة الذاكرة
- الموارد
- واصفات الملفات (File Descriptors)
بينما تمتلك كل thread ما يخصها من:
- عدَّاد البرنامج (Program Counter)
- الـ Stack
- مجموعة السجلات (Register Set)
مخطط العلاقة
graph LR
A[Operating System] --> B[Process 1]
A --> C[Process 2]
B --> D[Thread 1.1]
B --> E[Thread 1.2]
B --> F[Thread 1.3]
C --> G[Thread 2.1]
C --> H[Thread 2.2]
style A fill:#f9f9f9
style B fill:#e3f2fd
style C fill:#e3f2fd
style D fill:#c8e6c9
style E fill:#c8e6c9
style F fill:#c8e6c9
style G fill:#c8e6c9
style H fill:#c8e6c9أساسيات الـ Threading
إنشاء thread وتشغيلها
الخطوات الأساسية للتعامل مع الـ threads في C#:
flowchart TD
A[Create Thread Object] --> B[Assign Method to Execute]
B --> C[Start Thread]
C --> D{Need to Wait?}
D -->|Yes| E[Call Join]
D -->|No| F[Continue Main Thread]
E --> G[Thread Completes]
F --> G
style A fill:#e1f5ff
style B fill:#e1f5ff
style C fill:#fff4e1
style E fill:#ffe1e1
style G fill:#e8f5e9المثال 1: إنشاء thread أساسية
using System;
using System.Threading;
class Program
{
static void Main()
{
Console.WriteLine("Main thread started.");
// Create a new thread, passing in the method it should execute
Thread myThread = new Thread(PrintNumbers);
// Start the thread
myThread.Start();
// Using Fork-Join style with threads
// Wait for the new thread to complete
myThread.Join();
Console.WriteLine("Main thread completed.");
}
// Method to be executed in a separate thread
static void PrintNumbers()
{
Console.WriteLine("Secondary thread started.");
// Loop to print numbers from 1 to 5
for (int i = 1; i <= 5; i++)
{
Console.WriteLine($"Number: {i}");
Thread.Sleep(500); // Pause for 500ms to simulate work
}
Console.WriteLine("Secondary thread completed.");
}
}نقاط أساسية:
- يأخذ بانِي (Constructor) الصنف
Threadمفوِّض تابع (ThreadStart) Start()يبدأ تنفيذ الـ threadJoin()يوقف الـ thread المُستدعية حتى تنتهي الـ thread المستهدفة (نمط Fork-Join)Thread.Sleep()يُوقِف التنفيذ مؤقتًا لمدة زمنية محددة
الناتج المتوقع:
Main thread started.
Secondary thread started.
Number: 1
Number: 2
Number: 3
Number: 4
Number: 5
Secondary thread completed.
Main thread completed.التنفيذ المتزامن (Synchronous) مقابل غير المتزامن (Asynchronous)
التنفيذ المتزامن (Synchronous)
تُنفَّذ العمليات بالتتابع، واحدة تلو الأخرى. يجب أن تكتمل كل عملية قبل بدء التالية.
sequenceDiagram
participant Main
participant Task1
participant Task2
Main->>Task1: Execute
Task1-->>Main: Complete
Main->>Task2: Execute
Task2-->>Main: Complete
Main->>Main: Continueالتنفيذ غير المتزامن (Asynchronous)
يمكن تنفيذ عدة عمليات في آن واحد. يمكن أن تبدأ عملية قبل اكتمال العمليات السابقة.
sequenceDiagram
participant Main
participant Thread1
participant Thread2
Main->>Thread1: Start
Main->>Thread2: Start
Thread1-->>Main: Working...
Thread2-->>Main: Working...
Thread1-->>Main: Complete
Thread2-->>Main: Completeمثال: تنفيذ متتابع مقابل تنفيذ متوازٍ
using System.Diagnostics;
namespace Threading;
public class Program
{
public static void Main()
{
// Measure execution time
Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
PingPong();
stopwatch.Stop();
Console.WriteLine($"Time elapsed: {stopwatch.ElapsedMilliseconds} ms");
}
private static void PingPong()
{
// Sequential execution (Synchronous)
// Ping();
// Pong();
Console.WriteLine("--------------------");
// Parallel execution (Asynchronous)
Thread pingThread = new Thread(Ping);
Thread pongThread = new Thread(Pong);
// Background vs Foreground threads
pingThread.IsBackground = true;
pongThread.IsBackground = true;
// Start the threads
pingThread.Start();
pongThread.Start();
Console.WriteLine("--------------------");
// Wait for both threads to complete
pingThread.Join();
pongThread.Join();
Console.WriteLine("Join finished");
}
// Loop to be executed by the thread
private static void Ping()
{
for (int i = 0; i < 100; i++)
{
Console.WriteLine($"Ping: {i}");
}
}
private static void Pong()
{
for (int i = 0; i < 100; i++)
{
Console.WriteLine($"Pong: {i}");
}
}
}ملاحظات:
- التنفيذ المتتابع: تكتمل Ping بالكامل قبل أن تبدأ Pong
- التنفيذ المتوازي: تُنفَّذ Ping وPong في آن واحد، ويتداخل الناتج
- عادةً ما يكون التنفيذ المتوازي أسرع في المهام المرتبطة بعمليات الإدخال/الإخراج (I/O-bound) أو المهام المستقلة
دورة حياة الـ Thread وإدارتها
حالات الـ Thread
stateDiagram-v2
[*] --> Unstarted: Create Thread
Unstarted --> Running: Start()
Running --> WaitSleepJoin: Sleep/Join/Wait
WaitSleepJoin --> Running: Wake up
Running --> Stopped: Complete
Stopped --> [*]
note right of Running
Thread is executing
end note
note right of WaitSleepJoin
Thread is blocked
end noteالـ Threads في الخلفية (Background) مقابل الـ Threads في الواجهة (Foreground)
الـ Threads في الواجهة (Foreground) (الافتراضية):
- تُبقي التطبيق قيد التشغيل
- لا يُغلَق التطبيق حتى تنتهي جميع threads الواجهة
الـ Threads في الخلفية (Background):
- لا تمنع التطبيق من الإغلاق
- تُنهى تلقائيًا عند انتهاء جميع threads الواجهة
// Setting thread as background
pingThread.IsBackground = true;
pongThread.IsBackground = true;توابع الـ Thread
التوابع الشائعة:
| التابع | الوصف |
|---|---|
Start() | يبدأ تنفيذ الـ thread |
Join() | يوقف الـ thread المستدعية حتى تنتهي الـ thread المستهدفة |
Sleep(ms) | يُوقِف الـ thread مؤقتًا لعدد الميلي ثانية المحدد |
Abort() | ❌ مُهمَل (Deprecated) - ينهي الـ thread قسرًا (غير مُوصى به) |
ملاحظات مهمة:
- لم يعد
Abort()مدعومًا في إصدارات .NET الحديثة (خطير وغير متوقع النتائج) - لا يمكن استدعاء
Start()على نفس الـ thread أكثر من مرة (يُطلِق استثناءً) - استخدم
Join()لتنفيذ نقاط المزامنة
حالات التسابق والمزامنة
ما هي حالة التسابق (Race Condition)؟
تحدث حالة التسابق عندما تصل عدة threads إلى بيانات مشتركة في نفس الوقت، وتعتمد النتيجة على التوقيت غير المتوقع لتنفيذ الـ threads.
flowchart LR
A[Thread 1] --> C[Shared Variable: counter = 0]
B[Thread 2] --> C
C --> D[Thread 1 reads: 0]
C --> E[Thread 2 reads: 0]
D --> F[Thread 1 increments: 1]
E --> G[Thread 2 increments: 1]
F --> H[Thread 1 writes: 1]
G --> I[Thread 2 writes: 1]
H --> J[Final value: 1]
I --> J
style J fill:#ffccccالقيمة المتوقعة: counter = 2
القيمة الفعلية: counter = 1 (حالة تسابق!)
مثال: حالة تسابق مع ThreadPool
using System;
using System.Threading;
class Program
{
// Shared variable to demonstrate race condition
static int counter = 0;
static object lockObject = new object(); // Lock object to prevent race conditions
static void Main()
{
Console.WriteLine("Starting Thread Pool Example with Increment and Decrement Tasks");
// ThreadPool example - queue multiple tasks to increment and decrement counter
ThreadPool.SetMaxThreads(50, 50);
ThreadPool.QueueUserWorkItem(IncrementCounter);
ThreadPool.QueueUserWorkItem(DecrementCounter);
ThreadPool.QueueUserWorkItem(IncrementCounter);
ThreadPool.QueueUserWorkItem(DecrementCounter);
// Wait for a moment to see race condition effect
Thread.Sleep(500); // Note: this is for demo purposes, not ideal in real applications
Console.WriteLine($"Final counter value after Thread Pool (may vary due to race condition): {counter}");
}
// Method to increment the counter in ThreadPool
static void IncrementCounter(object state)
{
// WITHOUT LOCK (Race condition can occur)
for (int i = 0; i < 100; i++)
{
counter++;
}
Thread.Sleep(1); // Small delay to increase race condition chance
}
// Method to decrement the counter in ThreadPool
static void DecrementCounter(object state)
{
// WITHOUT LOCK (Race condition can occur)
for (int i = 0; i < 100; i++)
{
counter--;
}
Thread.Sleep(1); // Small delay to increase race condition chance
}
}الناتج المتوقع: 0 (200 عملية زيادة - 200 عملية إنقاص)
الناتج الفعلي: يتفاوت (مثل -23، 15، 42) بسبب حالات التسابق
منع حالات التسابق باستخدام الأقفال (Locks)
static void IncrementCounter(object state)
{
// WITH LOCK (No race condition)
lock (lockObject)
{
for (int i = 0; i < 100; i++)
{
counter++;
}
Thread.Sleep(1);
}
}
static void DecrementCounter(object state)
{
// WITH LOCK (No race condition)
lock (lockObject)
{
for (int i = 0; i < 100; i++)
{
counter--;
}
Thread.Sleep(1);
}
}كيف تعمل الأقفال (Locks)
sequenceDiagram
participant T1 as Thread 1
participant Lock as Lock Object
participant T2 as Thread 2
participant Data as Shared Data
T1->>Lock: Acquire lock
Lock-->>T1: Lock granted
T2->>Lock: Try acquire lock
Lock-->>T2: Wait...
T1->>Data: Read/Write
T1->>Lock: Release lock
Lock-->>T2: Lock granted
T2->>Data: Read/Write
T2->>Lock: Release lockالمفاهيم الأساسية:
- تضمن عبارة
lockأن thread واحدة فقط تُنفِّذ القسم الحرج (Critical Section) في كل مرة - تنتظر باقي الـ threads حتى يُحرَّر القفل
- اقفل دائمًا على كائن (Object) مخصص لذلك (
private static object lockObject) - لا تقفل أبدًا على
thisأو الأنواع (Types) أو السلاسل النصية (Strings)
الـ Thread Pool
ما هو الـ ThreadPool؟
الـ ThreadPool هو مجموعة من الـ threads تم إنشاؤها مسبقًا وتُديرها بيئة تشغيل .NET. بدلًا من إنشاء threads جديدة لكل مهمة، يمكنك وضع عناصر العمل في طابور (Queue) خاص بالـ ThreadPool.
graph TB
A[Application] -->|QueueUserWorkItem| B[ThreadPool]
B --> C[Thread 1]
B --> D[Thread 2]
B --> E[Thread 3]
B --> F[Thread N]
C -->|Executes| G[Task Queue]
D -->|Executes| G
E -->|Executes| G
F -->|Executes| G
style B fill:#e1f5ff
style G fill:#fff4e1مزايا الـ ThreadPool
- الكفاءة: يعيد استخدام threads موجودة بدلًا من إنشاء جديدة
- إدارة الموارد: يحدد أقصى عدد من الـ threads المتزامنة
- الأداء: يُقلِّل من العبء الناتج عن إنشاء الـ threads وإنهائها
- الإدارة التلقائية: تتولى بيئة تشغيل .NET إدارة دورة حياة الـ threads
الـ ThreadPool مقابل الـ Threads اليدوية
| الميزة | الـ ThreadPool | الـ Threads اليدوية |
|---|---|---|
| عبء الإنشاء | منخفض (يعيد استخدام الـ threads) | مرتفع (ينشئ threads جديدة) |
| التحكم | محدود | تحكم كامل |
| الأنسب لـ | المهام القصيرة | العمليات طويلة الأمد |
| التنظيف (Cleanup) | تلقائي | يدوي |
استخدام الـ ThreadPool
// Set maximum threads (optional)
ThreadPool.SetMaxThreads(50, 50);
// Queue work items
ThreadPool.QueueUserWorkItem(IncrementCounter);
ThreadPool.QueueUserWorkItem(DecrementCounter);تحليل الأداء: أحادي مقابل متعدد الـ Threads
مثال مقارن
يقارن هذا المثال بين التنفيذ الأحادي والمتعدد الـ threads لجمع مليار رقم.
using System;
using System.Diagnostics;
using System.Threading;
class Program
{
static long totalSum = 0; // Shared variable to hold the total sum
static object lockObject = new object(); // Lock object to prevent race conditions
static void Main()
{
Console.WriteLine("Single-threaded process started.");
// Start timing
Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
// Perform the task: Sum one billion numbers
CalculatePartialSum(1, 1_000_000_000);
// Stop timing
stopwatch.Stop();
Console.WriteLine($"Single-threaded sum: {totalSum}");
Console.WriteLine($"Time taken (single-threaded): {stopwatch.ElapsedMilliseconds} ms");
// -----------------------------------------------------
Console.WriteLine("\nMulti-threaded process started.");
totalSum = 0;
// Start timing
stopwatch.Reset();
stopwatch.Start();
// Define ranges for each thread
Thread thread1 = new Thread(() => CalculatePartialSum(1, 250_000_000));
Thread thread2 = new Thread(() => CalculatePartialSum(250_000_001, 500_000_000));
Thread thread3 = new Thread(() => CalculatePartialSum(500_000_001, 750_000_000));
Thread thread4 = new Thread(() => CalculatePartialSum(750_000_001, 1_000_000_000));
// Start all threads
thread1.Start();
thread2.Start();
thread3.Start();
thread4.Start();
// Wait for all threads to complete
thread1.Join();
thread2.Join();
thread3.Join();
thread4.Join();
// Stop timing
stopwatch.Stop();
Console.WriteLine($"Multi-threaded sum: {totalSum}");
Console.WriteLine($"Time taken (multi-threaded): {stopwatch.ElapsedMilliseconds} ms");
}
// Method to calculate partial sum within a range
static void CalculatePartialSum(int start, int end)
{
// Calculate partial sum locally (no contention)
long partialSum = 0;
for (int i = start; i <= end; i++)
{
partialSum += i;
}
// Lock only when updating shared variable
lock (lockObject)
{
totalSum += partialSum;
}
}
}مخطط سير التنفيذ
flowchart TB
subgraph Single-Threaded
A1[Start] --> A2[Calculate 1 to 1B]
A2 --> A3[End]
end
subgraph Multi-Threaded
B1[Start] --> B2[Thread 1: 1-250M]
B1 --> B3[Thread 2: 250M-500M]
B1 --> B4[Thread 3: 500M-750M]
B1 --> B5[Thread 4: 750M-1B]
B2 --> B6[Join All]
B3 --> B6
B4 --> B6
B5 --> B6
B6 --> B7[End]
end
style A2 fill:#ffcccc
style B2 fill:#ccffcc
style B3 fill:#ccffcc
style B4 fill:#ccffcc
style B5 fill:#ccffccاستراتيجية التحسين: المجموع الجزئي (Partial Sum)
flowchart LR
A[Thread 1] -->|Calculate locally| B[partialSum1]
C[Thread 2] -->|Calculate locally| D[partialSum2]
E[Thread 3] -->|Calculate locally| F[partialSum3]
G[Thread 4] -->|Calculate locally| H[partialSum4]
B -->|Lock once| I[totalSum]
D -->|Lock once| I
F -->|Lock once| I
H -->|Lock once| I
style B fill:#e8f5e9
style D fill:#e8f5e9
style F fill:#e8f5e9
style H fill:#e8f5e9
style I fill:#fff4e1ثلاثة أساليب:
- بدون قفل (خاطئ):
// ❌ Race condition - incorrect result
for (int i = start; i <= end; i++)
{
totalSum += i;
}- مع قفل في كل تكرار (بطيء):
// ✓ Correct but very slow - excessive locking
for (int i = start; i <= end; i++)
{
lock (lockObject)
{
totalSum += i;
}
}- مجموع جزئي مع قفل (الأمثل):
// ✓✓ Correct and fast - minimal locking
long partialSum = 0;
for (int i = start; i <= end; i++)
{
partialSum += i;
}
lock (lockObject)
{
totalSum += partialSum;
}نتائج الأداء
نتائج نموذجية على معالج رباعي النواة (4-core CPU):
| الأسلوب | الزمن | معدل التسريع |
|---|---|---|
| أحادي الـ thread | 4000 ms | 1x |
| متعدد الـ threads (4 threads) | 1200 ms | 3.3x |
ملاحظات مهمة:
- يوفر تعدد الـ threads تسريعًا ملحوظًا في المهام المرتبطة بالمعالج (CPU-bound)
- التسريع ليس خطيًا بسبب العبء الإضافي والمزامنة
- استراتيجية القفل الصحيحة أساسية للأداء
- أفضل أداء: تقليل التنافس على القفل (Lock Contention) عبر تقليل تكرار القفل
أمثلة تطبيقية
المثال 1: تسمية الـ Thread
private static void MainThreadName()
{
// Get the current thread
Thread currentThread = Thread.CurrentThread;
// Get the current thread's name
string currentThreadName = currentThread.Name ?? "No name";
Console.WriteLine($"Current thread name: {currentThreadName}");
// Set the current thread's name
currentThread.Name = "Main Thread";
Console.WriteLine($"Current thread name: {currentThread.Name}");
}المثال 2: تعبيرات Lambda مع الـ Threads
// Using lambda expressions to pass parameters
Thread thread1 = new Thread(() => CalculatePartialSum(1, 250_000_000));المثال 3: حالات الـ Thread
Thread myThread = new Thread(DoWork);
Console.WriteLine(myThread.ThreadState); // Unstarted
myThread.Start();
Console.WriteLine(myThread.ThreadState); // Running
myThread.Join();
Console.WriteLine(myThread.ThreadState); // Stoppedأفضل الممارسات
افعل ✓
- استخدم الـ ThreadPool للمهام القصيرة: فعّال للعمليات الصغيرة الكثيرة
- قلِّل مدة القفل: اقفل الأقسام الحرجة فقط
- استخدم متغيرات محلية: قلِّل الحالة المشتركة لتقليل التنافس
- اجعل الـ threads في الخلفية عند الحاجة: امنع إعاقة إغلاق التطبيق
- تعامل مع الاستثناءات: ضع كود الـ thread داخل كتل try-catch
- استخدم Join() بحذر: تجنَّب حالات الجمود (Deadlocks) الناتجة عن الانتظار الدائري
لا تفعل ✗
- لا تستخدم Abort(): مُهمَل وخطير
- لا تقفل على كائنات عامة (Public): استخدم كائنات قفل خاصة (Private)
- لا تقفل على الأنواع (Types) أو السلاسل النصية (Strings): قد يسبب حالات جمود
- لا تُنشئ عددًا كبيرًا جدًا من الـ threads: يسبب عبئًا إضافيًا واستنزافًا للموارد
- لا تتجاهل حالات التسابق: احمِ البيانات المشتركة دائمًا
- لا تنسَ تحرير الموارد (Dispose): نظِّف الموارد بشكل صحيح
ملخص
المفاهيم الأساسية التي تم تناولها
mindmap
root((Threading))
Basics
Thread Creation
Start and Join
Thread Lifecycle
Synchronization
Race Conditions
Locks
Critical Sections
ThreadPool
Queuing Work
Resource Management
Efficiency
Performance
Single vs Multi
Partial Sum Strategy
Lock Optimizationمخطط اتخاذ القرار في الـ Threading
flowchart TD
A[Need Concurrency?] -->|No| B[Sequential Execution]
A -->|Yes| C{Short Tasks?}
C -->|Yes| D[Use ThreadPool]
C -->|No| E[Create Manual Threads]
E --> F{Shared Data?}
F -->|No| G[No Synchronization Needed]
F -->|Yes| H[Use Locks/Synchronization]
H --> I[Minimize Lock Duration]
I --> J[Use Partial Results]
style D fill:#e8f5e9
style E fill:#fff4e1
style H fill:#ffebeeتمارين المعمل
تمرين 1: الـ Threading الأساسي
أنشئ برنامجًا يحتوي على thread تطبعان الأرقام الزوجية والفردية في آن واحد من 1 إلى 100.
تمرين 2: حالة التسابق
عدِّل مثال الـ ThreadPool ليقوم بما يلي:
- شغِّله أولًا بدون أقفال ولاحظ النتائج الخاطئة
- أضِف أقفالًا وتحقق من صحة النتائج
- قارن أزمنة التنفيذ
تمرين 3: تحليل الأداء
أنشئ برنامجًا يقوم بما يلي:
- ضرب مصفوفتين كبيرتين (1000×1000)
- تنفيذ نسختين، أحادية الـ thread ومتعددة الـ threads
- مقارنة أزمنة التنفيذ
- تحليل معدل التسريع مقابل عدد الـ threads (1، 2، 4، 8)
تمرين 4: نمط المنتِج والمستهلِك (Producer-Consumer)
نفِّذ نمط منتِج-مستهلِك بسيطًا باستخدام الـ threads والأقفال.
مراجع ومصادر إضافية للقراءة
- توثيق Microsoft الرسمي للـ Threading في .NET
- "C# Threading Handbook" - أفضل الممارسات للبرمجة المتزامنة
- Operating System Concepts (Silberschatz) - فصل الـ Threads
- فهم بدائيات مزامنة الـ threads (Thread Synchronization Primitives)
- تحسين الأداء في التطبيقات متعددة الـ threads
أسئلة للتفكير
- متى ينبغي استخدام تعدد الـ threads مقابل الـ thread الواحدة؟
- ما المقايضات (Trade-offs) بين الـ ThreadPool وإنشاء الـ threads يدويًا؟
- كيف تحدث حالات التسابق وكيف يمكن منعها؟
- لماذا يكون أسلوب المجموع الجزئي أسرع من القفل في كل تكرار؟
- ماذا يحدث إذا نسيت استدعاء Join() على thread في الواجهة (Foreground)؟
- كيف يؤثر عدد أنوية المعالج على أداء تعدد الـ threads؟
نهاية وثيقة المعمل