021-88881776

آموزش هم‌زمانی (Concurrency) در Swift

در دنیای برنامه‌نویسی مدرن، هم‌زمانی (Concurrency) در Swift یکی از مفاهیم کلیدی است که به توسعه‌دهندگان اجازه می‌دهد برنامه‌هایی کارآمدتر و پاسخ‌گوتر ایجاد کنند. آموزش Swift شامل درک دقیق هم‌زمانی است تا بتوانید از امکانات قدرتمند این زبان بهره‌مند شوید. در این مقاله، به بررسی جامع و کامل هم‌زمانی (Concurrency) در Swift می‌پردازیم و تمامی جنبه‌های این موضوع را از سطح مبتدی تا پیشرفته پوشش می‌دهیم.

مشکلات هم‌زمانی در زبان‌های قدیمی

در زبان‌های برنامه‌نویسی قدیمی‌تر، مدیریت هم‌زمانی (Concurrency) در Swift معمولاً پیچیده و مستعد خطا بود. استفاده از نخ‌ها (Threads) و قفل‌ها (Locks) می‌توانست به مشکلاتی مانند Race Condition، Deadlock و Resource Contention منجر شود. این مشکلات نه تنها توسعه را دشوار می‌کردند بلکه عملکرد برنامه‌ها را نیز تحت تاثیر قرار می‌دادند.

Race Condition

Race Condition زمانی رخ می‌دهد که دو یا چند نخ به طور همزمان به منابع مشترک دسترسی پیدا کنند و ترتیب اجرای آن‌ها تأثیرگذار بر نتیجه نهایی باشد. اگر دسترسی به این منابع به درستی مدیریت نشود، می‌تواند منجر به نتایج غیرقابل پیش‌بینی و خطاهای منطقی در برنامه شود.

مثال:
فرض کنید دو نخ به صورت همزمان سعی دارند مقدار یک متغیر مشترک را افزایش دهند:

public class Counter {
    private int count = 0;

    public void increment() {
        count++;
    }

    public int getCount() {
        return count;
    }
}

اگر دو نخ به طور همزمان متد increment را فراخوانی کنند، ممکن است مقدار نهایی count تنها یک افزایش داشته باشد به جای دو افزایش مورد انتظار.

Deadlock

Deadlock وضعیتی است که در آن دو یا چند نخ به طور دائمی منتظر منابعی هستند که هیچ کدام قادر به آزادسازی آن نیستند. این موضوع باعث می‌شود که برنامه به حالت تعلیق بی‌پایان برسد و هیچ‌یک از نخ‌ها قادر به پیشرفت نباشند.

مثال:
فرض کنید نخ A قفل روی منبع X را در اختیار دارد و برای دسترسی به منبع Y نیاز دارد، در حالی که نخ B قفل روی منبع Y را دارد و برای دسترسی به منبع X نیازمند است. در این حالت، هر دو نخ منتظر آزاد شدن منبعی هستند که توسط نخ دیگر نگه داشته شده است، و هیچ‌یک قادر به پیشرفت نیستند.

public class DeadlockExample {
    private final Object resource1 = new Object();
    private final Object resource2 = new Object();

    public void method1() {
        synchronized (resource1) {
            synchronized (resource2) {
                // عملیات
            }
        }
    }

    public void method2() {
        synchronized (resource2) {
            synchronized (resource1) {
                // عملیات
            }
        }
    }
}

Resource Contention

Resource Contention زمانی رخ می‌دهد که چند نخ به منابع محدودی مانند حافظه، پردازنده یا دستگاه‌های ورودی/خروجی دسترسی پیدا کنند. این رقابت می‌تواند منجر به کاهش کارایی و افزایش زمان انتظار نخ‌ها برای دسترسی به منابع شود.

مثال:
اگر چند نخ به طور همزمان به یک فایل بزرگ برای نوشتن داده‌ها دسترسی داشته باشند، ممکن است سرعت نوشتن کاهش یابد و نخ‌ها مجبور به انتظار برای دسترسی به فایل شوند.

پیچیدگی مدیریت نخ‌ها و قفل‌ها

مدیریت مستقیم نخ‌ها و قفل‌ها نیازمند دقت بالا و درک عمیق از اصول هم‌زمانی (Concurrency) در Swift است. خطا در استفاده از نخ‌ها و قفل‌ها می‌تواند به مشکلاتی همچون Race Condition و Deadlock منجر شود که یافتن و رفع آن‌ها بسیار دشوار است.

مثال:
در زبان‌هایی مانند Java، برنامه‌نویسان باید به طور دستی نخ‌ها را ایجاد و مدیریت کنند و از قفل‌ها برای همگام‌سازی دسترسی به منابع مشترک استفاده کنند. این فرآیند می‌تواند منجر به کد پیچیده و سخت‌خوانی شود که نگهداری و توسعه آن دشوار است.

public class ThreadManagement {
    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            // عملیات نخ اول
        });

        Thread thread2 = new Thread(() -> {
            // عملیات نخ دوم
        });

        thread1.start();
        thread2.start();
    }
}

Callback Hell و پیچیدگی کد ناهم‌زمان

در زبان‌های قدیمی‌تر که از مدل‌های ناهم‌زمان مبتنی بر کال‌بک استفاده می‌کنند، ساختار کد می‌تواند به شدت تو در تو و پیچیده شود، که به آن Callback Hell می‌گویند. این امر باعث کاهش خوانایی و افزایش احتمال بروز خطاهای منطقی در برنامه می‌شود.

مثال:
در JavaScript، استفاده از توابع کال‌بک برای انجام عملیات ناهم‌زمان می‌تواند منجر به ساختار کد تو در تو و دشوار برای مدیریت شود.

doSomething(function(result) {
    doSomethingElse(result, function(newResult) {
        doAnotherThing(newResult, function(finalResult) {
            // پردازش نهایی
        });
    });
});

مدیریت هم‌زمانی (Concurrency) در Swift در زبان‌های قدیمی‌تر با چالش‌ها و مشکلات متعددی همراه بود که توسعه‌دهندگان را مجبور به نوشتن کد پیچیده و مستعد خطا می‌کرد. این مشکلات نه تنها باعث افزایش زمان توسعه می‌شدند بلکه عملکرد و پایداری برنامه‌ها را نیز تحت تاثیر قرار می‌دادند. با معرفی هم‌زمانی (Concurrency) در Swift، این چالش‌ها تا حد زیادی کاهش یافته و توسعه‌دهندگان می‌توانند از ابزارها و ساختارهای پیشرفته‌تری برای مدیریت هم‌زمانی استفاده کنند.

مزایا و اهداف Swift Concurrency

هم‌زمانی (Concurrency) در Swift با هدف ساده‌سازی و بهبود کارایی برنامه‌های هم‌زمان ایجاد شده است. این امکانات به توسعه‌دهندگان اجازه می‌دهند تا به سادگی وظایف پیچیده هم‌زمانی را مدیریت کنند بدون اینکه نگران مشکلات سنتی مانند Race Condition باشند. در این بخش به بررسی جامع‌تر مزایا و اهداف Swift Concurrency می‌پردازیم و نشان می‌دهیم چگونه این ویژگی‌ها توسعه برنامه‌های بهتر و ایمن‌تر را ممکن می‌سازند.

اهداف Swift Concurrency

ساده‌سازی کدنویسی هم‌زمان: یکی از اصلی‌ترین اهداف Swift Concurrency، ساده‌سازی نوشتن کدهای هم‌زمان است. با معرفی مفاهیم جدیدی مانند async و await، توسعه‌دهندگان می‌توانند به جای استفاده از نخ‌ها و قفل‌های پیچیده، کدهایی خوانا و قابل فهم‌تر بنویسند.

بهبود کارایی: Swift Concurrency با بهینه‌سازی اجرای وظایف هم‌زمان، کارایی برنامه‌ها را بهبود می‌بخشد. این سیستم به طور خودکار مدیریت منابع را انجام می‌دهد و اطمینان حاصل می‌کند که وظایف به بهترین شکل ممکن اجرا شوند.

افزایش امنیت و پایداری: با استفاده از ساختارهای ایمن مانند Actors، Swift Concurrency از بروز مشکلاتی مانند Race Condition و Deadlock جلوگیری می‌کند. این امر منجر به برنامه‌هایی می‌شود که نه تنها سریع‌تر بلکه پایدارتر و ایمن‌تر نیز هستند.

پشتیبانی از هم‌زمانی ساختاریافته: Swift Concurrency مفهوم Structured Concurrency را معرفی می‌کند که تضمین می‌کند وظایف در محدوده مشخصی اجرا شوند و به درستی مدیریت و خاتمه یابند. این امر باعث می‌شود که کدهای هم‌زمان قابل پیش‌بینی‌تر و مدیریت‌پذیرتر باشند.

مزایای اصلی Swift Concurrency

1. سادگی

استفاده از Swift Concurrency به توسعه‌دهندگان اجازه می‌دهد تا بدون نیاز به نوشتن کدهای پیچیده و مدیریت مستقیم نخ‌ها و قفل‌ها، برنامه‌های هم‌زمانی کارآمدی ایجاد کنند.

