Sobes.tech
Middle+

თქვენ გეცნობათ სინქრონიზაციის მექანიზმები, როგორიცაა spinlock-ები და deadlock-ები?

sobes.tech AI

პასუხი AI-სგან

დიახ, ვიცნობ.

სპინლოკი არის სინქრონიზაციის მექანიზმი, სადაც ნაკადი, რომელიც ვერ იღებს რესურსის წვდომას, მუდმივად ამოწმებს მის ხელმისაწვდომობას ციკლში, "მორბის" (spinning), ნაცვლად იმისა, რომ გადავიდეს მოსალოდნელ მდგომარეობაზე (ბლოკირება). ეფექტურია კრიტიკული სექციების დაცვისთვის, რომლებიც სავარაუდოდ, ძალიან მოკლე დროში იქნება დაკავებული. თავიდან აიცილებს კონტექსტის გადართვის ხარჯებს, მაგრამ შეიძლება ბევრი პროცესორის დრო დაიხარჯოს, თუ ბლოკირება ხანგრძლივია.

მიუტექსი (ინგლისურად. mutually exclusive - ურთიერთგამორიცხული) არის სინქრონიზაციის ობიექტი, რომელიც საშუალებას აძლევს რამდენიმე ნაკადს ერთ რესურსზე ერთდროულად მუშაობა, მაგრამ უზრუნველყოფს, რომ ნებისმიერ დროს რესურსი გამოიყენება მხოლოდ ერთ ნაკადზე. თუ ნაკადი ვერ იღებს მიუტექსს, ის ბლოკდება და გადადის მოსალოდნელ მდგომარეობაზე, გამონთავისუფლებულ პროცესორზე. მიუტექსები შესაფერისია კრიტიკული სექციების დაცვისთვის, რომლებიც შეიძლება გაგრძელდეს ხანგრძლივად.

ურთიერთდაბლოკვა (deadlock) არის მდგომარეობა, როდესაც ორი ან მეტი ნაკადი სამუდამოდ ელოდება ერთმანეთს რესურსების განთავისუფლებას. წარმოიქმნება, როდესაც შესრულებულია კოფმანის ოთხი პირობა:

  1. ურთიერთგამორიცხულობა: რესურსები არ შეიძლება გამოყენებულ იქნას ერთდროულად რამდენიმე ნაკადზე.
  2. დამკვიდრება და ელოდება: ნაკადები ინარჩუნებენ უკვე გამოყოფილ რესურსებს და ელოდებიან ახალებს.
  3. მძიმე განთავისუფლება არ არის: რესურსები არ შეიძლება ძალით ამოიღოს მათზე მყოფი ნაკადებიდან.
  4. ციკლური ელოდება: არსებობს ციკლი ორი ან მეტი ნაკადის შორის, სადაც თითოეული ელოდება რესურსს, რომელიც დაკავებულია შემდეგი ნაკადის მიერ ციკლში.

ეს სიტუაციის თავიდან აცილებისთვის, შეიძლება გამოყენებულ იქნას სხვადასხვა სტრატეგიები, მაგალითად:

  • ერთდროულად რამდენიმე ბლოკირების მიღების თავიდან აცილება;
  • ბლოკირების მიღების წესის განსაზღვრა;
  • გამოვლენის და აღმოფხვრის მექანიზმების გამოყენება.

C++-ში std::atomic_flag გამოყენებით სპინლოკის მაგალითი:

#include <atomic>
#include <thread>
#include <iostream>

class Spinlock {
    std::atomic_flag flag = ATOMIC_FLAG_INIT;

public:
    void lock() {
        while (flag.test_and_set(std::memory_order_acquire));
    }

    void unlock() {
        flag.clear(std::memory_order_release);
    }
};

