Threads(Đa luồng) trong C++

Thread (Luồng): Là một đơn vị nhỏ nhất trong việc chia nhỏ công việc và thực hiện song song trên một bộ xử lý. Mỗi chương trình C++ thường bắt đầu bằng một luồng chính (main thread).

#include <iostream>
#include <thread>

// Hàm này sẽ được gọi khi các luồng bắt đầu thực thi
void threadFunction(int threadID) {
    std::cout << "Thread " << threadID << " is running." << std::endl;
}

int main() {
    // Luồng chính (main thread) bắt đầu thực thi khi chương trình chạy

    // Tạo một luồng mới và gọi hàm threadFunction với tham số là 1
    std::thread t1(threadFunction, 1);

    // Đoạn mã dưới đây vẫn được thực thi trên luồng chính (main thread)
    std::cout << "Main thread is running." << std::endl;

    // Gọi hàm join() để đợi luồng t1 hoàn thành trước khi tiếp tục
    t1.join();

    // Luồng t1 đã hoàn thành, chương trình tiếp tục thực thi trên luồng chính

    std::cout << "Main thread is done." << std::endl;

    return 0;
}

Trong ví dụ này:

– Chúng ta đã tạo một luồng mới t1 bằng cách sử dụng std::thread và truyền hàm threadFunction cùng với tham số là 1.

– Luồng chính (main thread) vẫn thực thi các dòng mã bình thường trong hàm main(), bao gồm việc in ra thông báo “Main thread is running.”

– Sau đó, chúng ta sử dụng t1.join() để đợi cho luồng t1 hoàn thành trước khi tiếp tục thực thi trong luồng chính. Điều này đảm bảo rằng các thông điệp được in ra từ luồng t1 hoàn thành trước khi thông điệp “Main thread is done.” được in ra.

– Vui lòng lưu ý rằng việc sử dụng join() để đợi cho luồng hoàn thành có thể dẫn đến chương trình bị chặn nếu luồng không hoàn thành. Cách khác là sử dụng detach() để giải phóng luồng và cho phép nó thực thi độc lập.

– Đồng Bộ Hóa (Synchronization): Điều chỉnh thứ tự thực thi của các luồng để tránh xung đột dữ liệu và tình trạng không đồng nhất. Mutex, semaphore và condition variables là các cơ chế thường được sử dụng để đảm bảo đồng bộ hóa.

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

std::mutex mtx;  // Khai báo một mutex để đồng bộ hóa

// Hàm thực hiện tăng giá trị biến count một cách an toàn
void incrementCounter(int &count) {
    for (int i = 0; i < 10000; ++i) {
        // Sử dụng lock_guard để lock mutex và tự động unlock khi hàm kết thúc
        std::lock_guard<std::mutex> lock(mtx);
        ++count;
    }
}

int main() {
    int counter = 0;

    // Tạo các luồng để tăng counter một cách song song
    std::thread t1(incrementCounter, std::ref(counter));
    std::thread t2(incrementCounter, std::ref(counter));

    // Đợi các luồng hoàn thành
    t1.join();
    t2.join();

    std::cout << "Final counter value: " << counter << std::endl;

    return 0;
}

Trong ví dụ này:

– Chúng ta sử dụng mutex mtx để đồng bộ hóa truy cập vào biến counter.

– Hai luồng t1t2 được tạo để gọi hàm incrementCounter cùng với tham chiếu đến biến counter.

– Trong hàm incrementCounter, chúng ta sử dụng std::lock_guard để lock mutex mtx. Khi luồng hoàn thành thực hiện, mutex tự động được unlock, đảm bảo rằng chỉ có một luồng có thể truy cập vào counter tại một thời điểm.

– Sau khi các luồng hoàn thành, chúng ta in ra giá trị cuối cùng của counter.

Mutex là một cơ chế quan trọng để đảm bảo đồng bộ hóa giữa các luồng và tránh tình trạng xung đột dữ liệu.

