C ++ 0x에 세마포어가 없습니까? 스레드를 동기화하는 방법?


135

C ++ 0x에 세마포어가없는 것이 사실입니까? 스택 오버플로에는 세마포어 사용과 관련하여 이미 몇 가지 질문이 있습니다. 스레드가 다른 스레드에서 일부 이벤트를 기다릴 수 있도록 항상 (posix 세마포어)를 사용합니다.

void thread0(...)
{
  doSomething0();

  event1.wait();

  ...
}

void thread1(...)
{
  doSomething1();

  event1.post();

  ...
}

내가 뮤텍스로 그렇게한다면 :

void thread0(...)
{
  doSomething0();

  event1.lock(); event1.unlock();

  ...
}

void thread1(...)
{
  event1.lock();

  doSomethingth1();

  event1.unlock();

  ...
}

문제 : 추악하고 thread1이 뮤텍스를 먼저 잠근다는 보장이 없습니다 (같은 스레드가 뮤텍스를 잠그고 잠금 해제해야한다면 thread0과 thread1이 시작되기 전에 event1을 잠글 수도 없습니다).

따라서 boost에는 세마포어가 없으므로 위의 가장 간단한 방법은 무엇입니까?


아마도 mutex와 std :: promise와 std :: future 조건을 사용할까요?
이브

답변:


180

뮤텍스와 조건 변수에서 하나를 쉽게 만들 수 있습니다.

#include <mutex>
#include <condition_variable>

class semaphore
{
private:
    std::mutex mutex_;
    std::condition_variable condition_;
    unsigned long count_ = 0; // Initialized as locked.

public:
    void notify() {
        std::lock_guard<decltype(mutex_)> lock(mutex_);
        ++count_;
        condition_.notify_one();
    }

    void wait() {
        std::unique_lock<decltype(mutex_)> lock(mutex_);
        while(!count_) // Handle spurious wake-ups.
            condition_.wait(lock);
        --count_;
    }

    bool try_wait() {
        std::lock_guard<decltype(mutex_)> lock(mutex_);
        if(count_) {
            --count_;
            return true;
        }
        return false;
    }
};

96
누군가 표준 제안자에게 제안서를 제출해야합니다

7
처음에 나를 당황스럽게 한 의견은 잠금 대기 중입니다. 잠금이 대기 상태에 있으면 스레드가 어떻게 알림을받을 수 있는지 묻습니다. 다소 저조한 모호 문서화 된 대답은 그 condition_variable.wait이 적어도 그게 내가 그것을 이해하는 방법은, 다른 스레드가 원자 방식으로 과거 얻을 통지 할 수 있도록 잠금 펄스입니다

31
그것은 한 의도적으로 세마포어가 함께 스스로를 거는 프로그래머를위한 너무 많은 로프가 있다는 근거 부스트에서 제외. 조건 변수가 더 관리하기 쉬운 것으로 추정됩니다. 나는 그들의 요점을 보지만 약간의 후원을 느낍니다. C ++ 11에도 동일한 논리가 적용된다고 가정합니다. 프로그래머는 "자연스럽게"condvar 또는 기타 승인 된 동기화 기술을 사용하는 방식으로 프로그램을 작성해야합니다. 세마포어를 제공하는 것은 condvar 위에 구현되었는지 또는 기본적으로 구현되는지에 관계없이 이에 대해 실행됩니다.
Steve Jessop

5
참고- 루프 의 이론적 근거는 en.wikipedia.org/wiki/Spurious_wakeup 을 참조하십시오 while(!count_).
Dan Nissenbaum

3
@Maxim 죄송합니다, 당신이 옳다고 생각하지 않습니다. sem_wait 및 sem_post 만 경합에 대한 syscall 만 ( sourceware.org/git/?p=glibc.git;a=blob;f=nptl/sem_wait.c 확인 ) 여기서 코드는 잠재적으로 버그가있는 libc 구현을 복제합니다. 시스템에서 이식성을 원하는 경우 솔루션 일 수 있지만 Posix 호환성 만 필요한 경우 Posix 세마포어를 사용하십시오.
xryl669

107

Maxim Yegorushkin의 답변을 기반으로 C ++ 11 스타일로 예제를 만들려고했습니다.

#include <mutex>
#include <condition_variable>