کلمات کلیدی async و await: این کلمات کلیدی امکان نوشتن کدهای ناهم‌زمان به صورت هم‌گام را فراهم می‌کنند که خوانایی کد را بهبود می‌بخشد و از تو در تو شدن کدها جلوگیری می‌کند.

func fetchData() async -> Data {
    // عملیات ناهم‌زمان مانند دریافت داده از شبکه
}

func loadData() async {
    let data = await fetchData()
    // پردازش داده دریافت شده
}

Actors: با استفاده از Actors، توسعه‌دهندگان می‌توانند به راحتی از مشکلات هم‌زمانی جلوگیری کنند بدون نیاز به مدیریت دستی قفل‌ها.

actor Counter {
    private var value = 0

    func increment() {
        value += 1
    }

    func getValue() -> Int {
        return value
    }
}

2. کارایی بالا

Swift Concurrency با بهینه‌سازی نحوه اجرای وظایف هم‌زمان، کارایی برنامه‌ها را بهبود می‌بخشد.

Task و Task Groups: این ساختارها به توسعه‌دهندگان اجازه می‌دهند تا وظایف را به صورت موازی و سازمان‌دهی‌شده اجرا کنند، که منجر به استفاده بهینه‌تر از منابع سیستم می‌شود.

func performTasks() async {
    await withTaskGroup(of: Void.self) { group in
        group.addTask {
            await task1()
        }
        group.addTask {
            await task2()
        }
    }
}

بهینه‌سازی زمان‌بندی: Swift Concurrency به طور خودکار زمان‌بندی وظایف را مدیریت می‌کند و اطمینان حاصل می‌کند که وظایف بهینه‌ترین مسیر ممکن برای اجرا دارند.

3. امنیت

Swift Concurrency با ارائه ساختارهای ایمن، از بروز مشکلات هم‌زمانی جلوگیری می‌کند.

Actor Isolation: Actors به طور خودکار دسترسی به داده‌ها را ایزوله می‌کنند و از دسترسی هم‌زمان غیرمجاز جلوگیری می‌کنند.

actor SafeCounter {
    private var count = 0

    func increment() {
        count += 1
    }

    func getCount() -> Int {
        return count
    }
}

Structured Concurrency: این مفهوم تضمین می‌کند که وظایف به درستی مدیریت و خاتمه یابند، که از بروز مشکلاتی مانند Resource Leaks جلوگیری می‌کند.

4. پشتیبانی از هم‌زمانی ساختاریافته

Structured Concurrency به توسعه‌دهندگان اجازه می‌دهد تا وظایف را در محدوده‌های مشخصی مدیریت کنند، که منجر به کدهای قابل پیش‌بینی‌تر و آسان‌تر برای نگهداری می‌شود.

Scopes و Hierarchies: وظایف می‌توانند در داخل scopes خاصی تعریف شوند که مدیریت آن‌ها را ساده‌تر می‌کند.

func performStructuredTasks() async {
    await withTaskGroup(of: Void.self) { group in
        group.addTask {
            await task1()
        }
        group.addTask {
            await task2()
        }
    }
    // وظایف پس از این نقطه خاتمه یافته‌اند
}

Error Handling: مدیریت خطاها در Swift Concurrency بهبود یافته است و به توسعه‌دهندگان اجازه می‌دهد تا به راحتی خطاهای ناشی از وظایف هم‌زمان را مدیریت کنند.

مثال‌های عملی از مزایا

ساده‌سازی کدهای ناهم‌زمان

با استفاده از async و await، کدهای ناهم‌زمان به صورت هم‌گام نوشته می‌شوند که خوانایی بیشتری دارند.

قبل از Swift Concurrency:

fetchData { data in
    processData(data) { result in
        updateUI(result)
    }
}

بعد از Swift Concurrency:

func loadData() async {
    let data = await fetchData()
    let result = await processData(data)
    updateUI(result)
}

افزایش کارایی با Task Groups

با استفاده از TaskGroup، می‌توان چندین وظیفه را به صورت هم‌زمان و بهینه اجرا کرد.

func fetchMultipleData() async {
    await withTaskGroup(of: Data.self) { group in
        for url in urls {
            group.addTask {
                await fetchData(from: url)
            }
        }
        for await data in group {
            processData(data)
        }
    }
}

Swift Concurrency با ارائه ابزارها و ساختارهای پیشرفته، توسعه‌دهندگان را قادر می‌سازد تا برنامه‌هایی هم‌زمان، کارآمد و ایمن بنویسند. با ساده‌سازی کدنویسی هم‌زمان، بهبود کارایی، افزایش امنیت و پشتیبانی از هم‌زمانی ساختاریافته، Swift Concurrency به یک ابزار ضروری برای توسعه‌دهندگان Swift تبدیل شده است. این امکانات نه تنها فرآیند توسعه را تسهیل می‌کنند، بلکه به ایجاد برنامه‌هایی پایدارتر و قابل اعتمادتر کمک می‌کنند.

async و await

یکی از مهم‌ترین ویژگی‌های هم‌زمانی (Concurrency) در Swift، استفاده از کلمات کلیدی async و await است. این کلمات کلیدی امکان نوشتن کدهای ناهم‌زمان را به صورت هم‌گام (Synchronous) فراهم می‌کنند، که باعث بهبود خوانایی و نگهداری کد می‌شود. در این بخش به بررسی دقیق‌تر async و await می‌پردازیم و نحوه استفاده از آن‌ها را با جزئیات بیشتری شرح می‌دهیم.

تعریف توابع async

توابع async توابعی هستند که می‌توانند به صورت ناهم‌زمان اجرا شوند. این توابع در زمان فراخوانی، کنترل را به کد فراخواننده بازمی‌گردانند تا نخ اصلی برنامه مسدود نشود و برنامه بتواند به اجرای سایر وظایف ادامه دهد. زمانی که یک تابع async فراخوانی می‌شود، می‌تواند عملیات‌هایی مانند دریافت داده از شبکه، خواندن از دیسک یا انجام محاسبات سنگین را بدون مسدود کردن نخ اصلی انجام دهد.

ساختار توابع async

برای تعریف یک تابع async، کافی است کلمه کلیدی async را قبل از علامت بازگشتی تابع قرار دهید. این توابع معمولاً با استفاده از کلمه کلیدی await در داخل آن‌ها، توابع ناهم‌زمان دیگر را فراخوانی می‌کنند.

func fetchData() async -> Data {
    // عملیات ناهم‌زمان مانند دریافت داده از شبکه
}

در مثال بالا، تابع fetchData به صورت ناهم‌زمان تعریف شده است و می‌تواند داده‌ها را از منابع مختلف بدون مسدود کردن نخ اصلی دریافت کند.

استفاده از await در فراخوانی توابع ناهم‌زمان

کلمه کلیدی await به شما اجازه می‌دهد تا منتظر تکمیل یک تابع async بمانید بدون اینکه نخ اصلی برنامه مسدود شود. با استفاده از await، کد شما به صورت هم‌گام نوشته می‌شود، در حالی که عملیات در پشت صحنه به صورت ناهم‌زمان اجرا می‌شود.

مثال عملی
در این مثال، تابع loadData از await برای فراخوانی تابع fetchData استفاده می‌کند. این باعث می‌شود که زمانی که fetchData در حال دریافت داده‌ها است، نخ اصلی برنامه مسدود نشود و بتواند به انجام سایر وظایف بپردازد.

func loadData() async {
    let data = await fetchData()
    // پردازش داده دریافت شده
}

مقایسه با روش‌های سنتی

قبل از معرفی async و await، توسعه‌دهندگان برای انجام عملیات ناهم‌زمان از روش‌هایی مانند کال‌بک‌ها (Callbacks) و پرومیس‌ها (Promises) استفاده می‌کردند. این روش‌ها ممکن است منجر به پیچیدگی کد و کاهش خوانایی شوند.

Callback Hell

یکی از مشکلات اصلی استفاده از کال‌بک‌ها، ایجاد ساختارهای تو در تو و پیچیده است که به آن Callback Hell می‌گویند. این امر باعث کاهش خوانایی کد و افزایش احتمال بروز خطاهای منطقی می‌شود.

doSomething(function(result) {
    doSomethingElse(result, function(newResult) {
        doAnotherThing(newResult, function(finalResult) {
            // پردازش نهایی
        });
    });
});

با استفاده از async و await، می‌توان این مشکلات را به راحتی برطرف کرد و کدهای ناهم‌زمان را به صورت هم‌گام و خوانا نوشت.

func loadData() async {
    let result = await doSomething()
    let newResult = await doSomethingElse(result)
    let finalResult = await doAnotherThing(newResult)
    // پردازش نهایی
}

مدیریت خطاها در async/await

یکی از ویژگی‌های مهم async و await، مدیریت ساده‌تر خطاها است. با استفاده از try و catch می‌توانید خطاهای ناشی از عملیات ناهم‌زمان را به راحتی مدیریت کنید.

مثال مدیریت خطا