– Deadlock: Tình trạng mà hai hoặc nhiều luồng đang chờ đợi nhau mà không có luồng nào hoàn thành.

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

std::mutex mtx1;
std::mutex mtx2;

// Hàm thực hiện công việc 1
void task1() {
    mtx1.lock();
    std::cout << "Task 1 has acquired mutex 1." << std::endl;
    std::this_thread::sleep_for(std::chrono::milliseconds(100));

    mtx2.lock();
    std::cout << "Task 1 has acquired mutex 2." << std::endl;
    mtx2.unlock();
    mtx1.unlock();
}

// Hàm thực hiện công việc 2
void task2() {
    mtx2.lock();
    std::cout << "Task 2 has acquired mutex 2." << std::endl;
    std::this_thread::sleep_for(std::chrono::milliseconds(100));

    mtx1.lock();
    std::cout << "Task 2 has acquired mutex 1." << std::endl;
    mtx1.unlock();
    mtx2.unlock();
}

int main() {
    std::thread t1(task1);
    std::thread t2(task2);

    t1.join();
    t2.join();

    std::cout << "Main thread is done." << std::endl;

    return 0;
}

Trong ví dụ này:

– Hai luồng t1t2 đang cố gắng khóa hai mutex mtx1mtx2 theo một thứ tự khác nhau.

– Luồng t1 khóa mtx1 và đang cố gắng khóa mtx2, trong khi luồng t2 khóa mtx2 và đang cố gắng khóa mtx1.

– Khi cả hai luồng đạt đến bước cố gắng khóa mutex thứ hai, chúng sẽ bị chặn vì mutex đang được giữ bởi luồng kia.

– Điều này dẫn đến tình trạng deadlock, vì cả hai luồng đang chờ đợi lẫn nhau mà không có luồng nào hoàn thành.

Để tránh deadlock, cần thiết lập quy tắc thứ tự cho việc khóa mutex và sử dụng các cơ chế như timeout, hoặc đảm bảo rằng chỉ có một luồng duy nhất có thể khóa nhiều mutex cùng một lúc.

– Joinable Threads: Các luồng mà bạn có thể sử dụng hàm join() để đợi chúng hoàn thành trước khi tiếp tục chạy luồng chính.

#include <iostream>
#include <thread>
#include <vector>

// Hàm thực hiện công việc trên mỗi luồng
void workerThread(int threadID) {
    std::cout << "Thread " << threadID << " is working." << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(2));
    std::cout << "Thread " << threadID << " has finished its work." << std::endl;
}

int main() {
    std::vector<std::thread> threads;

    // Tạo và bắt đầu các luồng
    for (int i = 1; i <= 3; ++i) {
        threads.emplace_back(workerThread, i);
    }

    std::cout << "Main thread is waiting for worker threads to finish." << std::endl;

    // Đợi cho tất cả các luồng hoàn thành
    for (auto &thread : threads) {
        thread.join();
    }

    std::cout << "All worker threads have finished. Main thread is done." << std::endl;

    return 0;
}

Trong ví dụ này:

– Chúng ta tạo một vector threads để lưu các luồng.

– Trong vòng lặp, chúng ta tạo và bắt đầu các luồng bằng cách sử dụng hàm emplace_back() để thêm luồng vào vector và truyền cho mỗi luồng một threadID riêng biệt.

– Luồng chính in ra thông điệp và sau đó sử dụng vòng lặp để gọi hàm join() cho mỗi luồng trong vector. Điều này đảm bảo rằng luồng chính sẽ chờ cho tất cả các luồng hoàn thành trước khi tiếp tục thực thi.

– Sau khi tất cả các luồng đã hoàn thành, luồng chính tiếp tục thực thi và in ra thông điệp.

Việc sử dụng join() giúp đảm bảo rằng các luồng con đã hoàn thành công việc của chúng trước khi chương trình chính kết thúc.