0Pricing
Learn Rust Coding · درس

الخيوط محددة النطاق

الاقتراض عبر الخيوط

الخيوط محددة النطاق درس مجاني في Learn Rust Coding على CoddyKit. هذا هو الدرس 3 من أصل 4. يمكنك قراءة الدرس كاملاً أدناه مجاناً — ثم تمرن عليه مباشرة في المتصفح باستخدام محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7. هذا الدرس جزء من مسار التعلم في Learn Rust Coding، وتقدمك يتزامن عبر الويب وتطبيق CoddyKit. تتضمن دورة Learn Rust Coding 4 دروس في المجموع.

مشكلة الاستعارة في الخيوط

يجب أن يكون الإغلاق العادي في thread::spawn من النوع 'static؛ فلا يمكنه استعارة متغيرات محلية، لأن الخيط قد يستمر بعد انتهاء الدالة المالكة لها. ولهذا ترون move وArc كثيرًا.

تحل الخيوط محددة النطاق هذه المشكلة من خلال ضمان انتهاء كل خيط قبل انتهاء النطاق، فتصبح استعارة البيانات المحلية آمنة.

لماذا تتطلب spawn النوع 'static

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

use std::thread;

fn main() {
    let nums = vec![1, 2, 3];
    // move transfers ownership into the thread
    let handle = thread::spawn(move || {
        println!("in thread: {:?}", nums);
    });
    handle.join().unwrap();
}

التعريف بـ thread::scope

تم تثبيت std::thread::scope في Rust 1.63، وهو ينشئ نطاقًا يمكن للخيوط داخله استعارة المتغيرات المحلية. يحظر النطاق التنفيذ حتى تكتمل جميع الخيوط الموجودة داخله، ولذلك لا يمكن أن تصبح الاستعارات معلّقة.

تنشئ الخيط باستخدام s.spawn(...)، مع استخدام مقبض النطاق s بدلًا من thread::spawn.

use std::thread;

fn main() {
    let data = vec![10, 20, 30];
    thread::scope(|s| {
        s.spawn(|| {
            println!("borrowed: {:?}", data);
        });
    });
    // data is still usable here
    println!("after scope: {:?}", data);
}

الاستعارة دون move

داخل thread::scope، يمكنك قراءة المتغيرات المحلية بالمرجع دون استخدام move. ويمكن لعدة خيوط ضمن النطاق مشاركة استعارة غير قابلة للتغيير للبيانات نفسها في الوقت نفسه، تمامًا مثل المراجع العادية.

use std::thread;

fn main() {
    let message = String::from("shared text");
    thread::scope(|s| {
        s.spawn(|| println!("thread 1 sees: {}", message));
        s.spawn(|| println!("thread 2 sees: {}", message));
    });
    println!("main still owns: {}", message);
}

تقسيم العمل على شريحة

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

use std::thread;

fn main() {
    let numbers = [1, 2, 3, 4, 5, 6];
    let (left, right) = numbers.split_at(3);
    thread::scope(|s| {
        s.spawn(|| {
            let sum: i32 = left.iter().sum();
            println!("left sum:  {}", sum);
        });
        s.spawn(|| {
            let sum: i32 = right.iter().sum();
            println!("right sum: {}", sum);
        });
    });
}

تجميع القيم المعادة

مثل الخيوط العادية، تُعيد s.spawn قيمة ScopedJoinHandle. استدعِ .join() للحصول على القيمة التي أعادها الخيط. يمكنك جمع المقابض ثم تنفيذ join لها بعد إنشاء الخيوط لجمع النتائج.

use std::thread;

fn main() {
    let inputs = [2, 4, 6];
    let mut handles = vec![];
    thread::scope(|s| {
        for &x in &inputs {
            handles.push(s.spawn(move || x * x));
        }
        let results: Vec<i32> = handles.into_iter()
            .map(|h| h.join().unwrap())
            .collect();
        println!("{:?}", results);
    });
}

تتطلب الاستعارات القابلة للتغيير عناية

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

فيما يلي، يعدّل كل خيط نصفًا منفصلًا من المصفوفة عبر split_at_mut.

use std::thread;

fn main() {
    let mut data = [1, 2, 3, 4];
    let (a, b) = data.split_at_mut(2);
    thread::scope(|s| {
        s.spawn(|| { for x in a.iter_mut() { *x *= 10; } });
        s.spawn(|| { for x in b.iter_mut() { *x += 100; } });
    });
    println!("{:?}", data);
}

ينضم النطاق إلى الخيوط تلقائيًا

لا يتعين عليك استدعاء join لكل خيط ضمن النطاق. فعندما تعود closure الخاصة بـ scope، تنضم Rust تلقائيًا إلى جميع الخيوط التي لم يُنفَّذ لها join بعد، قبل متابعة التنفيذ. ولهذا السبب نضمن بقاء الاستعارات صالحة طوال مدة حياة الخيط.