در این مثال، تابع fetchData ممکن است خطاهایی را در زمان دریافت داده‌ها ایجاد کند. با استفاده از try و catch، می‌توان این خطاها را مدیریت کرد.

func fetchData() async throws -> Data {
    // عملیات ناهم‌زمان مانند دریافت داده از شبکه که ممکن است خطا ایجاد کند
}

func loadData() async {
    do {
        let data = try await fetchData()
        // پردازش داده دریافت شده
    } catch {
        print("خطا در دریافت داده: \(error)")
    }
}

استفاده از async/await در توابع تو در تو

توابع async می‌توانند از توابع async دیگر فراخوانی کنند و این امر اجازه می‌دهد تا عملیات‌های پیچیده ناهم‌زمان به صورت سلسله‌مراتبی و قابل فهم نوشته شوند.

مثال توابع تو در تو

func fetchUserData() async throws -> UserData {
    let user = try await fetchUser()
    let profile = try await fetchUserProfile(user.id)
    return UserData(user: user, profile: profile)
}

func loadUserData() async {
    do {
        let userData = try await fetchUserData()
        // پردازش داده‌های کاربر
    } catch {
        print("خطا در دریافت داده‌های کاربر: \(error)")
    }
}

در این مثال، تابع fetchUserData به ترتیب از توابع fetchUser و fetchUserProfile استفاده می‌کند تا داده‌های کاربر را به دست آورد. این ساختار باعث می‌شود کد خوانا و قابل فهم باشد.

اجرای توابع async از کد هم‌زمان

گاهی اوقات نیاز است که توابع async را از کد هم‌زمان فراخوانی کنیم. برای این منظور، می‌توان از Task استفاده کرد که اجازه می‌دهد توابع async در یک زمینه ناهم‌زمان اجرا شوند.

مثال اجرای async از کد هم‌زمان

func performAsyncTask() {
    Task {
        let data = await fetchData()
        // پردازش داده دریافت شده
    }
}

در این مثال، با استفاده از Task، تابع fetchData به صورت ناهم‌زمان اجرا می‌شود و کنترل به کد فراخواننده بازمی‌گردد.

مزایای استفاده از async/await در Swift

استفاده از async و await در هم‌زمانی (Concurrency) در Swift مزایای زیادی دارد که به شرح زیر می‌باشند:

خوانایی بهتر کد: کدهای ناهم‌زمان به صورت هم‌گام نوشته می‌شوند که باعث افزایش خوانایی و قابلیت فهم کد می‌شود.
کاهش پیچیدگی: با حذف نیاز به استفاده از کال‌بک‌ها و پرومیس‌ها، پیچیدگی کد کاهش می‌یابد.
مدیریت ساده‌تر خطاها: استفاده از try و catch برای مدیریت خطاها در توابع async بسیار ساده‌تر است.
سازگاری با ساختارهای موجود: توابع async می‌توانند به راحتی با ساختارهای موجود کد ترکیب شوند و از آن‌ها استفاده کنند.

استفاده از async و await یکی از پیشرفت‌های بزرگ در هم‌زمانی (Concurrency) در Swift است که به توسعه‌دهندگان امکان می‌دهد تا کدهای ناهم‌زمان را به صورت هم‌گام و خوانا بنویسند. این ویژگی‌ها نه تنها خوانایی و نگهداری کد را بهبود می‌بخشند، بلکه به کاهش پیچیدگی و افزایش امنیت کدهای هم‌زمان نیز کمک می‌کنند. با استفاده از async و await، توسعه‌دهندگان می‌توانند به راحتی وظایف پیچیده هم‌زمانی را مدیریت کنند و از مزایای بالای Swift Concurrency بهره‌مند شوند.

 

Taskها و Task Groupها

در هم‌زمانی (Concurrency) در Swift، Task و TaskGroup ابزارهای قدرتمندی هستند که به توسعه‌دهندگان امکان می‌دهند تا وظایف پیچیده هم‌زمانی را به صورت سازمان‌یافته و بهینه مدیریت کنند. در این بخش به بررسی عمیق‌تری از ساختار Task و TaskGroup، نحوه استفاده از آن‌ها و مزایای آن‌ها می‌پردازیم.

ساختار Task و Task Group

Task

Task در Swift نمایانگر یک واحد اجرایی است که می‌تواند به صورت هم‌زمان اجرا شود. با استفاده از Task، می‌توانید عملیات‌های ناهم‌زمان را در پس‌زمینه اجرا کنید بدون اینکه نخ اصلی برنامه مسدود شود. ایجاد یک Task بسیار ساده است و می‌تواند شامل عملیات‌های پیچیده‌ای مانند درخواست‌های شبکه، پردازش داده‌ها و غیره باشد.

ایجاد و اجرای یک Task

برای ایجاد و اجرای یک Task، کافی است از سازنده Task استفاده کنید و بلوک کد ناهم‌زمان خود را در داخل آن قرار دهید:

Task {
    await performTask()
}

Task {
    await performAnotherTask()
}

در این مثال، دو Task ایجاد شده‌اند که به صورت هم‌زمان اجرا می‌شوند و عملیات‌های performTask و performAnotherTask را انجام می‌دهند.

مدیریت اولویت Taskها

می‌توانید به Taskها اولویت‌های مختلفی اختصاص دهید تا مشخص کنید کدام وظایف باید اولویت بیشتری در اجرا داشته باشند:

Task(priority: .high) {
    await performHighPriorityTask()
}

Task(priority: .low) {
    await performLowPriorityTask()
}

لغو Taskها
هر Task قابلیت لغو شدن را دارد. برای لغو یک Task، می‌توانید از متد cancel() استفاده کنید:

let task = Task {
    await performCancellableTask()
}

// در زمانی که نیاز به لغو Task دارید
task.cancel()

لغو یک Task باعث می‌شود که عملیات در حال اجرا متوقف شود، اما باید در داخل Task بررسی شود که آیا لغو شده است یا خیر:

func performCancellableTask() async {
    if Task.isCancelled {
        return
    }
    // ادامه عملیات
}

 

TaskGroup

TaskGroup به شما اجازه می‌دهد تا چندین وظیفه مرتبط را در یک گروه مدیریت کنید. این ابزار به ویژه برای مدیریت وظایف موازی و جمع‌آوری نتایج آن‌ها بسیار مفید است. با استفاده از TaskGroup، می‌توانید به راحتی وظایف جدیدی به گروه اضافه کنید و منتظر تکمیل آن‌ها بمانید.

ایجاد و استفاده از TaskGroup

برای ایجاد یک TaskGroup، از تابع withTaskGroup استفاده می‌کنید. در داخل این تابع، می‌توانید وظایف را به گروه اضافه کنید و نتایج آن‌ها را مدیریت کنید:

func performTasks() async {
    await withTaskGroup(of: Void.self) { group in
        group.addTask {
            await task1()
        }
        group.addTask {
            await task2()
        }
    }
}

در این مثال، دو وظیفه به TaskGroup اضافه شده‌اند که به صورت هم‌زمان اجرا می‌شوند و پس از تکمیل هر دو وظیفه، TaskGroup به پایان می‌رسد.

جمع‌آوری نتایج از TaskGroup

اگر وظایف شما نتایجی برمی‌گردانند، می‌توانید نوع گروه را به نوع داده مورد نظر تغییر دهید و نتایج را جمع‌آوری کنید:

func fetchMultipleData() async -> [Data] {
    await withTaskGroup(of: Data?.self) { group in
        for url in urls {
            group.addTask {
                return await fetchData(from: url)
            }
        }
        
        var results: [Data] = []
        for await data in group {
            if let data = data {
                results.append(data)
            }
        }
        return results
    }
}

در این مثال، چندین درخواست شبکه به صورت هم‌زمان اجرا می‌شوند و نتایج آن‌ها در آرایه results جمع‌آوری می‌شوند.

مدیریت وظایف موازی

مدیریت وظایف موازی یکی از چالش‌های اصلی در برنامه‌نویسی هم‌زمانی است. با استفاده از TaskGroup، می‌توانید وظایف را به صورت سازمان‌یافته و بهینه مدیریت کنید و از هم‌گام بودن آن‌ها اطمینان حاصل کنید.

هماهنگی وظایف با TaskGroup

TaskGroup به شما امکان می‌دهد تا وظایف مرتبط را در یک گروه جمع‌آوری کنید و سپس بر روی نتایج آن‌ها عملیات انجام دهید. این امر به ویژه در مواقعی که نیاز به پردازش هم‌زمان چندین منبع داده دارید، بسیار مفید است.

مثال: دانلود چندین تصویر به صورت هم‌زمان
فرض کنید می‌خواهید چندین تصویر را به صورت هم‌زمان از اینترنت دانلود کنید. با استفاده از TaskGroup، می‌توانید این کار را به راحتی انجام دهید:

func downloadImages(from urls: [URL]) async -> [UIImage] {
    await withTaskGroup(of: UIImage?.self) { group in
        for url in urls {
            group.addTask {
                return await downloadImage(from: url)
            }
        }
        
        var images: [UIImage] = []
        for await image in group {
            if let image = image {
                images.append(image)
            }
        }
        return images
    }
}

