C ++ 0x không có semaphores? Làm thế nào để đồng bộ hóa chủ đề?


135

Có đúng là C ++ 0x sẽ đến mà không có semaphores? Đã có một số câu hỏi về Stack Overflow liên quan đến việc sử dụng semaphores. Tôi sử dụng chúng (semixhores) mọi lúc để một chủ đề chờ một sự kiện nào đó trong một chủ đề khác:

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

  event1.wait();

  ...
}

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

  event1.post();

  ...
}

Nếu tôi sẽ làm điều đó với một mutex:

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

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

  ...
}

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

  doSomethingth1();

  event1.unlock();

  ...
}

Vấn đề: Thật xấu xí và không đảm bảo rằng thread1 sẽ khóa mutex trước (Do cùng một luồng nên khóa và mở khóa một mutex, bạn cũng không thể khóa event1 trước khi thread0 và thread1 bắt đầu).

Vì vậy, boost cũng không có semaphores, cách đơn giản nhất để đạt được những điều trên là gì?


Có thể sử dụng điều kiện mutex và std :: hứa và std :: tương lai?
Yves

Câu trả lời:


180

Bạn có thể dễ dàng xây dựng một từ một mutex và một biến điều kiện:

#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
ai đó nên gửi đề xuất cho các tiêu chuẩn được
khen thưởng

7
Một nhận xét ở đây làm tôi bối rối ban đầu là khóa chờ, người ta có thể hỏi làm thế nào một chủ đề có thể nhận được thông báo nếu khóa được giữ bằng cách chờ? câu trả lời có phần khó hiểu là tài liệu đó là condition_variable. đang chờ khóa, cho phép một luồng khác thông báo theo kiểu nguyên tử, ít nhất đó là cách tôi hiểu nó
Ion Todirel

31
Nó đã bị loại ra khỏi Boost một cách có chủ ý trên cơ sở rằng một semaphore có quá nhiều dây để các lập trình viên tự treo mình. Các biến điều kiện được cho là dễ quản lý hơn. Tôi thấy quan điểm của họ nhưng cảm thấy một chút bảo trợ. Tôi giả định rằng logic tương tự áp dụng cho C ++ 11 - các lập trình viên dự kiến ​​sẽ viết chương trình của họ theo cách "tự nhiên" sử dụng các condvars hoặc các kỹ thuật đồng bộ hóa được phê duyệt khác. Cung cấp một semaphore sẽ chống lại điều đó bất kể nó được thực hiện trên đỉnh của condvar hay nguyên bản.
Steve Jessop

5
Lưu ý - Xem en.wikipedia.org/wiki/Spquil_wakeup để biết lý do đằng sau while(!count_)vòng lặp.
Dan Nissenbaum

3
@Maxim Tôi xin lỗi, tôi không nghĩ bạn đúng. sem_wait và sem_post chỉ chọc vào tranh chấp (kiểm tra sourceware.org/git/?p=glibc.git;a=blob;f=nptl/sem_wait.c ) để mã ở đây kết thúc việc sao chép triển khai libc, với các lỗi có thể xảy ra. Nếu bạn dự định tính di động trên bất kỳ hệ thống nào, đó có thể là một giải pháp, nhưng nếu bạn chỉ cần khả năng tương thích Posix, hãy sử dụng Posix semaphore.
xryl669

107

Dựa trên câu trả lời của Maxim Yegorushkin , tôi đã cố gắng làm ví dụ theo kiểu 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
Bạn có thể thực hiện chờ () cũng là ba lớp lót:cv.wait(lck, [this]() { return count > 0; });
Domi

2
Thêm một lớp khác theo tinh thần của lock_guard cũng hữu ích. Theo kiểu RAII, hàm tạo, lấy semaphore làm tham chiếu, gọi cuộc gọi Wait () của semaphore và hàm hủy gọi cuộc gọi thông báo () của nó. Điều này ngăn chặn các trường hợp ngoại lệ không phát hành semaphore.
Jim Hunziker