use std::thread;
use std::time::Duration;

fn main() {
    let label = String::from("task");
    thread::scope(|s| {
        s.spawn(|| {
            thread::sleep(Duration::from_millis(30));
            println!("{} done", label);
        });
        println!("spawned, scope will wait");
    });
    println!("all scoped threads finished");
}

دمج النطاق مع التعديل المشترك

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

use std::sync::Mutex;
use std::thread;

fn main() {
    let counter = Mutex::new(0);
    thread::scope(|s| {
        for _ in 0..5 {
            s.spawn(|| {
                let mut n = counter.lock().unwrap();
                *n += 1;
            });
        }
    });
    println!("counter = {}", *counter.lock().unwrap());
}

الخيوط ذات النطاق مقابل الخيوط المُنشأة: متى تستخدم كلًّا منها

استخدم الخيوط ذات النطاق عندما يكون العمل محدودًا وينتهي داخل دالة، وعندما تريد استعارة بيانات المكدس من دون Arc أو استنساخ.

استخدم thread::spawn عندما يجب أن يستمر الخيط بعد انتهاء الدالة الحالية أو أن يعمل طوال عمر البرنامج. لا يمكن للخيوط ذات النطاق تجاوز نطاقها.

مثال على التعيين المتوازي

بتجميع المفاهيم السابقة: تعيين متوازٍ صغير يحوّل كل عنصر من عناصر متجه في خيط خاص به مع استعارة المدخلات، ثم يجمع النتائج بالترتيب.

use std::thread;

fn parallel_double(items: &[i32]) -> Vec<i32> {
    let mut handles = Vec::new();
    let mut out = Vec::new();
    thread::scope(|s| {
        for &x in items {
            handles.push(s.spawn(move || x * 2));
        }
        for h in handles {
            out.push(h.join().unwrap());
        }
    });
    out
}

fn main() {
    let nums = vec![1, 2, 3, 4];
    println!("{:?}", parallel_double(&nums));
}

اختبار سريع

اختبر مدى فهمك للخيوط ذات النطاق.

مراجعة

لقد تعلمت عن الخيوط ذات النطاق:

  • تتطلب الإغلاقات التي تستخدمها thread::spawn أن تكون 'static، أما الخيوط ذات النطاق فلا تتطلب ذلك.
  • تتيح thread::scope للخيوط استعارة المتغيرات المحلية بأمان.
  • ينضم النطاق تلقائيًا إلى جميع الخيوط قبل عودته.
  • لا تزال المشاركة القابلة للتغيير تتطلب Mutex، لكنك لا تحتاج إلى Arc داخل النطاق.
  • استخدم الخيوط ذات النطاق للتنفيذ المتوازي المحدود والمحلي للدالة.

الأسئلة الشائعة

هل درس «الخيوط محددة النطاق» مجاني؟

نعم — نص درس «الخيوط محددة النطاق» كامل متاح مجاناً هنا على الويب. لتمرينه بشكل تفاعلي (محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7) وفتح باقي دورة Learn Rust Coding، انتقل إلى CoddyKit PRO. تتضمن دورة Learn Rust Coding 4 دروس في المجموع.

ماذا ستتعلم في «الخيوط محددة النطاق»؟

الاقتراض عبر الخيوط تتمرن على Learn Rust Coding مع أكواد عملية تشغلها مباشرة في المتصفح، ومدرس ذكاء اصطناعي متاح 24/7 يجيب على أسئلتك أثناء عملك.

هل أحتاج إلى خبرة سابقة لأبدأ Learn Rust Coding؟

لا تُشترط خبرة سابقة. Learn Rust Coding على CoddyKit منظم للمبتدئين حتى المتقدمين، لذا يمكنك البدء من هنا أو من البداية والتقدم بسرعتك الخاصة. هذا هو الدرس 3 من أصل 4.

كم من الوقت يستغرق درس «الخيوط محددة النطاق»؟

معظم دروس CoddyKit تستغرق حوالي 5–10 دقائق. كل منها موجز وتفاعلي، لذا تحرز تقدماً مستمراً وتستأنف من حيث توقفت عبر الويب والتطبيق.

هل يمكنني كتابة وتشغيل أكواد في درس Learn Rust Coding هذا؟

نعم. كل درس في Learn Rust Coding يتضمن محرر أكواد مدمج، لذا تكتب وتشغل أكواداً حقيقية مباشرة في متصفحك وتحصل على تعليقات فورية من الذكاء الاصطناعي — بدون إعداد محلي.

جميع الدروس في هذه الدورة

  1. قنوات mpsc
  2. مشاركة الحالة باستخدام Arc/Mutex
  3. الخيوط محددة النطاق
  4. قنوات Crossbeam
← العودة إلى Learn Rust Coding