class Semaphore {
public:
    Semaphore (int count_ = 0)
        : count(count_) {}

    inline void notify()
    {
        std::unique_lock<std::mutex> lock(mtx);
        count++;
        cv.notify_one();
    }

    inline void wait()
    {
        std::unique_lock<std::mutex> lock(mtx);

        while(count == 0){
            cv.wait(lock);
        }
        count--;
    }

private:
    std::mutex mtx;
    std::condition_variable cv;
    int count;
};

34
wait ()도 3 라이너로 만들 수 있습니다.cv.wait(lck, [this]() { return count > 0; });
Domi

2
lock_guard의 정신으로 다른 클래스를 추가하는 것도 도움이됩니다. RAII 방식으로 세마포어를 참조로 사용하는 생성자는 세마포어의 wait () 호출을 호출하고 소멸자는 notify () 호출을 호출합니다. 이렇게하면 예외가 세마포어를 해제하지 못하게됩니다.
Jim Hunziker

N 스레드가 wait () 및 count == 0이라고한다면 교착 상태가없는 것입니다. cv.notify_one (); mtx가 출시되지 않았기 때문에 호출되지 않습니다?
Marcello

1
@Marcello 대기중인 스레드는 잠금을 유지하지 않습니다. 전체 조건 변수는 원자 적 "잠금 해제 및 대기"조작을 제공하는 것입니다.
David Schwartz

3
당신은 웨이크 업을 차단 즉시 피하기 위해 notify_one ()를 호출하기 전에 잠금을 해제해야합니다 ... 여기를 참조하십시오 en.cppreference.com/w/cpp/thread/condition_variable/notify_all
kylefinn

38

나는 가능한 한 표준 스타일로 할 수있는 가장 강력하고 일반적인 C ++ 11 세마포어를 작성하기로 결정했습니다 ( 주로 보통 not using semaphore = ...사용하는 semaphore것과 비슷한 이름을 사용합니다 )stringbasic_string

template <typename Mutex, typename CondVar>
class basic_semaphore {
public:
    using native_handle_type = typename CondVar::native_handle_type;

    explicit basic_semaphore(size_t count = 0);
    basic_semaphore(const basic_semaphore&) = delete;
    basic_semaphore(basic_semaphore&&) = delete;
    basic_semaphore& operator=(const basic_semaphore&) = delete;
    basic_semaphore& operator=(basic_semaphore&&) = delete;

    void notify();
    void wait();
    bool try_wait();
    template<class Rep, class Period>
    bool wait_for(const std::chrono::duration<Rep, Period>& d);
    template<class Clock, class Duration>
    bool wait_until(const std::chrono::time_point<Clock, Duration>& t);

    native_handle_type native_handle();

private:
    Mutex   mMutex;
    CondVar mCv;
    size_t  mCount;
};

using semaphore = basic_semaphore<std::mutex, std::condition_variable>;

template <typename Mutex, typename CondVar>
basic_semaphore<Mutex, CondVar>::basic_semaphore(size_t count)
    : mCount{count}
{}

template <typename Mutex, typename CondVar>
void basic_semaphore<Mutex, CondVar>::notify() {
    std::lock_guard<Mutex> lock{mMutex};
    ++mCount;
    mCv.notify_one();
}

template <typename Mutex, typename CondVar>
void basic_semaphore<Mutex, CondVar>::wait() {
    std::unique_lock<Mutex> lock{mMutex};
    mCv.wait(lock, [&]{ return mCount > 0; });
    --mCount;
}

template <typename Mutex, typename CondVar>
bool basic_semaphore<Mutex, CondVar>::try_wait() {
    std::lock_guard<Mutex> lock{mMutex};

    if (mCount > 0) {
        --mCount;
        return true;
    }

    return false;
}

template <typename Mutex, typename CondVar>
template<class Rep, class Period>
bool basic_semaphore<Mutex, CondVar>::wait_for(const std::chrono::duration<Rep, Period>& d) {
    std::unique_lock<Mutex> lock{mMutex};
    auto finished = mCv.wait_for(lock, d, [&]{ return mCount > 0; });

    if (finished)
        --mCount;

    return finished;
}