không có khóa chết, nếu nói N luồng được gọi là Wait () và Count == 0, thì cv.notify_one (); không bao giờ được gọi, vì mtx chưa được phát hành?
Marcello

1
@Marcello Các chủ đề chờ không giữ khóa. Toàn bộ điểm của các biến điều kiện là cung cấp thao tác "mở khóa và chờ" nguyên tử.
David Schwartz

3
Bạn nên mở khóa trước khi gọi notify_one () để tránh chặn ngay lập tức ... xem tại đây: en.cppreference.com/w/cpp/thread/condition_variable/notify_all
kylefinn

38

Tôi đã quyết định viết semaphore C ++ 11 mạnh mẽ / chung chung nhất có thể, theo kiểu tiêu chuẩn nhất có thể (lưu ý using semaphore = ..., thông thường bạn sẽ chỉ sử dụng tên semaphoretương tự như bình thường stringkhông sử dụng basic_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();
}

Điều này hoạt động, với một chỉnh sửa nhỏ. Các cuộc gọi wait_forwait_untilphương thức với vị từ trả về giá trị boolean (không phải là `std :: cv_status).
jdknight 4/03/2015

Xin lỗi để nit-pick quá muộn trong trò chơi. std::size_tkhông dấu nên giảm xuống dưới 0 là UB và nó sẽ luôn như vậy >= 0. IMHO countnên là một int.
Richard Hodges

3
@RichardHodges không có cách nào giảm xuống dưới 0 nên không có vấn đề gì, và số âm trong một semaphore có nghĩa là gì? Điều đó thậm chí không có ý nghĩa IMO.
David

1
@David Điều gì sẽ xảy ra nếu một chủ đề phải chờ người khác kích hoạt mọi thứ? ví dụ, 1 luồng người đọc để chờ 4 luồng, tôi sẽ gọi hàm tạo semaphore với -3 để làm cho luồng trình đọc chờ cho đến khi tất cả các luồng khác tạo một bài. Tôi đoán có nhiều cách khác để làm điều đó, nhưng nó không hợp lý? Tôi nghĩ thực tế đó là câu hỏi mà OP đang hỏi nhưng với nhiều "thread1" hơn.
jmmut

2
@RichardHodges là rất phạm vi, giảm một loại số nguyên không dấu dưới 0 không phải là UB.
jcai

15

phù hợp với semaphores, tôi sẽ thêm

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

Và tôi thích sử dụng một cơ chế đồng bộ hóa ở mức độ trừu tượng thuận tiện, thay vì luôn luôn sao chép việc dán một phiên bản được ghép với nhau bằng các toán tử cơ bản hơn.


9

Bạn cũng có thể kiểm tra cpp11-on-multicore - nó có triển khai semaphore di động và tối ưu.

Kho lưu trữ cũng chứa các tính năng phân luồng khác bổ sung cho phân luồng c ++ 11.


8

Bạn có thể làm việc với các biến mutex và điều kiện. Bạn có quyền truy cập độc quyền với mutex, kiểm tra xem bạn muốn tiếp tục hay cần đợi đến đầu kia. Nếu bạn cần chờ đợi, bạn chờ trong một điều kiện. Khi luồng khác xác định rằng bạn có thể tiếp tục, nó báo hiệu điều kiện.

Có một ví dụ ngắn trong thư viện boost :: thread mà bạn hầu như có thể chỉ cần sao chép (C ++ 0x và boost lib lib rất giống nhau).


Điều kiện tín hiệu chỉ để chờ đợi chủ đề, hay không? Vì vậy, nếu thread0 không ở đó chờ đợi khi tín hiệu thread1 nó sẽ bị chặn sau? Ngoài ra: Tôi không cần khóa bổ sung đi kèm với điều kiện - đó là chi phí hoạt động.
tauran

Có, điều kiện chỉ tín hiệu chờ chủ đề. Mẫu phổ biến là có một biến với trạng thái và một điều kiện trong trường hợp bạn cần chờ. Hãy suy nghĩ về một nhà sản xuất / người tiêu dùng, sẽ có một số đếm trên các mục trong bộ đệm, nhà sản xuất khóa, thêm phần tử, tăng số lượng và tín hiệu. Người tiêu dùng khóa, kiểm tra bộ đếm và nếu khác không tiêu thụ, trong khi nếu không chờ trong điều kiện.
David Rodríguez - dribeas

2
Bạn có thể mô phỏng một semaphore theo cách này: Khởi tạo một biến với giá trị mà bạn sẽ cung cấp cho semaphore, sau đó wait()được dịch thành "khóa, kiểm tra số nếu giảm không bằng 0 và tiếp tục; nếu không chờ điều kiện" trong khi đó postsẽ là "khóa," bộ đếm tăng, báo hiệu nếu là 0 "
David Rodríguez - dribeas

Ừ nghe được đấy. Tôi tự hỏi nếu semaphores posixhores được thực hiện theo cùng một cách.
tauran

@tauran: Tôi không biết chắc chắn (và nó có thể phụ thuộc vào hệ điều hành Posix nào), nhưng tôi nghĩ là không thể. Semaphores theo truyền thống là một nguyên thủy đồng bộ hóa "cấp độ thấp" hơn các biến thể và biến điều kiện, và về nguyên tắc có thể được thực hiện hiệu quả hơn so với chúng nếu được thực hiện trên đỉnh của một condvar. Vì vậy, nhiều khả năng trong một HĐH nhất định là tất cả các nguyên hàm đồng bộ ở cấp độ người dùng được xây dựng dựa trên một số công cụ phổ biến tương tác với bộ lập lịch.
Steve Jessop

3

Cũng có thể là trình bao bọc semaphore RAII hữu ích trong các chủ đề:

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;
};

Ví dụ sử dụng trong ứng dụng đa luồng:

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 cuối cùng sẽ có semaphores - std::counting_semaphore<max_count> .

Chúng sẽ có (ít nhất) các phương thức sau:

  • acquire() (chặn)
  • try_acquire() (không chặn, trả về ngay lập tức)
  • try_acquire_for() (không chặn, mất một khoảng thời gian)
  • try_acquire_until() (không chặn, mất một thời gian để ngừng cố gắng)
  • release()

Điều này chưa được liệt kê trên cppreference, nhưng bạn có thể đọc các slide thuyết trình CppCon 2019 này hoặc xem video . Cũng có đề xuất chính thức P0514R4 , nhưng tôi không chắc đó là phiên bản cập nhật nhất.


2

Tôi tìm thấy shared_ptr và yếu_ptr, một danh sách dài, đã thực hiện công việc tôi cần. Vấn đề của tôi là, tôi đã có một số khách hàng muốn tương tác với dữ liệu nội bộ của máy chủ lưu trữ. Thông thường, máy chủ tự cập nhật dữ liệu, tuy nhiên, nếu máy khách yêu cầu, máy chủ cần dừng cập nhật cho đến khi không có máy khách nào truy cập dữ liệu máy chủ. Đồng thời, một khách hàng có thể yêu cầu quyền truy cập độc quyền, để không có khách hàng nào khác, cũng như máy chủ lưu trữ có thể sửa đổi dữ liệu máy chủ đó.

Làm thế nào tôi làm điều này là, tôi đã tạo ra một cấu trúc:

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

Mỗi khách hàng sẽ có một thành viên như vậy:

UpdateLock::ptr m_myLock;

Sau đó, máy chủ lưu trữ sẽ có một thành viên yếu_ptr để độc quyền và một danh sách các yếu tố_ptrs cho các khóa không độc quyền:

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

Có một chức năng để cho phép khóa và một chức năng khác để kiểm tra xem máy chủ có bị khóa hay không:

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

Tôi kiểm tra các khóa trong LockUpdate, IsUpdateLocked và định kỳ trong thói quen Cập nhật của máy chủ. Việc kiểm tra khóa cũng đơn giản như kiểm tra xem điểm yếu đã hết hạn và xóa bất kỳ khóa nào đã hết hạn khỏi danh sách m_locks (tôi chỉ thực hiện việc này trong quá trình cập nhật máy chủ), tôi có thể kiểm tra xem danh sách có trống không; đồng thời, tôi nhận được mở khóa tự động khi máy khách đặt lại shared_ptr mà chúng đang treo, điều này cũng xảy ra khi máy khách bị hủy tự động.

Hiệu quả trên tất cả là, vì khách hàng hiếm khi cần độc quyền (thường chỉ dành cho bổ sung và xóa), nên phần lớn thời gian yêu cầu LockUpdate (sai), nghĩa là không độc quyền, thành công miễn là (! M_exinatingLock). Và LockUpdate (đúng), yêu cầu độc quyền, chỉ thành công khi cả (! M_exinatingLock) và (m_locks.empty ()).

Một hàng đợi có thể được thêm vào để giảm thiểu giữa các khóa độc quyền và không độc quyền, tuy nhiên, cho đến nay tôi không có va chạm nào, vì vậy tôi dự định sẽ đợi cho đến khi điều đó xảy ra để thêm giải pháp (chủ yếu là tôi có điều kiện thử nghiệm trong thế giới thực).

Cho đến nay điều này đang hoạt động tốt cho nhu cầu của tôi; Tôi có thể tưởng tượng sự cần thiết phải mở rộng điều này và một số vấn đề có thể phát sinh khi sử dụng mở rộng, tuy nhiên, việc này được thực hiện nhanh chóng và yêu cầu rất ít mã tùy chỉnh.


-4

Trong trường hợp ai đó quan tâm đến phiên bản nguyên tử, đây là cách thực hiện. Hiệu suất được mong đợi tốt hơn so với phiên bản biến đổi điều kiện & điều kiện.

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
Tôi hy vọng hiệu suất sẽ tồi tệ hơn nhiều . Mã này làm cho hầu hết mọi nghĩa đen có thể xảy ra. Chỉ là ví dụ rõ ràng nhất, giả sử waitmã phải lặp nhiều lần. Khi cuối cùng nó không khóa, nó sẽ đưa mẹ của tất cả các nhánh bị dự đoán sai vì dự đoán vòng lặp của CPU chắc chắn sẽ dự đoán nó sẽ lặp lại. Tôi có thể liệt kê nhiều vấn đề hơn với mã này.
David Schwartz

1
Đây là một kẻ giết hiệu năng rõ ràng khác: waitVòng lặp sẽ tiêu tốn tài nguyên vi xử lý CPU khi nó quay. Giả sử nó nằm trong cùng lõi vật lý với luồng được cho là của notifynó - nó sẽ làm chậm luồng đó xuống một cách khủng khiếp.
David Schwartz

1
Và đây chỉ là một điều nữa: Trên các CPU x86 (CPU phổ biến nhất hiện nay), thao tác so sánh_exchange_weak luôn là thao tác ghi, ngay cả khi nó bị lỗi (nó ghi lại cùng giá trị mà nó đã đọc nếu so sánh không thành công). Vì vậy, giả sử hai lõi là cả hai trong một waitvòng lặp cho cùng một semaphore. Cả hai đều ghi ở tốc độ tối đa vào cùng một dòng bộ đệm, có thể làm chậm các lõi khác để thu thập dữ liệu bằng cách bão hòa các xe buýt liên lõi.
David Schwartz

@DavidSchwartz Vui mừng khi thấy ý kiến ​​của bạn. Không chắc chắn hiểu phần '... dự đoán vòng lặp của CPU ...'. Đồng ý cái thứ 2. Rõ ràng trường hợp thứ 3 của bạn có thể xảy ra, nhưng so với mutex khiến chế độ người dùng chuyển sang chế độ kernel và gọi hệ thống, đồng bộ hóa giữa các lõi không tệ hơn.
Jeffery

1
Không có thứ gọi là semaphore khóa miễn phí. Toàn bộ ý tưởng về việc khóa miễn phí không phải là viết mã mà không sử dụng mutexes, mà là viết mã trong đó một luồng không bao giờ chặn cả. Trong trường hợp này, bản chất của semaphore là chặn các luồng gọi hàm Wait ()!
Carlo Wood
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.