я реализовал проблему с читателями/писателями в С++ 11... Я хотел бы знать, что с ним не так, потому что подобные вещи трудно предсказать самостоятельно.
в примере есть 3 читателя и 1 писатель, но также используют 2 или более автора....
Код:
class ReadersWriters {
private:
int AR; // number of active readers
int WR; // number of waiting readers
int AW; // number of active writers
int WW; // number of waiting writers
mutex lock;
mutex m;
condition_variable okToRead;
condition_variable okToWrite;
int data_base_variable;
public:
ReadersWriters() : AR(0), WR(0), AW(0), WW(0), data_base_variable(0) {}
void read_lock() {
unique_lock<mutex> l(lock);
WR++; // no writers exist
// is it safe to read?
okToRead.wait(l, [this](){ return WW == 0; });
okToRead.wait(l, [this](){ return AW == 0; });
WR--; // no longer waiting
AR++; // now we are active
}
void read_unlock() {
unique_lock<mutex> l(lock);
AR--; // no longer active
if (AR == 0 && WW > 0) { // no other active readers
okToWrite.notify_one(); // wake up one writer
}
}
void write_lock() {
unique_lock<mutex> l(lock);
WW++; // no active user exist
// is it safe to write?
okToWrite.wait(l, [this](){ return AR == 0; });
okToWrite.wait(l, [this](){ return AW == 0; });
WW--; // no longer waiting
AW++; // no we are active
}
void write_unlock() {
unique_lock<mutex> l(lock);
AW--; // no longer active
if (WW > 0) { // give priority to writers
okToWrite.notify_one(); // wake up one writer
}
else if (WR > 0) { // otherwize, wake reader
okToRead.notify_all(); // wake all readers
}
}
void data_base_thread_write(unsigned int thread_id) {
for (int i = 0; i < 10; i++) {
write_lock();
data_base_variable++;
m.lock();
cout << "data_base_thread: " << thread_id << "...write: " << data_base_variable << endl;
m.unlock();
write_unlock();
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
}
void data_base_thread_read(unsigned int thread_id) {
for (int i = 0; i < 10; i++) {
read_lock();
m.lock();
cout << "data_base_thread: " << thread_id << "...read: " << data_base_variable << endl;
m.unlock();
read_unlock();
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
}
};
int main() {
// your code goes here
ReadersWriters rw;
thread w1(&ReadersWriters::data_base_thread_write, &rw, 0);
thread r1(&ReadersWriters::data_base_thread_read, &rw, 1);
thread r2(&ReadersWriters::data_base_thread_read, &rw, 2);
thread r3(&ReadersWriters::data_base_thread_read, &rw, 3);
w1.join();
r1.join();
r2.join();
r3.join();
cout << "\nThreads successfully completed..." << endl;
return 0;
}
Обратная связь:
1
. В нем отсутствуют все необходимые #includes.
2
. Он предполагает using namespace std
, что является плохим стилем в объявлениях, поскольку это загрязняет всех ваших клиентов с пространством имен std.
3
. Выпуск ваших замков не является безопасным:
write_lock();
data_base_variable++;
m.lock();
cout << "data_base_thread: " << thread_id << "...write: " << data_base_variable << endl;
m.unlock(); // leaked if an exception is thrown after m.lock()
write_unlock(); // leaked if an exception is thrown after write_lock()
4
. m.lock()
cout
в data_base_thread_write
действительно не нужна, поскольку write_lock()
должен уже предоставлять эксклюзивный доступ. Однако я понимаю, что это всего лишь демоверсия.
5
. Я думаю, что я вижу ошибку в логике чтения/записи:
step 1 2 3 4 5 6
WR 0 1 1 1 0 0
AR 0 0 0 0 1 1
WW 0 0 1 1 1 0
AW 1 1 1 0 0 1
На шаге 1 поток 1 имеет блокировку записи.
На шаге 2 поток 2 пытается получить блокировку чтения, увеличивает WR
и блокирует второй okToRead
, ожидая AW == 0
.
На шаге 3 поток 3 пытается получить блокировку записи, увеличивает WW
и блокирует второй okToWrite
, ожидая AW == 0
.
На шаге 4 поток 1 освобождает, блокирует запись, okToWrite
AW
до 0 и сигнализирует okToWrite
.
На шаге 5 поток 2, несмотря на то, что он не сигнализирован, пробуждается ложно, отмечает, что AW == 0
, и захватывает блокировку чтения, устанавливая WR
в 0 и AR
на 1.
На шаге 6 поток 3 принимает сигнал, отмечает, что AW == 0
, и захватывает блокировку записи, устанавливая WW
в 0 и AW
на 1.
На этапе 6 оба потока 2 владеют блокировкой чтения, а поток 3 владеет блокировкой записи (одновременно).
6
. Класс ReadersWriters
имеет две функции:
Лучшая конструкция будет использовать фреймы mutex/lock, установленные в С++ 11:
Создайте мьютекс ReaderWriter
с членами:
// unique ownership
void lock(); // write_lock
void unlock(); // write_unlock
// shared ownership
lock_shared(); // read_lock
unlock_shared(); // read_unlock
Первые два имени, lock
и unlock
являются целенаправленно теми же именами, что и те, которые используются в Mutex С++ 11. Просто это позволяет вам делать такие вещи, как:
std::lock_guard<ReaderWriter> lk1(mut);
// ...
std::unique_lock<ReaderWriter> lk2(mut);
// ...
std::condition_variable_any cv;
cv.wait(lk2); // wait using the write lock
И если вы добавите:
void try_lock();
Затем вы также можете:
std::lock(lk2, <any other std or non-std locks>); // lock multiple locks
lock_shared
и unlock_shared
выбраны из-за типа std::shared_lock<T>
который в настоящее время находится в С++ 1y (мы надеемся, что y is 4) работает над проектом. Он задокументирован в N3659. И тогда вы можете сказать такие вещи, как:
std::shared_lock<ReaderWriter> lk3(mut); // read_lock
std::condition_variable_any cv;
cv.wait(lk3); // wait using the read lock
Т.е. просто создавая автономный ReaderWriter
мьютекса ReaderWriter
с очень тщательно подобранными именами для функций-членов, вы получаете возможность взаимодействия с std-определенными блокировками, condition_variable_any
и алгоритмами блокировки.
См. N2406 для более глубокого обоснования этой структуры.