template <typename Mutex, typename CondVar>
template<class Clock, class Duration>
bool basic_semaphore<Mutex, CondVar>::wait_until(const std::chrono::time_point<Clock, Duration>& t) {
    std::unique_lock<Mutex> lock{mMutex};
    auto finished = mCv.wait_until(lock, t, [&]{ return mCount > 0; });

    if (finished)
        --mCount;

    return finished;
}

template <typename Mutex, typename CondVar>
typename basic_semaphore<Mutex, CondVar>::native_handle_type basic_semaphore<Mutex, CondVar>::native_handle() {
    return mCv.native_handle();
}

이것은 약간의 편집으로 작동합니다. wait_forwait_until술어와 메서드 호출은 부울 값 (안`표준 : cv_status)을 반환합니다.
jdknight

늦게 게임에서 늦게 뽑아서 죄송합니다. std::size_t부호가 없으므로 0 미만으로 줄이면 UB이며 항상입니다 >= 0. IMHO count는이어야합니다 int.
Richard Hodges

3
@RichardHodges 0 미만으로 감소하는 방법이 없으므로 문제가 없으며 세마포어의 음수가 의미하는 것은 무엇입니까? IMO조차도 말이되지 않습니다.
David

1
@David 스레드가 다른 사람들이 일을 초기화하기를 기다려야한다면 어떻게해야합니까? 예를 들어, 1 개의 스레드가 4 개의 스레드를 기다리려면 -3으로 세마포어 생성자를 호출하여 다른 모든 스레드가 게시물을 만들 때까지 리더 스레드를 기다리게하십시오. 다른 방법이 있다고 생각하지만 합리적이지 않습니까? 실제로 OP가 묻는 질문이지만 더 많은 "thread1"이 있다고 생각합니다.
jmmut

2
@RichardHodges는 매우 pedantic 한 것으로 0 미만의 부호없는 정수 유형을 줄이는 것은 UB가 아닙니다.
jcai

15

posix 세마포어에 따라

class semaphore
{
    ...
    bool trywait()
    {
        boost::mutex::scoped_lock lock(mutex_);
        if(count_)
        {
            --count_;
            return true;
        }
        else
        {
            return false;
        }
    }
};

그리고 더 기본적인 연산자를 사용하여 스티칭 된 버전을 붙여 넣는 것보다 항상 편리한 추상화 수준에서 동기화 메커니즘을 사용하는 것이 좋습니다.


9

cpp11-on-multicore를 확인할 수도 있습니다. 이식 가능하고 최적의 세마포어 구현이 있습니다.

이 저장소에는 c ++ 11 스레딩을 보완하는 다른 스레딩 기능도 포함되어 있습니다.


8

뮤텍스 및 조건 변수로 작업 할 수 있습니다. 뮤텍스에 독점적으로 액세스하고, 계속 진행할 것인지 또는 다른 쪽 끝을 기다려야하는지 확인하십시오. 기다릴 필요가 있으면 조건을 기다립니다. 다른 스레드가 계속할 수 있다고 판단하면 조건을 알립니다.

boost :: thread 라이브러리에는 복사 할 수 있는 짧은 예제 가 있습니다 (C ++ 0x 및 boost thread 라이브러리는 매우 유사 함).


상태 신호는 대기중인 스레드에만 표시됩니까? 따라서 thread0이 thread1 신호를 기다릴 때 대기하지 않으면 나중에 차단됩니까? 플러스 : 조건과 함께 제공되는 추가 잠금 장치가 필요하지 않습니다. 오버 헤드입니다.
tauran

예. 조건은 대기 스레드 만 신호합니다. 일반적인 패턴은 대기해야 할 경우를 대비하여 상태 및 조건에 따라 변수를 갖는 것입니다. 생산자 / 소비자에 대해 생각하십시오. 버퍼에있는 항목에 카운트가 있고, 생산자가 잠기고, 요소를 추가하고, 카운트와 신호를 증가시킵니다. 컨슈머는 카운터를 잠그고 카운터를 확인하며 0이 아닌 경우 소비하며 상태가 0이면 대기합니다.
David Rodríguez-dribeas

2
세마포어를 다음과 같이 시뮬레이션 할 수 있습니다. 세마포어에 제공 할 값으로 변수를 초기화 한 다음 wait()"잠금, 0이 아닌 경우 카운트를 확인하고 계속하십시오. 조건이 0 인 경우 post"잠금, 증가 카운터, 신호는 0 "인 경우
dribeas - 데이비드 로드리게스

예, 잘 들립니다. posix 세마포어가 동일한 방식으로 구현되는지 궁금합니다.
tauran

@ 타우 란 : 확실하지 않으며 (Posix OS에 따라 다를 수 있음), 가능성은 낮습니다. 세마포어는 전통적으로 뮤텍스 및 조건 변수보다 "낮은 레벨"동기화 프리미티브이며, 원칙적으로 condvar 위에 구현할 때보 다 더 효율적으로 만들 수 있습니다. 따라서 주어진 OS에서 모든 사용자 레벨 동기화 프리미티브는 스케줄러와 상호 작용하는 공통 도구 위에 빌드 될 가능성이 높습니다.
Steve Jessop

3

스레드에서 유용한 RAII 세마포 랩퍼도 사용할 수 있습니다.

class ScopedSemaphore
{
public:
    explicit ScopedSemaphore(Semaphore& sem) : m_Semaphore(sem) { m_Semaphore.Wait(); }
    ScopedSemaphore(const ScopedSemaphore&) = delete;
    ~ScopedSemaphore() { m_Semaphore.Notify(); }

   ScopedSemaphore& operator=(const ScopedSemaphore&) = delete;

private:
    Semaphore& m_Semaphore;
};

멀티 스레드 앱의 사용 예 :

boost::ptr_vector<std::thread> threads;
Semaphore semaphore;

for (...)
{
    ...
    auto t = new std::thread([..., &semaphore]
    {
        ScopedSemaphore scopedSemaphore(semaphore);
        ...
    }
    );
    threads.push_back(t);
}

for (auto& t : threads)
    t.join();

3

C ++ 20에는 마침내 세마포어가 있습니다- std::counting_semaphore<max_count> 있습니다.

이것들은 (적어도) 다음과 같은 방법을 갖습니다.

  • acquire() (블로킹)
  • try_acquire() (비 차단, 즉시 반환)
  • try_acquire_for() (비 차단, 시간이 걸립니다)
  • try_acquire_until() (비 차단, 시도를 중지하는 데 시간이 걸립니다)
  • release()

이것은 cppreference에 아직 나와 있지 않지만 CppCon 2019 프레젠테이션 슬라이드를 읽 거나 비디오를 볼 수 있습니다. 공식 제안 P0514R4 도 있지만 최신 버전인지 확실하지 않습니다.


2

목록이 긴 shared_ptr과 weak_ptr이 필요한 작업을 수행했습니다. 내 문제는 호스트의 내부 데이터와 상호 작용하려는 여러 클라이언트가 있다는 것입니다. 일반적으로 호스트는 자체적으로 데이터를 업데이트하지만 클라이언트가 요청하면 클라이언트가 호스트 데이터에 액세스하지 않을 때까지 호스트 업데이트를 중지해야합니다. 동시에 클라이언트는 단독 액세스를 요청할 수 있으므로 다른 클라이언트 나 호스트가 해당 호스트 데이터를 수정할 수 없습니다.

내가 이것을 한 방법은 구조체를 만들었습니다.

struct UpdateLock
{
    typedef std::shared_ptr< UpdateLock > ptr;
};

각 고객에게는 다음과 같은 구성원이 있습니다.

UpdateLock::ptr m_myLock;

그러면 호스트는 독점 성을위한 weak_ptr 멤버와 비 배타적 잠금을위한 weak_ptr 목록을 갖게됩니다.

std::weak_ptr< UpdateLock > m_exclusiveLock;
std::list< std::weak_ptr< UpdateLock > > m_locks;

잠금을 활성화하는 기능과 호스트가 잠겨 있는지 확인하는 다른 기능이 있습니다.

UpdateLock::ptr LockUpdate( bool exclusive );       
bool IsUpdateLocked( bool exclusive ) const;

LockUpdate, IsUpdateLocked 및 주기적으로 호스트의 업데이트 루틴에서 잠금을 테스트합니다. 잠금 테스트는 weak_ptr이 만료되었는지 확인하고 m_locks 목록에서 만료 된 항목을 제거하는 것 (호스트 업데이트 중에 만 수행)만큼 간단합니다. 목록이 비어 있는지 확인할 수 있습니다. 동시에 클라이언트가 걸려있는 shared_ptr을 재설정하면 자동 잠금 해제가 발생하며 클라이언트가 자동으로 파괴 될 때도 발생합니다.

모든 효과는 클라이언트가 독점 성을 거의 필요로하지 않기 때문에 (일반적으로 추가 및 삭제 전용으로 예약 됨) 대부분의 경우 LockUpdate (false)에 대한 요청, 즉 비 독점은 (! m_exclusiveLock)만큼 성공합니다. 독점 성 요청 인 LockUpdate (true)는 (! m_exclusiveLock) 및 (m_locks.empty ()) 모두에 대해서만 성공합니다.

배타적 잠금과 배타적 잠금을 완화하기 위해 대기열을 추가 할 수는 있지만 지금까지 충돌은 없었으므로 솔루션을 추가 할 때까지 기다릴 것입니다 (대부분 실제 테스트 조건이 있습니다).

지금까지 이것은 내 요구에 잘 작동합니다. 나는 이것을 확장해야 할 필요성과 확장 된 사용으로 인해 발생할 수있는 몇 가지 문제를 상상할 수 있지만, 구현이 빠르고 사용자 정의 코드가 거의 필요하지 않았습니다.


-4

누군가 원자 버전에 관심이있는 경우 구현은 다음과 같습니다. mutex & condition 변수 버전보다 성능이 더 좋습니다.

class semaphore_atomic
{
public:
    void notify() {
        count_.fetch_add(1, std::memory_order_release);
    }

    void wait() {
        while (true) {
            int count = count_.load(std::memory_order_relaxed);
            if (count > 0) {
                if (count_.compare_exchange_weak(count, count-1, std::memory_order_acq_rel, std::memory_order_relaxed)) {
                    break;
                }
            }
        }
    }

    bool try_wait() {
        int count = count_.load(std::memory_order_relaxed);
        if (count > 0) {
            if (count_.compare_exchange_strong(count, count-1, std::memory_order_acq_rel, std::memory_order_relaxed)) {
                return true;
            }
        }
        return false;
    }
private:
    std::atomic_int count_{0};
};

4
성능이 훨씬 나빠질 것으로 기대합니다 . 이 코드는 거의 모든 가능한 실수를합니다. 가장 명백한 예와 같이 wait코드가 여러 번 반복되어야 한다고 가정하십시오 . 마지막으로 차단을 해제하면 CPU의 루프 예측이 확실히 다시 반복 될 것으로 예측하므로 잘못 예측 된 모든 브랜치의 모체가 필요합니다. 이 코드와 관련된 더 많은 문제를 나열 할 수 있습니다.
David Schwartz

1
또 다른 명백한 성능 저하 wait요인은 다음과 같습니다. 루프는 CPU 미세 실행 리소스를 사용하면서 소비합니다. 그것이 예상되는 스레드와 동일한 물리적 코어에 있다고 가정하면 notify스레드가 크게 느려집니다.
David Schwartz

1
x86 CPU (오늘날 가장 인기있는 CPU)에서 compare_exchange_weak 작업은 실패하더라도 항상 쓰기 작업입니다 (비교에 실패하면 읽은 것과 동일한 값을 다시 씁니다). 따라서 두 개의 코어가 모두 wait동일한 세마포어 에 대한 루프에 있다고 가정하십시오 . 둘 다 동일한 캐시 라인 에 최고 속도로 쓰기 때문에 코어 간 버스를 포화시켜 다른 코어를 크롤링하는 속도를 늦출 수 있습니다.
David Schwartz

@DavidSchwartz 다행이 귀하의 의견을 참조하십시오. '... CPU의 루프 예측 ...'부분을 이해하지 못했습니다. 두 번째에 동의했습니다. 분명히 세 번째 경우가 발생할 수 있지만 뮤텍스와 비교하여 사용자 모드에서 커널 모드로 전환하고 시스템 호출을 수행하면 코어 간 동기화가 나 빠지지 않습니다.
Jeffery

1
잠금없는 세마포어는 없습니다. 잠금을 해제한다는 아이디어는 뮤텍스를 사용하지 않고 코드를 작성하는 것이 아니라 스레드가 전혀 차단하지 않는 코드를 작성하는 것입니다. 이 경우 세마포어의 본질은 wait () 함수를 호출하는 스레드를 차단하는 것입니다!
카를로 우드
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.