func downloadImage(from url: URL) async -> UIImage? {
    do {
        let (data, _) = try await URLSession.shared.data(from: url)
        return UIImage(data: data)
    } catch {
        print("خطا در دانلود تصویر از \(url): \(error)")
        return nil
    }
}

در این مثال، تصاویر از چندین URL به صورت هم‌زمان دانلود می‌شوند و پس از تکمیل دانلود، تصاویر به آرایه images اضافه می‌شوند.

کنترل تعداد وظایف هم‌زمان

گاهی اوقات نیاز است که تعداد وظایف هم‌زمان محدود شود تا از مصرف بیش از حد منابع سیستم جلوگیری شود. با استفاده از TaskGroup و محدود کردن تعداد وظایف اضافه شده به گروه، می‌توانید این کنترل را اعمال کنید:

func fetchDataWithLimit(urls: [URL], limit: Int) async -> [Data] {
    await withTaskGroup(of: Data?.self) { group in
        var index = 0
        while index < urls.count {
            if group.count < limit {
                group.addTask {
                    return await fetchData(from: urls[index])
                }
                index += 1
            } else {
                await group.next()
            }
        }
        
        var results: [Data] = []
        for await data in group {
            if let data = data {
                results.append(data)
            }
        }
        return results
    }
}

در این مثال، تعداد وظایف هم‌زمان محدود به مقدار limit است و تا زمانی که تعداد وظایف فعال به این مقدار نرسد، وظایف جدید به گروه اضافه می‌شوند.

مزایا و کاربردها

استفاده از Task و TaskGroup در هم‌زمانی (Concurrency) در Swift مزایای فراوانی دارد که به شرح زیر هستند:

1. بهبود کارایی

با اجرای وظایف به صورت هم‌زمان و موازی، می‌توانید از منابع سیستم بهینه‌تر استفاده کنید و کارایی برنامه را افزایش دهید. این امر به ویژه در مواقعی که نیاز به انجام چندین عملیات سنگین دارید، بسیار مفید است.

2. ساده‌سازی مدیریت وظایف

با استفاده از TaskGroup، مدیریت چندین وظیفه مرتبط ساده‌تر و سازمان‌یافته‌تر می‌شود. این ابزار به شما اجازه می‌دهد تا به راحتی وظایف جدیدی به گروه اضافه کنید و نتایج آن‌ها را جمع‌آوری کنید.

3. افزایش خوانایی و نگهداری کد

استفاده از Task و TaskGroup به جای روش‌های سنتی مانند نخ‌ها و قفل‌ها، باعث افزایش خوانایی کد و کاهش پیچیدگی می‌شود. این امر به نگهداری و توسعه برنامه کمک می‌کند.

4. مدیریت خطاهای هم‌زمانی

با استفاده از TaskGroup، می‌توانید به راحتی خطاهای ناشی از وظایف هم‌زمان را مدیریت کنید و از بروز مشکلاتی مانند Race Condition جلوگیری کنید.

مثال‌های پیشرفته

1. پردازش داده‌های بزرگ به صورت هم‌زمان

فرض کنید شما نیاز دارید تا یک مجموعه بزرگ از داده‌ها را پردازش کنید. با استفاده از TaskGroup، می‌توانید این عملیات را به صورت هم‌زمان و بهینه انجام دهید:

func processLargeDataSet(_ dataSet: [Data]) async -> [ProcessedData] {
    await withTaskGroup(of: ProcessedData?.self) { group in
        for data in dataSet {
            group.addTask {
                return await processData(data)
            }
        }
        
        var results: [ProcessedData] = []
        for await processedData in group {
            if let processedData = processedData {
                results.append(processedData)
            }
        }
        return results
    }
}

func processData(_ data: Data) async -> ProcessedData? {
    // عملیات پردازش داده
}

2. هماهنگی درخواست‌های شبکه

در این مثال، چندین درخواست شبکه به صورت هم‌زمان ارسال می‌شوند و پس از دریافت پاسخ‌ها، نتایج جمع‌آوری می‌شوند:

func fetchAllData(from urls: [URL]) async -> [Data] {
    await withTaskGroup(of: Data?.self) { group in
        for url in urls {
            group.addTask {
                return await fetchData(from: url)
            }
        }
        
        var allData: [Data] = []
        for await data in group {
            if let data = data {
                allData.append(data)
            }
        }
        return allData
    }
}

func fetchData(from url: URL) async -> Data? {
    do {
        let (data, _) = try await URLSession.shared.data(from: url)
        return data
    } catch {
        print("Error fetching data from \(url): \(error)")
        return nil
    }
}

بهترین روش‌ها

برای استفاده بهینه از Task و TaskGroup در هم‌زمانی (Concurrency) در Swift، رعایت برخی بهترین روش‌ها توصیه می‌شود:

1. استفاده از Structured Concurrency

همیشه سعی کنید از TaskGroup و withTaskGroup استفاده کنید تا وظایف به صورت ساختاریافته مدیریت شوند. این کار باعث می‌شود که وظایف به درستی خاتمه یابند و از بروز مشکلاتی مانند نشت منابع جلوگیری شود.

2. مدیریت صحیح خطاها

همیشه خطاهای احتمالی در وظایف را مدیریت کنید تا از بروز مشکلات ناخواسته جلوگیری شود. می‌توانید از بلوک‌های do-catch برای مدیریت خطاها استفاده کنید:

await withTaskGroup(of: Data?.self) { group in
    for url in urls {
        group.addTask {
            do {
                let data = try await fetchData(from: url)
                return data
            } catch {
                print("Error fetching data from \(url): \(error)")
                return nil
            }
        }
    }
    
    var results: [Data] = []
    for await data in group {
        if let data = data {
            results.append(data)
        }
    }
    return results
}

3. محدود کردن تعداد وظایف هم‌زمان

برای جلوگیری از مصرف بیش از حد منابع سیستم، تعداد وظایف هم‌زمان را محدود کنید. این کار می‌تواند با استفاده از کنترل‌های منطقی در داخل TaskGroup انجام شود.

4. استفاده از اولویت‌های مناسب

با اختصاص دادن اولویت‌های مناسب به Taskها، می‌توانید اطمینان حاصل کنید که وظایف مهم‌تر با اولویت بالاتری اجرا شوند:

Task(priority: .high) {
    await performHighPriorityTask()
}

Task(priority: .low) {
    await performLowPriorityTask()
}

5. لغو وظایف غیرضروری

برای بهبود کارایی و جلوگیری از انجام وظایف غیرضروری، می‌توانید وظایف را در صورت لزوم لغو کنید:

let task = Task {
    await performTask()
}

// در زمانی که نیاز به لغو Task دارید
task.cancel()

استفاده از Task و TaskGroup در هم‌زمانی (Concurrency) در Swift ابزارهایی قدرتمند و انعطاف‌پذیر برای مدیریت وظایف هم‌زمان فراهم می‌کنند. این ابزارها با ساده‌سازی کدنویسی، بهبود کارایی و افزایش امنیت برنامه‌ها، توسعه‌دهندگان را قادر می‌سازند تا برنامه‌هایی پیچیده و هم‌زمان ایجاد کنند بدون اینکه نگران مشکلات سنتی هم‌زمانی باشند. با رعایت بهترین روش‌ها و استفاده صحیح از این ابزارها، می‌توانید به بهینه‌ترین شکل ممکن از امکانات هم‌زمانی Swift بهره‌مند شوید.

Actors

معرفی Actor به عنوان جایگزین کلاس در محیط هم‌زمانی

در هم‌زمانی (Concurrency) در Swift، Actorها به عنوان جایگزینی ایمن و موثر برای کلاس‌ها معرفی شده‌اند. Actorها نوع خاصی از کلاس‌ها هستند که به طور خاص برای مدیریت ایمن دسترسی به داده‌ها در محیط‌های هم‌زمان طراحی شده‌اند. هدف اصلی Actorها، جلوگیری از بروز مشکلات هم‌زمانی مانند Race Condition و Deadlock از طریق Actor Isolation است.

چرا Actor به جای کلاس‌ها؟

در برنامه‌نویسی هم‌زمان، مدیریت دسترسی همزمان به منابع مشترک یکی از چالش‌های اصلی است. کلاس‌های سنتی در Swift برای مدیریت این دسترسی‌ها نیازمند استفاده از قفل‌ها (Locks) و مکانیزم‌های همگام‌سازی پیچیده هستند که می‌تواند به کدهای پیچیده و مستعد خطا منجر شود. Actorها با ارائه یک مدل ساده‌تر و ایمن‌تر برای مدیریت دسترسی به داده‌ها، این مشکلات را به حداقل می‌رسانند.

ساختار یک Actor

ساختار Actor شبیه به کلاس‌ها است، اما تفاوت‌های کلیدی در نحوه مدیریت دسترسی به داده‌ها و توابع آن‌ها وجود دارد. در زیر یک مثال ساده از تعریف یک Actor آورده شده است:

