C ++ 0x tidak memiliki semaphores? Bagaimana cara menyinkronkan utas?


135

Benarkah C ++ 0x akan datang tanpa semaphores? Sudah ada beberapa pertanyaan tentang Stack Overflow mengenai penggunaan semaphores. Saya menggunakannya (posix semaphores) setiap saat untuk membiarkan utas menunggu acara di utas lain:

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

  event1.wait();

  ...
}

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

  event1.post();

  ...
}

Jika saya akan melakukannya dengan mutex:

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

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

  ...
}

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

  doSomethingth1();

  event1.unlock();

  ...
}

Masalah: Ini jelek dan tidak dijamin bahwa thread1 mengunci mutex pertama (Mengingat bahwa thread yang sama harus mengunci dan membuka kunci mutex, Anda juga tidak dapat mengunci event1 sebelum thread0 dan thread1 dimulai).

Jadi karena boost juga tidak memiliki semaphores, apa cara paling sederhana untuk mencapai hal di atas?


Mungkin menggunakan kondisi mutex dan std :: janji dan std :: masa depan?
Yves

Jawaban:


179

Anda dapat dengan mudah membangunnya dari mutex dan variabel kondisi:

#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
seseorang harus mengajukan proposal ke komite standar

7
komentar di sini yang membuat saya bingung pada awalnya adalah kunci menunggu, orang mungkin bertanya bagaimana sebuah thread dapat melewati pemberitahuan jika kunci dipegang oleh menunggu? jawaban yang agak kurang jelas didokumentasikan adalah condition_variable. tunggu pulsa kunci, memungkinkan utas lainnya melewati pemberitahuan dengan cara atomik, setidaknya begitulah cara saya memahaminya

31
Itu sengaja dikeluarkan dari Boost dengan alasan bahwa semaphore terlalu banyak tali bagi programmer untuk menggantung diri. Variabel kondisi seharusnya lebih mudah dikelola. Saya mengerti maksud mereka tetapi merasa sedikit dilindungi. Saya berasumsi bahwa logika yang sama berlaku untuk C ++ 11 - programmer diharapkan untuk menulis program mereka dengan cara yang "secara alami" menggunakan condvars atau teknik sinkronisasi yang disetujui lainnya. Menyediakan semaphore akan bertentangan dengan itu terlepas dari apakah itu diterapkan di atas condvar atau asli.
Steve Jessop

5
Catatan - Lihat en.wikipedia.org/wiki/Spurious_wakeup untuk alasan di balik while(!count_)loop.
Dan Nissenbaum

3
@ Maxim Maaf, saya pikir Anda tidak benar. sem_wait dan sem_post hanya syscall pada pertikaian juga (periksa sourceware.org/git/?p=glibc.git;a=blob;f=nptl/sem_wait.c ) sehingga kode di sini berakhir menduplikasi implementasi libc, dengan kemungkinan bug. Jika Anda bermaksud portabilitas pada sistem apa pun, itu mungkin menjadi solusi, tetapi jika Anda hanya perlu kompatibilitas Posix, gunakan posix semaphore.
xryl669

107

Berdasarkan jawaban Maxim Yegorushkin , saya mencoba membuat contoh dalam gaya 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
Anda dapat membuat wait () juga tiga-liner:cv.wait(lck, [this]() { return count > 0; });
Domi

2
Menambahkan kelas lain dalam semangat lock_guard juga membantu. Dalam mode RAII, konstruktor, yang menggunakan semaphore sebagai referensi, memanggil panggilan wait () semaphore, dan destructor memanggil panggilan notify (). Ini mencegah pengecualian dari gagal untuk melepaskan semaphore.
Jim Hunziker

tidak ada kunci mati, jika katakan N utas disebut tunggu () dan hitung == 0, lalu cv.notify_one (); tidak pernah dipanggil, karena mtx belum dirilis?
Marcello

1
@ Marscello Utas tunggu tidak memegang kunci. Seluruh titik variabel kondisi adalah untuk menyediakan operasi "buka dan tunggu" atom.
David Schwartz

3
Anda harus melepaskan kunci sebelum memanggil notify_one () untuk menghindari pemblokiran segera bangun ... lihat di sini: en.cppreference.com/w/cpp/thread/condition_variable/notify_all
kylefinn

38

Saya memutuskan untuk menulis semaphore C ++ 11 yang paling kuat / generik yang saya bisa, dengan gaya standar sebanyak yang saya bisa (perhatikan using semaphore = ..., Anda biasanya hanya akan menggunakan nama yang semaphoremirip dengan yang biasanya stringtidak menggunakan 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();
}

