Рекурсивное блокирование мьютекса, знакомимся с std::recursive_mutex.

Всем привет! Случайно вспомнил, что мой блог про программирование, математику ващет и в целом создан для просвещения мешков с костями и мясом.

Поэтому хочу рассказать про std::recursive_mutex. Он такой же, как и std::mutex, но запоминает номер потока, из которого был заблокирован и вторую блокировку в том же потоке игнорирует. В моём случае был приблизительно такой код:

struct Item {
    ~Item() {
        getStorage().RemoveItem(otherItemId);
        getStorage().AddItem(newItem);
    }
}
using ItemPtr = std::shared_ptr<Item>;
using ItemRef = const ItemPtr&;
using ItemId = std::size_t;
 
class Storage {
public:
    ItemId AddItem(ItemRef item)
    {
        std::scoped_lock lock(m_itemsGuard);
        m_items.push_back(item);
        return m_items.size() - 1;
    }
 
    bool HasItem(ItemId id) {
        return m_items.size() < id;
    }
 
    void RemoveItem(ItemId id)
    {
        std::scoped_lock lock(m_itemsGuard);
        auto item = m_items.at(id);
        m_items.erase(m_items.begin() + id);
        item.reset(); // Тут повторно заходим в RemoveItem
    }
 
private:
    std::vector<ItemPtr> m_items;
    std::recursive_mutex m_itemsGuard;
};
 
 
void test() {
    Storage storage;
    auto itemId = storage.AddItem(std::make_shared<Item>());
    storage.RemoveItem(itemId); // здесь проблема
}

Обратите внимание, что в деструкторе Item есть обращение к методам Storage, а там на каждое изменение массива стоит блокировка мьютекса. И оно попадет в вечный lock с std::mutex. А на этом у меня всё. Enjoy!

Почему Thread.sleep() — это прошлый век

Допустим, есть задача: создать поток, который скачивает что-то из интернета, а из основного потока ему валятся задания. Стандартный путь реализации такой:

class DownloadThread extends Thread {
    public interface Listener {
        void onImageLoaded(Image image);
    }
 
    private String reqUrl_ = null;
    private final Listener listener_;
    private boolean isFinished_ = false;
 
    DownloadThread(Listener listener) {
        listener_ = listener;
    }
 
    public void imageRequest(String url) {
        synchronized (this) {
            // Запрос на загрузку изображения
            reqUrl_ = url;
        }
    }
 
    @Override
    public void run() {
        while (!isFinished_)
            String url = null;
 
            synchronized (this) {
                // Проверка: есть ли запрос?
                if (null != reqUrl_) {
                    url = reqUrl_;
                    reqUrl_ = null;
                }
            }
 
            if (null != url) {
                // Запрос есть, загружаем изображение
                Image image = download(url);
                listener_.onImageLoaded(image);
            }
 
            try {
                // Поспим одну милисекунду, чтоб не грузить CPU
                sleep(1);
            }
            catch (InterruptedException e) {
                isFinished_ = true;
                break;
            }
        }
    }
 
    public void stopThread() {
        isFinished_ = true;
    }
}

Почему этот плохой, негодный пример? Потому что, когда заданий много, между ними одна миллисекунда бездействия минимум — это во-первых. Во-вторых, если задач для потока нет, то он работает без всякой пользы, создавая лишнюю нагрузку процессору.

Рассмотрим пример номер два:

class DownloadThread extends Thread {
    public interface Listener {
        void onImageLoaded(Image image);
    }
 
    private String reqUrl_ = null;
    private final Listener listener_;
    private boolean isFinished_ = false;
 
    DownloadThread(Listener listener) {
        listener_ = listener;
    }
 
    public void imageRequest(String url) {
        synchronized (this) {
            // Запрос на загрузку изображения
            reqUrl_ = url;
            // Уведомляем поток, что есть задание
            notify();
        }
    }
 
    @Override
    public void run() {
        while (!isFinished_)
            String url = null;
 
            synchronized (this) {
                if (isFinished_) {
                    break;
                }
                // Проверка: есть ли запрос?
                if (null != reqUrl_) {
                    url = reqUrl_;
                    reqUrl_ = null;
                }
                else {
                    try {
                        // Ждем вызова notify
                        wait();
                    }
                    catch (InterruptedException e) {
                        isFinished_ = true;
                        break;
                    }
                }
            }
 
            if (null != url) {
                // Запрос есть, загружаем изображение
                Image image = download(url);
                listener_.onImageLoaded(image);
            }
        }
    }
 
 
    public void stopThread() {
        synchronized (this) {
            isFinished_ = true;
            notify();
        }
    }
 
}

Почему этот пример лучше первого? Во-первых, когда заданий нет, поток находится в режиме ожидания без вызовов sleep(). Во-вторых, когда задание приходит, поток реагирует моментально просыпаясь.

Обратите внимание, что метод stopThread() слегка изменился. Ввиду того, что поток не выйдет из состояния wait() без вызова notify().

UPD: По совету друзей исправил метод run() для корректного завершения потока.

В обоих примерах не учитывается, что изображения нам нужны пачкой сразу, то есть очередь запросов выкинута для упрощения кода.

P.S. Кстати, в C/C++ для тех же целей можно использовать pthread_cond_wait() и pthread_cond_signal()

Блог Евгения Жирнова