actor Counter {
    private var value = 0

    func increment() {
        value += 1
    }

    func getValue() -> Int {
        return value
    }
}

در این مثال، Counter یک Actor است که دارای یک متغیر خصوصی value و دو تابع increment و getValue می‌باشد. دسترسی به value تنها از طریق این توابع امکان‌پذیر است، که این امر تضمین می‌کند که تغییرات به صورت ایمن و بدون تداخل رخ می‌دهند.

جلوگیری از Race Condition

یکی از مشکلات اصلی در برنامه‌نویسی هم‌زمانی، Race Condition است. این مشکل زمانی رخ می‌دهد که دو یا چند نخ به طور همزمان به منابع مشترک دسترسی پیدا کنند و ترتیب اجرای آن‌ها تأثیرگذار بر نتیجه نهایی باشد. Actorها با استفاده از Actor Isolation این مشکل را به طور موثر حل می‌کنند.

چگونه Actor از Race Condition جلوگیری می‌کند؟

Actorها تضمین می‌کنند که تنها یک وظیفه (Task) در یک زمان به داده‌های آن‌ها دسترسی دارد. این به معنای آن است که تمام توابع داخل Actor به صورت سریالی اجرا می‌شوند و بنابراین هیچ دو وظیفه‌ای نمی‌توانند به طور همزمان به داده‌های مشترک دسترسی پیدا کنند.

مثال عملی
فرض کنید شما یک Actor دارید که مسئول مدیریت تعداد کاربران آنلاین در یک برنامه است:

actor UserManager {
    private var onlineUsers = 0

    func userLoggedIn() {
        onlineUsers += 1
    }

    func userLoggedOut() {
        onlineUsers -= 1
    }

    func getOnlineUsers() -> Int {
        return onlineUsers
    }
}

اگر چندین نخ به طور همزمان توابع userLoggedIn و userLoggedOut را فراخوانی کنند، بدون استفاده از Actor، ممکن است مقدار onlineUsers نادرست شود. با استفاده از Actor، این توابع به صورت سریالی اجرا می‌شوند و از بروز Race Condition جلوگیری می‌کنند.

let userManager = UserManager()

Task {
    await userManager.userLoggedIn()
}

Task {
    await userManager.userLoggedIn()
}

Task {
    let count = await userManager.getOnlineUsers()
    print("Online Users: \(count)")
}

در این مثال، Actor تضمین می‌کند که دسترسی به onlineUsers به صورت ایمن و بدون تداخل انجام می‌شود.

Actor Isolation

Actor Isolation یکی از اصول کلیدی در هم‌زمانی (Concurrency) در Swift است که تضمین می‌کند دسترسی به داده‌های Actor تنها از طریق توابع ایمن آن امکان‌پذیر است. این اصل باعث می‌شود که داده‌های داخلی Actor از دسترسی مستقیم و غیر ایمن توسط نخ‌های دیگر محافظت شوند.

نحوه عملکرد Actor Isolation

هر Actor دارای یک صف (Queue) داخلی است که وظایف (Tasks) مربوط به آن را مدیریت می‌کند. زمانی که یک تابع از Actor فراخوانی می‌شود، این وظیفه به صف Actor اضافه می‌شود و به ترتیب اجرا می‌گردد. این امر تضمین می‌کند که تنها یک وظیفه در یک زمان به داده‌های Actor دسترسی دارد.

مثال: دسترسی ایمن به داده‌ها

فرض کنید شما یک Actor دارید که اطلاعات یک حساب کاربری را مدیریت می‌کند:

actor UserAccount {
    private var balance: Double = 0.0

    func deposit(amount: Double) {
        balance += amount
    }

    func withdraw(amount: Double) -> Bool {
        if balance >= amount {
            balance -= amount
            return true
        } else {
            return false
        }
    }

    func getBalance() -> Double {
        return balance
    }
}

با استفاده از Actor Isolation، توابع deposit و withdraw به صورت سریالی اجرا می‌شوند و از بروز مشکلاتی مانند Race Condition جلوگیری می‌کنند.

let account = UserAccount()

Task {
    await account.deposit(amount: 100.0)
}

Task {
    let success = await account.withdraw(amount: 50.0)
    print("Withdraw Successful: \(success)")
}

Task {
    let currentBalance = await account.getBalance()
    print("Current Balance: \(currentBalance)")
}

در این مثال، حتی اگر چندین نخ به طور همزمان توابع deposit و withdraw را فراخوانی کنند، Actor تضمین می‌کند که هر تابع به ترتیب اجرا شود و مقدار balance به درستی به‌روزرسانی شود.

مزایای Actor Isolation

ایمنی داده‌ها: با محدود کردن دسترسی به داده‌های داخلی Actor تنها از طریق توابع ایمن، از بروز خطاهای هم‌زمانی جلوگیری می‌شود.
سادگی در مدیریت هم‌زمانی: توسعه‌دهندگان نیازی به مدیریت دستی نخ‌ها و قفل‌ها ندارند، زیرا Actorها این کار را به صورت خودکار انجام می‌دهند.
کاهش پیچیدگی کد: با استفاده از Actorها، کدهای هم‌زمانی ساده‌تر و خواناتر می‌شوند.

مثال‌های پیشرفته از Actors

1. مدیریت وضعیت یک برنامه چت

فرض کنید شما در حال توسعه یک برنامه چت هستید و می‌خواهید تعداد پیام‌های ارسال شده را مدیریت کنید:

actor ChatManager {
    private var messageCount = 0

    func sendMessage(_ message: String) {
        // ارسال پیام
        messageCount += 1
    }

    func getMessageCount() -> Int {
        return messageCount
    }
}

با استفاده از ChatManager، شما می‌توانید مطمئن باشید که تعداد پیام‌ها به صورت ایمن و بدون تداخل به‌روزرسانی می‌شود.

let chatManager = ChatManager()

Task {
    await chatManager.sendMessage("Hello!")
}

Task {
    await chatManager.sendMessage("How are you?")
}

Task {
    let count = await chatManager.getMessageCount()
    print("Total Messages Sent: \(count)")
}

2. هماهنگی دسترسی به منابع مشترک

فرض کنید شما یک برنامه دارید که نیاز به دسترسی هم‌زمان به یک فایل دارد:

actor FileManagerActor {
    private let fileHandle: FileHandle

    init(filePath: String) {
        self.fileHandle = FileHandle(forWritingAtPath: filePath)!
    }

    func write(data: Data) {
        fileHandle.write(data)
    }

    func close() {
        fileHandle.closeFile()
    }
}

با استفاده از FileManagerActor، شما می‌توانید اطمینان حاصل کنید که نوشتن به فایل به صورت ایمن و بدون تداخل انجام می‌شود.

let fileManager = FileManagerActor(filePath: "/path/to/file.txt")

Task {
    let data = "First Line\n".data(using: .utf8)!
    await fileManager.write(data: data)
}

Task {
    let data = "Second Line\n".data(using: .utf8)!
    await fileManager.write(data: data)
}

Task {
    await fileManager.close()
}

تعامل بین Actors

Actorها می‌توانند با یکدیگر تعامل داشته باشند، اما باید به این نکته توجه شود که توابع آن‌ها باید به صورت async فراخوانی شوند تا اطمینان حاصل شود که دسترسی به داده‌ها به صورت ایمن انجام می‌شود.

مثال: تعامل دو Actor

فرض کنید شما دو Actor دارید که یکی مسئول مدیریت کاربران و دیگری مسئول مدیریت سفارش‌ها است:

actor UserManager {
    private var users: [String] = []

    func addUser(_ user: String) {
        users.append(user)
    }

    func getUsers() -> [String] {
        return users
    }
}

actor OrderManager {
    private var orders: [String] = []

    func placeOrder(_ order: String) {
        orders.append(order)
    }

    func getOrders() -> [String] {
        return orders
    }
}

شما می‌توانید این Actorها را به صورت هم‌زمان مدیریت کنید:

let userManager = UserManager()
let orderManager = OrderManager()

Task {
    await userManager.addUser("Alice")
    await orderManager.placeOrder("Order1")
}

Task {
    await userManager.addUser("Bob")
    await orderManager.placeOrder("Order2")
}

Task {
    let users = await userManager.getUsers()
    let orders = await orderManager.getOrders()
    print("Users: \(users)")
    print("Orders: \(orders)")
}

در این مثال، Actorها به صورت مستقل و ایمن عمل می‌کنند و دسترسی به داده‌های آن‌ها بدون تداخل و به صورت هم‌زمان مدیریت می‌شود.

Actorها یکی از ابزارهای کلیدی در هم‌زمانی (Concurrency) در Swift هستند که با ارائه یک مدل ساده و ایمن برای مدیریت دسترسی به داده‌ها، توسعه‌دهندگان را قادر می‌سازند تا برنامه‌هایی پیچیده و هم‌زمان ایجاد کنند بدون اینکه نگران مشکلات سنتی هم‌زمانی مانند Race Condition باشند. با استفاده از Actor Isolation، Actorها تضمین می‌کنند که داده‌ها تنها از طریق توابع ایمن و به صورت سریالی دسترسی پیدا می‌کنند، که این امر باعث افزایش ایمنی، کارایی و خوانایی کد می‌شود. به علاوه، Actorها با کاهش نیاز به مدیریت دستی نخ‌ها و قفل‌ها، فرآیند توسعه را ساده‌تر و سریع‌تر می‌کنند.

