(C++ : Concurrency) 9. semaphore

Posted by : at

Category : Cpp


semaphore기초

#include <iostream>
#include <mutex>
#include <thread>
#include <string>
#include <chrono>
using namespace std::literals;

std::mutex m;

void Download(std::string name)
{
    m.lock();
    for(int i = 0; i < 100; i++)
    {
        std::cout << name;
        std::this_thread::sleep_for(30ms);
    }
    m.unlock();
}

int main()
{
    std::thread t1(Download, "1");
    std::thread t2(Download, "2");
    std::thread t3(Download, "3");
    std::thread t4(Download, "4");
    std::thread t5(Download, "5");
    t1.join();
    t2.join();
    t3.join();
    t4.join();
    t5.join();
}

결과를 보면 1이 모두 끝나고 2가 시작 2가 모두 끝나고 3이 시작 … 이런식으로 자원의 독접이 일어난다.
자원을 공유하는 방안은 없을까?

#include <iostream>
#include <thread>
#include <string>
#include <chrono>
#include <semaphore>
using namespace std::literals;

std::counting_semaphore<3> sem(3);  
// std::counting_semaphore<MAX_COUNT> sem(초기값);

void Download(std::string name)
{
    sem.acquire();
    for(int i = 0; i < 100; i++)
    {
        std::cout << name;
        std::this_thread::sleep_for(30ms);
    }
    sem.release();
}

int main()
{
    std::thread t1(Download, "1");
    std::thread t2(Download, "2");
    std::thread t3(Download, "3");
    std::thread t4(Download, "4");
    std::thread t5(Download, "5");
    t1.join();
    t2.join();
    t3.join();
    t4.join();
    t5.join();
}

3개까지는 들어올 수 있다

#include <iostream>
#include <thread>
#include <string>
#include <chrono>
#include <semaphore>
using namespace std::literals;

std::counting_semaphore<3> sem(0);  // 초기값이 0이라면?
// 어디선가 release를 해줘야 들어갈 수 있음

std::counting_semaphore<1> sem(1);
// std::binrary_semaphore sem(1); // 위 표현과 동일
// counting_semaphore<1>은 mutex와 동일한거 아닌가 왜만들었지?
// mutex는 lock을 잡은 해당 thread만 unlock이 가능하지만
// counting_semaphore<1>는 어디서든 release가 가능함

thread_local

#include <iostream>
#include <thread>
#include <string_view>

int next3times()
{
    // 싱글스레드일때는 정상적으로 동작
    static int n = 0;
    n = n + 3;
    return n;
}

void foo(std::string_view name)
{
    std::cout << name << " : " << next3times() << std::endl;
    std::cout << name << " : " << next3times() << std::endl;
    std::cout << name << " : " << next3times() << std::endl;
}

int main()
{
    foo("A");

    // 만약 멀티스레드환경이라면?
    std::thread t1(foo, "A");
    std::thread t2(foo, "\tB");
    t1.join();
    t2.join();
    // 결과부터 말하면 3 6 9 12 15 18 이나오게된다.
    // 만약 스레드별로 3 6 9 를 출력하고 싶다면?
    // 각각 스레드별로 변수를 갖고싶다(TLS : Thread Local Storage) : 스레드별로 스택을 만들어줘
}
  • 예전에는 이렇게 TLS를 사용했다.
    • linux(gcc) : _thread static int x
    • windows(cl) : __declspec(thread)
    • C++표준이 나오고 부터 : thread_local
int next3times()
{
    thread_local int n = 0;
    // thread_local static int n = 0;
    // static이 암시적으로 선언이 되기에 생략해도 된다.
    n = n + 3;
    return n;
}

call_once

동일한 함수를 여러 개의 스레드를 수행하지만, 초기화 작업은 한 번만 하고 싶다면?

#include <iostream>
#include <thread>
#include <chrono>
#include <mutex>
using namespace std::literals;

std::once_flag init_flag;

void init(int a, double d)
{
    std::cout << "init" << std::endl;
    std::this_thread::sleep_for(2s);
    // 만약 여기서 대기를 해야한다면
    // 이 이후에 호출된 스레드들은 모두 std::call_once에서 init이 종료되기를 대기하고 있는다
}

void foo()
{
    std::cout << "start foo" << std::endl;
    //init(10, 3.4);
    std::call_once(init_flag, init, 10, 3.4);   // 처음 불린이후에는 호출되지 않음.
    std::cout << "finish foo" << std::endl;
}

int main()
{
    std::thread t1(foo);
    std::thread t2(foo);
    std::thread t3(foo);
    t1.join();
    t2.join();
    t3.join();
}

Example

class Singleton
{
private:
    Singleton() = default;
    static Singleton* sinstance;
public:
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

    static Singleton* getInstance()
    {
        // 여기가 멀티스레드 환경에서 불안정하다
        if(sinstance == nullptr)
            // new를 하는 중간에 다른 thread에서 new를 해버리면??
            sinstance = new Singleton;
        return sinstance;
    }
};
Singleton* Singleton::sinstance == nullptr;
class Singleton
{
private:
    Singleton() = default;
    static Singleton* sinstance;
    static std::once_flag create_flag;
public:
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

    static Singleton* getInstance()
    {
        std::call_once(create_flag, initSingleton);
        return sinstance;
    }
    static void initSingleton()
    {
        sinstance = new Singleton;
    }
};
Singleton* Singleton::sinstance == nullptr;
std::once_flag Singleton::create_flag;

Example2) Mayer’s Singleton

class Singleton
{
private:
    Singleton()
    {
        std::cout << "start ctor" << std::endl;
        std::this_thread::sleep_for(3s);
        std::cout << "finish ctor" << std::endl;
    }
public:
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

    static Singleton& getInstance()
    {
        std::cout << "start getInstance" << std::endl;
        static Singleton instance;
        std::cout << "finish getInstance" << std::endl;
        return instance;
        // static으로 선언되어있으니 안전할까?
        // Yes! C++11부터는 static 지역변수는 스레드에 안전하다
    }
};

About Taehyung Kim

안녕하세요? 8년차 현업 C++ 개발자 김태형이라고 합니다. 😁 C/C++을 사랑하며 다양한 사람과의 협업을 즐깁니다. ☕ 꾸준한 자기개발을 미덕이라 생각하며 노력중이며, 제가 얻은 지식을 홈페이지에 정리 중입니다. 좀 더 상세한 제 이력서 혹은 Private 프로젝트 접근 권한을 원하신다면 메일주세요. 😎

Star
Useful Links