ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • C++ std::thread와 std::mutex를 사용한 멀티스레딩 예제
    프로그래밍/C/C++ 2023. 2. 22. 11:23

    std::thread는 C++11부터 추가된 표준 라이브러리 클래스로, 멀티스레드 프로그래밍을 쉽게 구현할 수 있도록 도와줍니다.

    std::thread 클래스는 스레드를 생성하고, 실행시키며, 종료시키는 등의 다양한 기능을 제공합니다.

     

    std::thread를 사용하면, 별도의 스레드를 생성하여 해당 스레드에서 작업을 수행할 수 있습니다. 예를 들어, 주 스레드에서는 사용자 인터페이스를 처리하고, 별도의 스레드에서는 파일 입출력 또는 계산 등의 작업을 수행하는 것이 가능합니다.

     

    std::thread는 스레드 생성 시 실행될 함수(또는 람다 표현식)와 인수를 전달하여 스레드를 생성할 수 있습니다. 생성된 스레드는 join 함수를 사용하여 종료될 때까지 대기하거나, detach 함수를 사용하여 분리(detached)할 수 있습니다. 분리된 스레드는 실행을 계속하며, 메인 스레드에서는 더 이상 해당 스레드를 제어할 수 없습니다.

     

    std::thread를 사용하면 멀티스레드 프로그래밍을 쉽게 구현할 수 있지만, 스레드 간의 동기화나 뮤텍스 등의 관리를 위해서는 추가적인 작업이 필요합니다. 따라서, std::thread를 사용할 때는 스레드 간의 동기화와 관련된 문제를 고려하여 안정적인 코드를 작성해야 합니다.

    #include <iostream>
    #include <thread>
    #include <mutex>
    #include <vector>
    
    std::vector<int> v;
    std::mutex mutex;
    
    void add_to_vector(int id) {
        // 벡터에 원소 추가하기 전에 뮤텍스 락을 건다.
        std::lock_guard<std::mutex> lock(mutex);
        
        for (int i = 0; i < 5; i++) {
            v.push_back(id * 10 + i);
        }
    }
    
    int main() {
        std::vector<std::thread> threads;
        for (int i = 0; i < 10; i++) {
            threads.emplace_back(add_to_vector, i);
        }
        for (auto& t : threads) {
            t.join();
        }
        
        // 모든 스레드가 실행된 후 벡터의 내용을 출력한다.
        for (int i : v) {
            std::cout << i << " ";
        }
        std::cout << std::endl;
        return 0;
    }

    이 코드는 다음과 같은 일을 합니다.

    1. v라는 빈 벡터와 mutex라는 뮤텍스를 만듭니다.
    2. add_to_vector라는 함수를 정의합니다. 이 함수는 id라는 인수를 받아서 v에 id와 그에 따른 5개의 정수를 추가합니다. 함수 내에서는 lock_guard를 이용하여 뮤텍스를 락하고, 벡터에 원소를 추가한 뒤 자동으로 뮤텍스를 언락합니다.
    3. main 함수에서는 10개의 스레드를 생성합니다. 각각의 스레드는 add_to_vector 함수를 호출하고, 그 인수로는 스레드의 인덱스 값을 전달합니다.
    4. 10개의 스레드가 모두 종료될 때까지 대기합니다. 이를 위해서는 join 함수를 사용합니다.
    5. 모든 스레드가 종료된 후, v 벡터의 내용을 출력합니다.

    이 코드에서 mutex를 사용하는 이유는, 여러 스레드가 동시에 v 벡터에 원소를 추가하려고 할 때 생기는 문제를 방지하기 위해서입니다. lock_guard를 사용하여 뮤텍스를 락하면, 락을 획득한 스레드만이 벡터에 원소를 추가할 수 있고, 락을 획득하지 못한 스레드는 대기하게 됩니다. 이를 통해 벡터에 동시에 접근하는 것을 방지하여 문제를 해결할 수 있습니다.

     

     

    클래스 형태에서 사용하는 방법

    클래스 멤버 함수를 멀티스레드 환경에서 안전하게 사용하려면, 멤버 함수가 호출되는 동안 다른 스레드에서 동시에 해당 멤버 함수를 호출하지 못하도록 보호해야 합니다. 이를 위해서는 다음과 같은 방법들이 있습니다.

    1. std::mutex를 사용하여 뮤텍스를 생성하고, 멤버 함수 내에서 뮤텍스를 잠금(또는 잠금 해제)합니다. 이를 통해 다른 스레드가 해당 함수를 호출하지 못하도록 보호할 수 있습니다.

    class Example {
    public:
        void foo() {
            std::lock_guard<std::mutex> lock(mutex_);
            // 멤버 함수 내에서 실행될 코드
        }
    private:
        std::mutex mutex_;
    };

    2. std::atomic을 사용하여 원자적 연산을 수행하도록 멤버 변수를 지정할 수 있습니다. 이를 통해 멤버 변수의 값을 여러 스레드가 동시에 접근해도 안전하게 사용할 수 있습니다.

    class Example {
    public:
        void set_value(int value) {
            value_ = value;
        }
        int get_value() const {
            return value_;
        }
    private:
        std::atomic<int> value_;
    };

    3. 락프리(lock-free) 알고리즘을 사용하여 멤버 함수를 구현할 수 있습니다. 이를 통해 뮤텍스 등의 동기화 방법을 사용하지 않고도 멤버 변수를 안전하게 사용할 수 있습니다. 락프리 알고리즘은 성능상의 이점이 있지만, 구현이 복잡하고 버그 발생 가능성이 높기 때문에 주의해서 사용해야 합니다.

    class Example {
    public:
        void set_value(int value) {
            int expected = value_;
            while (!value_.compare_exchange_weak(expected, value)) {
                // 값을 업데이트할 수 없는 경우, 다시 시도
            }
        }
        int get_value() const {
            return value_;
        }
    private:
        std::atomic<int> value_;
    };

    위와 같이 뮤텍스, 원자 변수, 락프리 알고리즘 등의 방법을 사용하여 클래스 멤버 함수를 멀티스레드 환경에서 안전하게 사용할 수 있습니다. 하지만, 멀티스레드 환경에서 코드를 작성할 때는 스레드 간의 경쟁 상황(race condition)을 고려하여 적절한 동기화 방법을 선택하고, 안전한 코드를 작성해야 합니다.

     

    클래스 멤버함수를 인자로 줄 수 있는 방법도 있을까?

    std::thread를 사용하여 클래스의 멤버 함수를 실행하려면, 멤버 함수 포인터와 함께 해당 클래스 객체의 주소를 std::thread 생성자에 전달해야 합니다. 이를 위해서는 다음과 같은 방법을 사용할 수 있습니다.

     

    1. 람다 함수를 사용하여 해당 클래스 객체와 멤버 함수 포인터를 전달합니다.

    class Example {
    public:
        void foo() {
            // 멤버 함수 내에서 실행될 코드
        }
    };
    
    int main() {
        Example obj;
        std::thread t([&obj]() {
            obj.foo();
        });
        t.join();
        return 0;
    }

    2. std::bind를 사용하여 해당 클래스 객체와 멤버 함수 포인터를 전달합니다.

    class Example {
    public:
        void foo() {
            // 멤버 함수 내에서 실행될 코드
        }
    };
    
    int main() {
        Example obj;
        std::thread t(std::bind(&Example::foo, &obj));
        t.join();
        return 0;
    }

    위와 같이 람다 함수나 std::bind를 사용하여 해당 클래스 객체와 멤버 함수 포인터를 전달하면, std::thread 생성자가 이를 받아서 새로운 스레드에서 해당 멤버 함수를 실행합니다. 이 때, 해당 클래스 객체의 주소(&obj와 같은 형태)를 전달해야 해당 객체의 멤버 변수나 멤버 함수를 스레드에서 접근할 수 있습니다.

    댓글

Designed by Tistory.