Structured Concurrency

ساختار کنترل‌شدهٔ وظایف (Tasks) در بلاک‌های do، TaskGroup و …

Structured Concurrency در هم‌زمانی (Concurrency) در Swift به معنای مدیریت وظایف به صورت ساختار یافته و کنترل‌شده است. این روش تضمین می‌کند که وظایف در محدوده مشخصی اجرا شوند و منابع به درستی آزاد گردند. با استفاده از Structured Concurrency، توسعه‌دهندگان می‌توانند اطمینان حاصل کنند که وظایف ناهم‌زمان به صورت منظم مدیریت می‌شوند و از بروز مشکلاتی مانند نشت منابع و پیچیدگی‌های غیرقابل پیش‌بینی جلوگیری می‌شود.

مفهوم Structured Concurrency

Structured Concurrency مفهومی است که وظایف (Tasks) را به گونه‌ای سازمان‌دهی می‌کند که هر وظیفه دارای یک والد مشخص باشد و به محض پایان وظیفه والد، تمامی وظایف فرزند نیز خاتمه یابند. این ساختار به جلوگیری از اجرای وظایف نامرتب و بدون کنترل کمک می‌کند و باعث می‌شود که مدیریت منابع و خطاها ساده‌تر گردد.

استفاده از withTaskGroup

یکی از ابزارهای اصلی در Structured Concurrency، استفاده از withTaskGroup است. این تابع به شما اجازه می‌دهد تا چندین وظیفه مرتبط را در یک گروه مدیریت کنید و از هم‌زمانی آن‌ها اطمینان حاصل کنید. همچنین، اطمینان می‌دهد که تمام وظایف گروه قبل از خروج از بلوک withTaskGroup تکمیل شده‌اند.

مثال ساده از withTaskGroup

در این مثال، دو وظیفه به صورت هم‌زمان اجرا می‌شوند و پس از تکمیل هر دو وظیفه، گروه به پایان می‌رسد:

func structuredConcurrencyExample() async {
    await withTaskGroup(of: Void.self) { group in
        group.addTask {
            await task1()
        }
        group.addTask {
            await task2()
        }
    }
}

در اینجا، task1 و task2 به صورت هم‌زمان اجرا می‌شوند و withTaskGroup تضمین می‌کند که هر دو وظیفه قبل از ادامه برنامه تکمیل شوند.

جمع‌آوری نتایج از TaskGroup

اگر وظایف شما نیاز به بازگشت نتایجی دارند، می‌توانید نوع گروه را به نوع داده مورد نظر تغییر دهید و نتایج را جمع‌آوری کنید:

func fetchMultipleData() async -> [Data] {
    await withTaskGroup(of: Data?.self) { group in
        for url in urls {
            group.addTask {
                return await fetchData(from: url)
            }
        }
        
        var results: [Data] = []
        for await data in group {
            if let data = data {
                results.append(data)
            }
        }
        return results
    }
}

در این مثال، چندین درخواست شبکه به صورت هم‌زمان اجرا می‌شوند و نتایج آن‌ها در آرایه results جمع‌آوری می‌شوند.

بلوک‌های do و TaskGroup

استفاده از بلوک‌های do به همراه TaskGroup امکان مدیریت بهتر خطاها و کنترل دقیق‌تر وظایف را فراهم می‌کند. در اینجا یک مثال پیشرفته‌تر از استفاده همزمان do و withTaskGroup آورده شده است:

func performStructuredTasks() async {
    do {
        try await withThrowingTaskGroup(of: Void.self) { group in
            group.addTask {
                try await task1()
            }
            group.addTask {
                try await task2()
            }
        }
    } catch {
        print("خطا در اجرای وظایف: \(error)")
    }
}

در این مثال، اگر هر یک از وظایف task1 یا task2 خطایی ایجاد کنند، بلوک catch آن را مدیریت می‌کند و از بروز مشکلات بزرگ‌تر جلوگیری می‌کند.

اصول طراحی هم‌زمانی ایمن

برای طراحی هم‌زمانی ایمن در هم‌زمانی (Concurrency) در Swift، باید از اصولی پیروی کرد که از بروز مشکلات هم‌زمانی جلوگیری کنند و کدهای قابل اعتماد و پایدار ایجاد نمایند. در ادامه به برخی از این اصول اشاره می‌شود:

1. Actor Isolation

استفاده از Actorها برای ایزوله کردن دسترسی به داده‌های مشترک یکی از اصول کلیدی هم‌زمانی ایمن است. Actorها تضمین می‌کنند که تنها یک وظیفه در یک زمان به داده‌های آن‌ها دسترسی دارد و از بروز Race Condition جلوگیری می‌کنند.

2. Avoiding Shared Mutable State

داده‌های مشترک که قابل تغییر هستند (Mutable) باید به حداقل برسند. اگر نیاز به استفاده از داده‌های مشترک است، باید از Actorها یا سایر مکانیزم‌های ایمن استفاده کرد تا از دسترسی همزمان جلوگیری شود.

3. Using Task Groups

استفاده از TaskGroupها برای مدیریت وظایف هم‌زمان، اطمینان حاصل می‌کند که وظایف به صورت ساختاریافته مدیریت می‌شوند و منابع به درستی آزاد می‌گردند. این روش به جلوگیری از نشت منابع و کاهش پیچیدگی کد کمک می‌کند.

مدیریت خطاها در Structured Concurrency

یکی از مزایای Structured Concurrency، مدیریت ساده‌تر و موثرتر خطاها است. با استفاده از بلوک‌های do-catch و withThrowingTaskGroup، می‌توانید خطاهای ناشی از وظایف هم‌زمان را به راحتی مدیریت کنید:

func fetchDataWithHandling() async {
    do {
        try await withThrowingTaskGroup(of: Data.self) { group in
            for url in urls {
                group.addTask {
                    let data = try await fetchData(from: url)
                    return data
                }
            }
            
            for try await data in group {
                processData(data)
            }
        }
    } catch {
        print("خطا در دریافت داده‌ها: \(error)")
    }
}

در این مثال، اگر هر یک از وظایف fetchData خطایی ایجاد کنند، خطا در بلوک catch مدیریت می‌شود و برنامه می‌تواند به درستی واکنش نشان دهد.

لغو وظایف در Structured Concurrency

لغو وظایف (Cancellation) یکی از ویژگی‌های مهم در Structured Concurrency است که امکان مدیریت بهینه وظایف را فراهم می‌کند. با استفاده از متد cancel() می‌توانید وظایف غیرضروری را لغو کنید:

func performCancelableTasks() async {
    let task = Task {
        await performLongRunningTask()
    }
    
    // در زمانی که نیاز به لغو Task دارید
    task.cancel()
}

همچنین، در داخل وظیفه باید بررسی شود که آیا وظیفه لغو شده است یا خیر تا عملیات متوقف شود:

func performLongRunningTask() async {
    for i in 1...1000 {
        if Task.isCancelled {
            print("وظیفه لغو شد")
            return
        }
        // ادامه عملیات
    }
}

بهترین روش‌ها برای استفاده از Structured Concurrency

برای بهره‌برداری بهینه از Structured Concurrency در هم‌زمانی (Concurrency) در Swift، رعایت برخی بهترین روش‌ها توصیه می‌شود:

1. تعریف وظایف در محدوده مشخص

همیشه سعی کنید وظایف را در محدوده‌های مشخص تعریف کنید تا مدیریت آن‌ها ساده‌تر شود و از بروز نشت منابع جلوگیری شود.

2. استفاده از TaskGroup برای وظایف مرتبط

وظایفی که به هم مرتبط هستند و باید به صورت هم‌زمان اجرا شوند را در یک TaskGroup قرار دهید تا مدیریت آن‌ها ساده‌تر و منظم‌تر گردد.

3. مدیریت خطاها به صورت جامع

همواره خطاهای ممکن در وظایف را مدیریت کنید تا از بروز مشکلات بزرگ‌تر جلوگیری شود. استفاده از withThrowingTaskGroup و بلوک‌های do-catch می‌تواند در این زمینه کمک‌کننده باشد.

4. محدود کردن تعداد وظایف هم‌زمان

برای جلوگیری از مصرف بیش از حد منابع سیستم، تعداد وظایف هم‌زمان را محدود کنید. این کار می‌تواند با استفاده از کنترل‌های منطقی در داخل TaskGroup انجام شود.

5. استفاده از اولویت‌های مناسب برای وظایف

با اختصاص دادن اولویت‌های مناسب به Taskها، می‌توانید اطمینان حاصل کنید که وظایف مهم‌تر با اولویت بالاتری اجرا می‌شوند و منابع سیستم بهینه‌تری مصرف می‌شوند.

مثال‌های پیشرفته از Structured Concurrency