Spinlock spin;
void worker() {
    spin.lock();
    // კრიტიკული სექცია
    std::cout << "ნაკადი " << std::this_thread::get_id() << " შევიდა კრიტიკულ სექციაში\n";
    std::this_thread::sleep_for(std::chrono::milliseconds(10)); // იმიტაცია სამუშაოს
    std::cout << "ნაკადი " << std::this_thread::get_id() << " გამოდის კრიტიკულ სექციიდან\n";
    spin.unlock();
}
// int main() {
//     std::thread t1(worker);
//     std::thread t2(worker);
//     t1.join();
//     t2.join();
//     return 0;
// }

std::mutex გამოყენებით მიუტექსის მაგალითი:

#include <mutex>
#include <thread>
#include <iostream>

std::mutex mtx;
void worker_mutex() {
    mtx.lock();
    // კრიტიკული სექცია
    std::cout << "ნაკადი " << std::this_thread::get_id() << " შევიდა კრიტიკულ სექციაში\n";
    std::this_thread::sleep_for(std::chrono::milliseconds(10)); // იმიტაცია სამუშაოს
    std::cout << "ნაკადი " << std::this_thread::get_id() << " გამოდის კრიტიკულ სექციიდან\n";
    mtx.unlock();
}
// int main() {
//     std::thread t1(worker_mutex);
//     std::thread t2(worker_mutex);
//     t1.join();
//     t2.join();
//     return 0;
// }

შესაძლოა, ურთიერთდაბლოკვის მაგალითი:

#include <mutex>
#include <thread>
#include <iostream>

std::mutex mutex1;
std::mutex mutex2;

// ფუნქცია, რომელიც პირველ რიგში იღებს mutex1, შემდეგ mutex2
void thread1_func() {
    mutex1.lock();
    std::cout << "ნაკადი 1 აიღო mutex1\n";
    std::this_thread::sleep_for(std::chrono::milliseconds(100)); // იმიტაცია სამუშაოს
    mutex2.lock(); // ნაკადი 1 ელოდება mutex2-ს, რომელიც შეიძლება იყოს დაკავებული ნაკადი 2-ით
    std::cout << "ნაკადი 1 აიღო mutex2\n";
    // კრიტიკული სექცია ორივე მյუტექსით
    mutex2.unlock();
    mutex1.unlock();
}
// ფუნქცია, რომელიც პირველ რიგში იღებს mutex2, შემდეგ mutex1
void thread2_func() {
    mutex2.lock();
    std::cout << "ნაკადი 2 აიღო mutex2\n";
    std::this_thread::sleep_for(std::chrono::milliseconds(100)); // იმიტაცია სამუშაოს
    mutex1.lock(); // ნაკადი 2 ელოდება mutex1-ს, რომელიც შეიძლება იყოს დაკავებული ნაკადი 1-ით
    std::cout << "ნაკადი 2 აიღო mutex1\n";
    // კრიტიკული სექცია ორივე მյუტექსით
    mutex1.unlock();
    mutex2.unlock();
}
// int main() {
//     std::thread t1(thread1_func);
//     std::thread t2(thread2_func);
//     t1.join();
//     t2.join();
//     return 0;
// }

ამ მაგალითში, თუ ნაკადი 1 აიღებს mutex1 და შემდეგ გაათიშება, ხოლო ნაკადი 2 აიღებს mutex2 და შემდეგ გაათიშება, შემდეგ ნაკადი 1 ელოდება mutex2-ს (რომელიც დაკავებულია ნაკადი 2-ით), ხოლო ნაკადი 2 ელოდება mutex1-ს (რომელიც დაკავებულია ნაკადი 1-ით). ეს ქმნის ურთიერთდაბლოკვას.

ამ პრობლემის თავიდან ასაცილებლად, შეიძლება უბრალოდ შეცვალოთ mutex-ების დაჭერის წესრიგი ერთ-ერთ ფუნქციაში, რათა ორივე მათგანი პირველ რიგში აიღოს mutex1, შემდეგ mutex2.