Ini berfungsi, dengan suntingan kecil. The wait_fordan wait_untilmetode panggilan dengan predikat mengembalikan nilai boolean (bukan `std :: cv_status).
jdknight

maaf untuk nit-pick sangat terlambat dalam permainan. std::size_ttidak ditandatangani sehingga mengurangi di bawah nol adalah UB, dan itu akan selalu terjadi >= 0. IMHO countharus menjadi int.
Richard Hodges

3
@RichardHodges tidak ada cara untuk mengurangi di bawah nol sehingga tidak ada masalah, dan apa artinya negatif pada semafor artinya? Itu bahkan tidak masuk akal IMO.
David

1
@ David Bagaimana jika utas harus menunggu orang lain untuk memulai sesuatu? misalnya, 1 pembaca utas untuk menunggu 4 utas, saya akan memanggil semaphore constructor dengan -3 untuk membuat utas pembaca menunggu sampai semua utas lainnya membuat posting. Saya kira ada cara lain untuk melakukan itu, tetapi bukankah itu masuk akal? Saya pikir itu sebenarnya pertanyaan OP tetapi dengan lebih "thread1".
jmmut

2
@RichardHodges menjadi sangat bertele-tele, mengurangi tipe integer yang tidak ditandatangani di bawah 0 bukanlah UB.
jcai

15

sesuai dengan posix semaphores, saya akan menambahkan

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

Dan saya lebih suka menggunakan mekanisme sinkronisasi pada tingkat abstraksi yang nyaman, daripada selalu menyalin paste versi yang dijahit menggunakan operator yang lebih dasar.


9

Anda juga dapat melihat cpp11-on-multicore - ini memiliki implementasi semaphore yang portabel dan optimal.

Repositori juga berisi barang threading lain yang melengkapi threading c ++ 11.


8

Anda dapat bekerja dengan variabel mutex dan kondisi. Anda mendapatkan akses eksklusif dengan mutex, periksa apakah Anda ingin melanjutkan atau perlu menunggu ujung lainnya. Jika Anda perlu menunggu, Anda menunggu dalam kondisi. Ketika utas lainnya menentukan bahwa Anda dapat melanjutkan, itu menandakan kondisi.

Ada contoh singkat di pustaka boost :: thread yang kemungkinan besar bisa Anda salin (C ++ 0x dan boost lib thread sangat mirip).


Isyarat sinyal hanya ke utas menunggu, atau tidak? Jadi jika thread0 tidak ada menunggu ketika sinyal thread1 akan diblokir nanti? Plus: Saya tidak perlu kunci tambahan yang datang dengan kondisi - itu overhead.
tauran

Ya, kondisi hanya menandakan utas menunggu. Pola umum adalah memiliki variabel dengan keadaan dan kondisi jika Anda perlu menunggu. Pikirkan produsen / konsumen, akan ada hitungan pada item dalam buffer, kunci produsen, tambahkan elemen, kenaikan jumlah dan sinyal. Konsumen mengunci, memeriksa konter dan apakah konsumsi non-nol, sedangkan jika nol menunggu dalam kondisi.
David Rodríguez - dribeas

2
Anda dapat mensimulasikan semaphore dengan cara ini: Menginisialisasi variabel dengan nilai yang akan Anda berikan semaphore, kemudian wait()diterjemahkan ke "kunci, periksa jumlah jika pengurangan tidak nol dan lanjutkan; jika nol menunggu dengan kondisi" sementara postakan "mengunci, penghitung kenaikan, memberi sinyal jika itu 0 "
David Rodríguez - dribeas

Ya, kedengarannya bagus. Saya bertanya-tanya apakah posix semaphores diimplementasikan dengan cara yang sama.
tauran

@tauran: Saya tidak tahu pasti (dan itu mungkin tergantung pada OS Posix), tapi saya pikir tidak mungkin. Semafor secara tradisional adalah sinkronisasi "tingkat rendah" yang primitif daripada variabel mutex dan kondisi, dan pada prinsipnya dapat dibuat lebih efisien daripada yang seharusnya jika diterapkan di atas sebuah condvar. Jadi, lebih mungkin dalam OS yang diberikan adalah bahwa semua primitif sinkronisasi tingkat pengguna dibangun di atas beberapa alat umum yang berinteraksi dengan penjadwal.
Steve Jessop

3

Juga dapat bermanfaat pembungkus RAII semaphore di utas:

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

Contoh penggunaan dalam aplikasi multithread:

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 akhirnya akan memiliki semaphores - std::counting_semaphore<max_count>.

Ini akan memiliki (setidaknya) metode berikut:

  • acquire() (pemblokiran)
  • try_acquire() (tanpa pemblokiran, mengembalikan dengan segera)
  • try_acquire_for() (non-blocking, membutuhkan waktu)
  • try_acquire_until() (non-blocking, membutuhkan waktu untuk berhenti mencoba)
  • release()

Ini belum terdaftar di cppreference, tetapi Anda dapat membaca slide presentasi CppCon 2019 ini , atau menonton video . Ada juga proposal resmi P0514R4 , tapi saya tidak yakin itu versi yang paling mutakhir.


2

Saya menemukan shared_ptr dan lemah_ptr, panjang dengan daftar, melakukan pekerjaan yang saya butuhkan. Masalah saya adalah, saya punya beberapa klien yang ingin berinteraksi dengan data internal host. Biasanya, tuan rumah memperbarui data itu sendiri, namun, jika klien memintanya, tuan rumah harus berhenti memperbarui hingga tidak ada klien yang mengakses data host. Pada saat yang sama, klien dapat meminta akses eksklusif, sehingga tidak ada klien lain, atau tuan rumah, yang dapat mengubah data host tersebut.

Bagaimana saya melakukan ini, saya membuat sebuah struct:

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

Setiap klien akan memiliki anggota seperti:

UpdateLock::ptr m_myLock;

Kemudian tuan rumah akan memiliki anggota lemah_ptr untuk eksklusivitas, dan daftar lemah_ptr untuk kunci non-eksklusif:

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

Ada fungsi untuk mengaktifkan kunci, dan fungsi lainnya untuk memeriksa apakah host terkunci:

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

Saya menguji kunci di LockUpdate, IsUpdateLocked, dan secara berkala di rutin pembaruan host. Menguji kunci adalah sesederhana memeriksa apakah lemah_ptr kadaluarsa, dan menghapus apa pun yang kadaluwarsa dari daftar m_locks (saya hanya melakukan ini selama pembaruan host), saya dapat memeriksa apakah daftar kosong; pada saat yang sama, saya mendapatkan pembukaan kunci otomatis ketika klien me-reset shared_ptr yang mereka gunakan, yang juga terjadi ketika klien dihancurkan secara otomatis.

Efek keseluruhannya adalah, karena klien jarang memerlukan eksklusivitas (biasanya dicadangkan untuk tambahan dan penghapusan saja), sebagian besar waktu permintaan untuk LockUpdate (false), yaitu non-eksklusif, berhasil selama (! M_exclusiveLock). Dan LockUpdate (true), permintaan eksklusivitas, hanya berhasil ketika keduanya (! M_exclusiveLock) dan (m_locks.empty ()).

Antrian dapat ditambahkan untuk mengurangi antara kunci eksklusif dan non-eksklusif, namun, saya belum memiliki tabrakan sejauh ini, jadi saya bermaksud menunggu sampai itu terjadi untuk menambahkan solusi (kebanyakan jadi saya memiliki kondisi pengujian dunia nyata).

Sejauh ini ini bekerja dengan baik untuk kebutuhan saya; Saya bisa membayangkan perlunya memperluas ini, dan beberapa masalah yang mungkin timbul karena penggunaan yang diperluas, namun, ini cepat untuk diterapkan, dan hanya memerlukan sedikit kode khusus.


-4

Jika seseorang tertarik pada versi atom, ini adalah implementasinya. Kinerja diharapkan lebih baik daripada versi variabel mutex & kondisi.

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
Saya berharap kinerjanya menjadi jauh lebih buruk. Kode ini membuat hampir setiap kesalahan yang mungkin terjadi secara harfiah. Sebagai contoh yang paling jelas, anggap waitkode harus berulang beberapa kali. Ketika akhirnya membuka blokir, itu akan mengambil ibu dari semua cabang yang salah diprediksi sebagai prediksi loop CPU pasti akan memprediksi itu akan berulang lagi. Saya bisa mendaftar lebih banyak masalah dengan kode ini.
David Schwartz

1
Berikut ini adalah pembunuh kinerja yang jelas: waitLoop akan mengkonsumsi sumber daya eksekusi mikro CPU saat berputar. Misalkan ada di inti fisik yang sama dengan utas yang seharusnya notify- itu akan memperlambat utas itu.
David Schwartz

1
Dan ini satu lagi: Pada CPU x86 (CPU paling populer saat ini), operasi compare_exchange_weak selalu merupakan operasi tulis, bahkan jika gagal (ia menulis kembali nilai yang sama dengan yang dibacanya jika perbandingan gagal). Jadi misalkan dua core keduanya dalam satu waitlingkaran untuk semaphore yang sama. Keduanya menulis dengan kecepatan penuh ke baris cache yang sama , yang dapat memperlambat core lainnya untuk merayap dengan menjenuhkan bus antar-inti.
David Schwartz

@ DavidSchwartz Senang melihat komentar Anda. Tidak yakin memahami bagian '... prediksi loop CPU ...'. Menyetujui yang ke-2. Rupanya kasus ke-3 Anda dapat terjadi, tetapi dibandingkan dengan mutex yang menyebabkan mode pengguna ke mode kernel beralih dan panggilan sistem, sinkronisasi antar-inti tidak lebih buruk.
Jeffery

1
Tidak ada yang namanya semaphore bebas kunci. Gagasan menjadi bebas kunci bukan untuk menulis kode tanpa menggunakan mutex, tetapi untuk menulis kode di mana utas tidak pernah menghalangi sama sekali. Dalam hal ini intisari semaphore adalah untuk memblokir utas yang memanggil fungsi wait ()!
Carlo Wood
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.