1. پردازش هم‌زمان چندین فایل

فرض کنید شما نیاز دارید چندین فایل را به صورت هم‌زمان پردازش کنید. با استفاده از TaskGroup، می‌توانید این کار را به صورت سازمان‌یافته و بهینه انجام دهید:

func processFiles(urls: [URL]) async -> [ProcessedFile] {
    await withTaskGroup(of: ProcessedFile?.self) { group in
        for url in urls {
            group.addTask {
                return await processFile(at: url)
            }
        }
        
        var results: [ProcessedFile] = []
        for await processedFile in group {
            if let file = processedFile {
                results.append(file)
            }
        }
        return results
    }
}

func processFile(at url: URL) async -> ProcessedFile? {
    do {
        let data = try await Data(contentsOf: url)
        // پردازش داده‌ها
        return ProcessedFile(data: data)
    } catch {
        print("خطا در پردازش فایل \(url): \(error)")
        return nil
    }
}

در این مثال، چندین فایل به صورت هم‌زمان پردازش می‌شوند و نتایج آن‌ها در آرایه results جمع‌آوری می‌شوند.

2. هماهنگی درخواست‌های API

فرض کنید شما نیاز به ارسال چندین درخواست API به صورت هم‌زمان دارید و می‌خواهید نتایج آن‌ها را جمع‌آوری کنید:

func fetchAllAPIData(endpoints: [URL]) async -> [APIResponse] {
    await withTaskGroup(of: APIResponse?.self) { group in
        for endpoint in endpoints {
            group.addTask {
                return await fetchAPI(from: endpoint)
            }
        }
        
        var responses: [APIResponse] = []
        for await response in group {
            if let response = response {
                responses.append(response)
            }
        }
        return responses
    }
}

func fetchAPI(from url: URL) async -> APIResponse? {
    do {
        let (data, _) = try await URLSession.shared.data(from: url)
        let response = try JSONDecoder().decode(APIResponse.self, from: data)
        return response
    } catch {
        print("خطا در دریافت داده از \(url): \(error)")
        return nil
    }
}

 

در این مثال، چندین درخواست API به صورت هم‌زمان ارسال می‌شوند و پاسخ‌های آن‌ها در آرایه responses جمع‌آوری می‌شوند. Structured Concurrency یکی از اصول کلیدی در هم‌زمانی (Concurrency) در Swift است که با ارائه یک چارچوب ساختار یافته برای مدیریت وظایف ناهم‌زمان، به توسعه‌دهندگان امکان می‌دهد تا برنامه‌هایی منظم‌تر، قابل اعتمادتر و پایدارتر ایجاد کنند. با استفاده از ابزارهایی مانند TaskGroup و رعایت اصول طراحی هم‌زمانی ایمن، می‌توانید از بروز مشکلات سنتی هم‌زمانی مانند Race Condition و نشت منابع جلوگیری کنید و برنامه‌های بهینه‌تری توسعه دهید.

AsyncSequence و AsyncStream

ایجاد توالی ناهم‌زمان

در هم‌زمانی (Concurrency) در Swift، AsyncSequence و AsyncStream ابزارهایی قدرتمند برای ایجاد و مدیریت توالی‌های ناهم‌زمان هستند. این ابزارها به توسعه‌دهندگان امکان می‌دهند تا داده‌ها را به صورت جریان (Stream) پردازش کنند، که این امر به ویژه در مواقعی که نیاز به دریافت و پردازش داده‌ها به صورت پیوسته و بدون وقفه داریم، بسیار مفید است. در این بخش به بررسی دقیق‌تر AsyncSequence و AsyncStream، نحوه استفاده از آن‌ها و مزایای آن‌ها می‌پردازیم.

مفهوم AsyncSequence

AsyncSequence پروتکل‌ای است که به انواع داده‌ها اجازه می‌دهد تا به صورت ناهم‌زمان توالی‌ای از عناصر را تولید کنند. این توالی‌ها می‌توانند شامل داده‌هایی باشند که به تدریج و در طول زمان تولید می‌شوند، مانند دریافت داده‌های شبکه، خواندن از فایل‌های بزرگ، یا دریافت رویدادهای کاربری.

پیاده‌سازی AsyncSequence

برای پیاده‌سازی یک AsyncSequence، باید پروتکل AsyncSequence را پیاده‌سازی کرده و یک AsyncIterator تعریف کنید که مسئول تولید عناصر توالی است. در زیر یک مثال ساده از پیاده‌سازی AsyncSequence آورده شده است:

struct NumberGenerator: AsyncSequence {
    typealias Element = Int

    struct AsyncIterator: AsyncIteratorProtocol {
        var current = 0

        mutating func next() async -> Int? {
            defer { current += 1 }
            return current < 10 ? current : nil
        }
    }

    func makeAsyncIterator() -> AsyncIterator {
        return AsyncIterator()
    }
}

Task {
    for await number in NumberGenerator() {
        print(number)
    }
}

در این مثال، NumberGenerator یک AsyncSequence است که اعداد از ۰ تا ۹ را به صورت ناهم‌زمان تولید می‌کند. تابع next() در AsyncIterator هر بار یک عدد جدید را تولید می‌کند تا زمانی که به حداقل تعیین شده برسد.

مفهوم AsyncStream

AsyncStream نوعی ساختار است که به سرعت امکان ایجاد توالی‌های ناهم‌زمان را فراهم می‌کند بدون نیاز به پیاده‌سازی کامل پروتکل AsyncSequence. این ساختار به ویژه برای مواردی که نیاز به تولید داده‌ها به صورت پویا و در طول زمان داریم، بسیار مناسب است.

ایجاد AsyncStream

با استفاده از AsyncStream، می‌توانید به راحتی یک توالی ناهم‌زمان ایجاد کنید که داده‌ها را به صورت جریان تولید می‌کند. در زیر یک مثال از ایجاد و استفاده از AsyncStream آورده شده است:

func numberStream() -> AsyncStream<Int> {
    AsyncStream { continuation in
        for i in 0..<10 {
            continuation.yield(i)
        }
        continuation.finish()
    }
}

Task {
    for await number in numberStream() {
        print(number)
    }
}

در این مثال، numberStream یک AsyncStream است که اعداد از ۰ تا ۹ را به صورت جریان تولید می‌کند. بلوک continuation مسئول تولید عناصر و پایان دادن به توالی است.

کاربرد در پردازش داده‌های استریم

استفاده از AsyncSequence و AsyncStream در پردازش داده‌های استریم بسیار مفید است، زیرا این ابزارها به شما امکان می‌دهند تا داده‌ها را به صورت پیوسته و بدون وقفه دریافت و پردازش کنید. این ویژگی به ویژه در موارد زیر کاربرد دارد:

1. دریافت داده‌های شبکه به صورت پیوسته

در برنامه‌های موبایل و وب، اغلب نیاز به دریافت داده‌ها از شبکه به صورت پیوسته داریم، مانند دریافت پیام‌های جدید در یک برنامه چت یا دریافت داده‌های زنده از یک سرویس API. با استفاده از AsyncSequence و AsyncStream، می‌توانید این داده‌ها را به صورت ناهم‌زمان دریافت و پردازش کنید.

مثال: دریافت پیام‌های چت به صورت پیوسته

func chatMessageStream() -> AsyncStream<String> {
    AsyncStream { continuation in
        // فرض کنید این یک کلاینت چت است که پیام‌ها را دریافت می‌کند
        ChatClient.onMessageReceived { message in
            continuation.yield(message)
        }
        
        // زمانی که چت خاتمه یافت
        ChatClient.onChatEnded {
            continuation.finish()
        }
    }
}

Task {
    for await message in chatMessageStream() {
        print("پیام جدید: \(message)")
    }
}

در این مثال، chatMessageStream به صورت ناهم‌زمان پیام‌های جدید را از یک کلاینت چت دریافت می‌کند و آن‌ها را به صورت جریان تولید می‌کند.

2. پردازش رویدادهای کاربری

در برنامه‌های تعاملی، مانند برنامه‌های گرافیکی یا بازی‌ها، نیاز به پردازش رویدادهای کاربری به صورت پیوسته داریم. با استفاده از AsyncSequence و AsyncStream، می‌توانیم این رویدادها را به صورت ناهم‌زمان دریافت و پردازش کنیم.

مثال: دریافت رویدادهای کلیک کاربر

func userClickStream() -> AsyncStream<Void> {
    AsyncStream { continuation in
        Button.onClick {
            continuation.yield(())
        }
        
        Button.onDestroy {
            continuation.finish()
        }
    }
}

Task {
    for await _ in userClickStream() {
        print("کاربر کلیک کرد!")
    }
}

در این مثال، userClickStream رویدادهای کلیک کاربر را به صورت ناهم‌زمان دریافت می‌کند و هر بار که کاربر کلیک می‌کند، یک پیام چاپ می‌شود.

3. خواندن از فایل‌های بزرگ

در برخی از برنامه‌ها، نیاز به خواندن داده‌ها از فایل‌های بزرگ داریم که نمی‌توان آن‌ها را به صورت هم‌زمان در حافظه بارگذاری کرد. با استفاده از AsyncSequence و AsyncStream، می‌توانیم داده‌ها را به صورت تکه‌تکه و ناهم‌زمان از فایل خوانده و پردازش کنیم.

مثال: خواندن خط به خط یک فایل متنی

func fileLineStream(fileURL: URL) -> AsyncStream<String> {
    AsyncStream { continuation in
        do {
            let fileHandle = try FileHandle(forReadingFrom: fileURL)
            while let line = fileHandle.readLine() {
                continuation.yield(line)
            }
            continuation.finish()
        } catch {
            continuation.finish(throwing: error)
        }
    }
}

Task {
    do {
        for try await line in fileLineStream(fileURL: someURL) {
            print("خط جدید: \(line)")
        }
    } catch {
        print("خطا در خواندن فایل: \(error)")
    }
}

در این مثال، fileLineStream به صورت ناهم‌زمان خط به خط یک فایل متنی را خوانده و هر خط را به عنوان یک عنصر در توالی تولید می‌کند.

مزایا و کاربردهای AsyncSequence و AsyncStream

استفاده از AsyncSequence و AsyncStream در هم‌زمانی (Concurrency) در Swift دارای مزایای متعددی است که به شرح زیر می‌باشد:

1. خوانایی و سادگی بیشتر کد

با استفاده از AsyncSequence و AsyncStream، می‌توانید کدهای ناهم‌زمان را به صورت هم‌گام و خوانا بنویسید. این ابزارها از پیچیدگی‌های مدیریت دستی نخ‌ها و قفل‌ها جلوگیری می‌کنند و کدهای ساده‌تر و قابل فهم‌تری ایجاد می‌کنند.

2. مدیریت بهینه منابع

این ابزارها به صورت خودکار مدیریت منابع را انجام می‌دهند و اطمینان حاصل می‌کنند که منابع به درستی آزاد می‌شوند. این امر به جلوگیری از نشت منابع و بهبود کارایی برنامه کمک می‌کند.

3. انعطاف‌پذیری بالا

AsyncSequence و AsyncStream به شما امکان می‌دهند تا توالی‌های پیچیده‌ای از داده‌ها را به صورت ناهم‌زمان مدیریت کنید. این ابزارها قابلیت استفاده در انواع مختلفی از کاربردها را دارند، از جمله دریافت داده‌های شبکه، پردازش رویدادهای کاربری، و خواندن از فایل‌های بزرگ.

4. ترکیب با سایر ابزارهای هم‌زمانی

این ابزارها به خوبی با سایر امکانات هم‌زمانی Swift مانند Task، TaskGroup، و Actors ترکیب می‌شوند و امکان ایجاد ساختارهای پیچیده‌تر و کارآمدتر را فراهم می‌کنند.

مثال‌های پیشرفته از AsyncSequence و AsyncStream

1. دریافت داده‌های زنده از یک سرویس API

فرض کنید شما نیاز دارید داده‌های زنده را از یک سرویس API دریافت کنید و آن‌ها را به صورت پیوسته نمایش دهید. با استفاده از AsyncStream می‌توانید این کار را به صورت ناهم‌زمان انجام دهید:

func liveDataStream(from url: URL) -> AsyncStream<Data> {
    AsyncStream { continuation in
        let task = URLSession.shared.dataTask(with: url) { data, response, error in
            if let data = data {
                continuation.yield(data)
            }
            if let error = error {
                continuation.finish(throwing: error)
            }
        }
        task.resume()
        
        continuation.onTermination = { _ in
            task.cancel()
        }
    }
}

Task {
    do {
        for try await data in liveDataStream(from: liveDataURL) {
            processLiveData(data)
        }
    } catch {
        print("خطا در دریافت داده‌های زنده: \(error)")
    }
}

در این مثال، liveDataStream داده‌های زنده را از یک سرویس API دریافت می‌کند و آن‌ها را به صورت جریان تولید می‌کند. هر داده دریافت شده به صورت ناهم‌زمان پردازش می‌شود.

2. پردازش هم‌زمان چندین جریان داده

با استفاده از AsyncSequence و AsyncStream، می‌توانید چندین جریان داده را به صورت هم‌زمان مدیریت و پردازش کنید:

func combinedDataStream(urls: [URL]) -> AsyncStream<Data> {
    AsyncStream { continuation in
        Task {
            await withTaskGroup(of: Void.self) { group in
                for url in urls {
                    group.addTask {
                        do {
                            let (data, _) = try await URLSession.shared.data(from: url)
                            continuation.yield(data)
                        } catch {
                            print("خطا در دریافت داده از \(url): \(error)")
                        }
                    }
                }
                await group.waitForAll()
                continuation.finish()
            }
        }
    }
}

Task {
    for await data in combinedDataStream(urls: dataURLs) {
        handleData(data)
    }
}

در این مثال، combinedDataStream چندین درخواست داده را به صورت هم‌زمان ارسال می‌کند و داده‌های دریافت شده را به صورت جریان تولید می‌کند. این امر به شما اجازه می‌دهد تا داده‌ها را به صورت هم‌زمان و بهینه پردازش کنید.

بهترین روش‌ها برای استفاده از AsyncSequence و AsyncStream

برای بهره‌برداری بهینه از AsyncSequence و AsyncStream در هم‌زمانی (Concurrency) در Swift، رعایت برخی بهترین روش‌ها توصیه می‌شود:

1. استفاده از Structured Concurrency

همواره سعی کنید از اصول Structured Concurrency پیروی کنید و وظایف ناهم‌زمان را در محدوده‌های مشخص تعریف کنید. این کار به مدیریت بهتر وظایف و جلوگیری از نشت منابع کمک می‌کند.

2. مدیریت خطاها به صورت جامع

همواره خطاهای احتمالی در توالی‌های ناهم‌زمان را مدیریت کنید. استفاده از بلوک‌های do-catch و توابع withThrowingTaskGroup می‌تواند در این زمینه بسیار مفید باشد.

3. محدود کردن تعداد عناصر جریان

برای جلوگیری از مصرف بیش از حد حافظه و منابع، تعداد عناصر تولید شده در جریان را محدود کنید. این کار می‌تواند با استفاده از کنترل‌های منطقی در داخل توابع تولید جریان انجام شود.

4. استفاده از توابع کمکی برای ساده‌سازی کد

برای پیچیدگی‌های خاص مانند پردازش داده‌های بزرگ یا هماهنگی چندین جریان داده، از توابع کمکی و الگوهای طراحی مناسب استفاده کنید تا کدهای شما ساده‌تر و قابل فهم‌تر باشند.

5. استفاده از Actors برای مدیریت داده‌های مشترک

اگر نیاز به مدیریت داده‌های مشترک در توالی‌های ناهم‌زمان دارید، از Actorها برای ایزوله کردن دسترسی به این داده‌ها استفاده کنید تا از بروز مشکلات هم‌زمانی جلوگیری شود.

استفاده از AsyncSequence و AsyncStream در هم‌زمانی (Concurrency) در Swift ابزارهای قدرتمندی برای ایجاد و مدیریت توالی‌های ناهم‌زمان فراهم می‌کنند. این ابزارها با ارائه یک مدل ساده و خوانا برای پردازش داده‌ها به صورت جریان، توسعه‌دهندگان را قادر می‌سازند تا برنامه‌هایی پیچیده و هم‌زمان ایجاد کنند بدون اینکه نگران مشکلات سنتی هم‌زمانی مانند Race Condition باشند. با رعایت بهترین روش‌ها و استفاده صحیح از این ابزارها، می‌توانید از امکانات پیشرفته‌ی هم‌زمانی Swift بهره‌مند شوید و برنامه‌هایی کارآمد، ایمن و قابل نگهداری‌تر توسعه دهید.

نتیجه‌گیری

در این مقاله، به بررسی جامع هم‌زمانی (Concurrency) در Swift پرداختیم و تمامی جنبه‌های این موضوع را از سطح مبتدی تا پیشرفته مورد بررسی قرار دادیم. هم‌زمانی (Concurrency) در Swift با ارائه ابزارها و ساختارهای پیشرفته‌ای مانند async و await، Task و TaskGroup، Actors، و AsyncSequence و AsyncStream، توسعه‌دهندگان را قادر ساخته است تا برنامه‌هایی کارآمدتر، ایمن‌تر و قابل نگهداری‌تر ایجاد کنند. برای بهره‌برداری کامل از امکانات هم‌زمانی (Concurrency) در Swift، توصیه می‌شود که توسعه‌دهندگان به مطالعه و تمرین مداوم در این حوزه ادامه دهند و با رعایت بهترین روش‌ها، کدهای ایمن و بهینه بنویسند. همچنین، آشنایی عمیق‌تر با مفاهیم پیشرفته‌تر و ابزارهای موجود، می‌تواند به توسعه‌دهندگان کمک کند تا برنامه‌هایی قدرتمندتر و کارآمدتر ایجاد کنند.

آموزش هم‌زمانی (Concurrency) در Swift

دیدگاه های شما

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *