【问题标题】:C++ Lock a mutex as if from another thread?C ++像从另一个线程一样锁定互斥锁?
【发布时间】:2018-08-03 08:35:22
【问题描述】:

我正在编写一个 Audio 类,其中包含一个 std::thread 用于异步重新填充一些缓冲区。假设我们调用主线程 A 和后台(类成员)线程 B。我正在使用 std::mutex 来阻止线程 B 每当声音没有播放时,这样它就不会在不必要时在后台运行并且不会不要使用过多的 CPU 资源。默认情况下,线程 A 锁定了互斥锁,因此线程 B 被阻塞,然后当播放声音时,线程 A 解锁互斥锁,线程 B 循环运行(通过锁定然后立即解锁)。

当线程 B 发现它已到达文件末尾时,问题就出现了。它可以停止播放和清理缓冲区等,但它不能停止自己的循环,因为线程 B 无法锁定线程 A 的互斥锁。

以下是相关代码大纲:

class Audio {
private:

    // ...

    std::thread Thread;
    std::mutex PauseMutex;    // mutex that blocks Thread, locked in constructor
    void ThreadFunc();     // assigned to Thread in constructor

 public:

    // ...

    void Play();
    void Stop();
}

_

void Audio::ThreadFunc() {

    // ... (include initial check of mutex here)

    while (!this->EndThread) {    // Thread-safe flag, only set when Audio is destructed

            // ... Check and refill buffers as necessary, etc ...

        if (EOF)
             Stop();

        // Attempt a lock, blocks thread if sound/music is not playing
        this->PauseMutex.lock();
        this->PauseMutex.unlock();
    }
}

void Audio::Play() {
     // ...
     PauseMutex.unlock();     // unlock mutex so loop in ThreadFunc can start
}

void Audio::Stop() {
     // ...
     PauseMutex.lock();     // locks mutex to stop loop in ThreadFunc
     // ^^ This is the issue here
}

在上面的设置中,当后台线程看到它到达 EOF 时,它会调用类的 Stop() 函数,该函数应该锁定互斥体以停止后台线程。这不起作用,因为互斥锁必须由主线程而不是后台线程锁定(在此示例中,它在 ThreadFunc 中崩溃,因为后台线程在锁定 @987654330 后尝试在其主循环中锁定@)。

此时我唯一能想到的就是让后台线程以某种方式锁定互斥锁就好像它是主线程,让主线程拥有互斥锁...如果这可能吗?线程有没有办法将互斥锁的所有权转移给另一个线程?或者这是我创建的设置中的设计缺陷? (如果是后者,是否有任何合理的解决方法?)到目前为止,课堂上的其他所有内容都按设计工作。

【问题讨论】:

  • 这是乞求基于RAII 的锁定方案。我同意上述观点。这需要一些真正的谓词数据、一个保护该数据的互斥体,以及一个表示该数据潜在状态变化的条件变量。如果您发现自己使用std::mutex 类手动调用lockunlock,而这所有您似乎都在做,那么您可能做错了什么。
  • @user463035818 那不需要主线程反复检查它是否应该调用Stop()
  • 您已经在循环中反复锁定和解锁互斥锁。有一个循环while(running) { ... 更自然,其中running 可以由任一线程设置
  • 互斥锁在后台线程中反复被锁定和解锁,同时主线程可能正忙于其他完全不相关的任务

标签: c++ multithreading mutex


【解决方案1】:

我什至不会假装理解你的代码是如何尝试做它正在做的事情的。然而,有一件事是显而易见的。您正在尝试使用互斥锁来传达一些谓词状态更改,这是在高速公路上行驶的错误车辆。

谓词状态变化是通过耦合三件事来处理的:

  • 一些谓词数据
  • 保护谓词的互斥锁
  • 一个条件变量,用于传达谓词状态的可能变化。

目标

以下示例的目标是演示在控制跨多个线程的程序流时如何协同使用互斥锁、条件变量和谓词数据。它显示了使用waitwait_for 条件变量功能的示例,以及将成员函数作为线程过程运行的一种方法。


下面是一个简单的Player 类在四种可能的状态之间切换:

  • 已停止:播放器未播放、未暂停或退出。
  • 正在播放:玩家正在播放
  • 已暂停:播放器已暂停,一旦恢复播放,播放器将从中断处继续播放。
  • 退出:玩家应该停止正在做的事情并终止。

谓词数据相当明显。 state 成员。它必须受到保护,这意味着它不能被更改也不能检查,除非在互斥锁的保护下。我已经添加了一个counter,它只是在保持播放状态一段时间的过程中增加。更具体地说:

  • 播放时,counter 每 200 毫秒递增一次,然后将一些数据转储到控制台。
  • 暂停时,counter 不会更改,但会在播放时保留其最后一个值。这意味着当恢复时,它将从中断处继续。
  • 当停止时,counter 被重置为零,并在控制台输出中注入一个换行符。这意味着切换回“正在播放”将重新开始计数器序列。
  • 设置退出状态对counter 没有影响,它将与其他所有内容一起消失。

守则

#include <iostream>
#include <mutex>
#include <condition_variable>
#include <thread>
#include <unistd.h>

using namespace std::chrono_literals;

struct Player
{
private:
    std::mutex mtx;
    std::condition_variable cv;
    std::thread thr;

    enum State
    {
        Stopped,
        Paused,
        Playing,
        Quit
    };

    State state;
    int counter;

    void signal_state(State st)
    {
        std::unique_lock<std::mutex> lock(mtx);
        if (st != state)
        {
            state = st;
            cv.notify_one();
        }
    }

    // main player monitor
    void monitor()
    {
        std::unique_lock<std::mutex> lock(mtx);
        bool bQuit = false;

        while (!bQuit)
        {
            switch (state)
            {
                case Playing:
                    std::cout << ++counter << '.';
                    cv.wait_for(lock, 200ms, [this](){ return state != Playing; });
                    break;

                case Stopped:
                    cv.wait(lock, [this]() { return state != Stopped; });
                    std::cout << '\n';
                    counter = 0;
                    break;

                case Paused:
                    cv.wait(lock, [this]() { return state != Paused; });
                    break;

                case Quit:
                    bQuit = true;
                    break;
            }
        }
    }

public:
    Player()
        : state(Stopped)
        , counter(0)
    {
        thr = std::thread(std::bind(&Player::monitor, this));
    }

    ~Player()
    {
        quit();
        thr.join();
    }

    void stop() { signal_state(Stopped); }
    void play() { signal_state(Playing); }
    void pause() { signal_state(Paused); }
    void quit() { signal_state(Quit); }
};

int main()
{
    Player player;
    player.play();
    sleep(3);
    player.pause();
    sleep(3);
    player.play();
    sleep(3);
    player.stop();
    sleep(3);
    player.play();
    sleep(3);
}

输出

我无法真正证明这一点。您必须运行它并查看它是如何工作的,我邀请您像上面所说的那样玩弄main() 中的状态。但是请注意,一旦调用quit,将不会监视任何其他声明。设置 Quit 状态将关闭监视器线程。对于它的价值,上面的运行应该是这样的:

1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.
1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.

第一组号码被分成两组(1..15,然后是 16..30),作为播放的结果,然后暂停,然后再次播放。然后发出停止信号,然后再次播放约 3 秒。之后,对象自毁,并在此过程中设置 Quit 状态,并等待监视器终止。

总结

希望你能从中有所收获。如果您发现自己试图通过手动锁定和释放互斥锁来管理谓词状态,那么您需要一个条件变量设计模式来帮助检测这些更改。

希望你能从中有所收获。

【讨论】:

  • Quit 的情况下,您可以直接从monitor() 返回,而不是bQuit
  • @Caleth 是的。我是老同学。单出口。只是过去三年做这件事的习惯。
  • 谢谢,我认为现在这一切都说得通了。我对多线程还是很陌生,我假设std::condition_variable::wait() 会阻塞线程直到满足条件(内部没有循环或其他什么)?因为这样做的目的是节省 CPU 使用率(如果不可能的话,我可以忍受)
  • @TheJared802 wait and it's siblings are documented。如果您不熟悉 pthreads 的工作原理,它非常相似(并且相似,我的意思是相同)。人们在使用它们时最常犯的错误是没有意识到它们只是一种宣布谓词状态变化的信号机制。实际谓词数据本身是独立的,并且需要在读取和写入时互斥以确保正确的并发性。人们经常尝试使用 cvars 作为自己的状态;这是错误的。阅读此评论中的链接。
  • @TheJared802 最后,冒着听起来自我满足的风险,see this answer。它使用 C pthreads,并且是对实际上是一个简单问题的一个非常冗长的答案,但详细演示了条件变量、互斥锁和一些谓词数据如何在几个示例中协同使用。适应 C++ 的线程支持库将是微不足道的,并且可能值得作为练习。
【解决方案2】:
class CtLockCS
{
public:
    //--------------------------------------------------------------------------
    CtLockCS()      { ::InitializeCriticalSection(&m_cs); }
    //--------------------------------------------------------------------------
    ~CtLockCS()     { ::DeleteCriticalSection(&m_cs); }
    //--------------------------------------------------------------------------
    bool TryLock()  { return ::TryEnterCriticalSection(&m_cs) == TRUE; }
    //--------------------------------------------------------------------------
    void Lock()     { ::EnterCriticalSection(&m_cs); }
    //--------------------------------------------------------------------------
    void Unlock()   { ::LeaveCriticalSection(&m_cs); }
    //--------------------------------------------------------------------------
protected:
    CRITICAL_SECTION m_cs;
};


///////////////////////////////////////////////////////////////////////////////
// class CtLockMX - using mutex

class CtLockMX
{
public:
    //--------------------------------------------------------------------------
    CtLockMX(const TCHAR* nameMutex = 0)
        { m_mx = ::CreateMutex(0, FALSE, nameMutex); }
    //--------------------------------------------------------------------------
    ~CtLockMX()
        { if (m_mx) { ::CloseHandle(m_mx); m_mx = NULL; } }
    //--------------------------------------------------------------------------
    bool TryLock()
        {   return m_mx ? (::WaitForSingleObject(m_mx, 0) == WAIT_OBJECT_0) : false; }
    //--------------------------------------------------------------------------
    void Lock()
        {   if (m_mx)   { ::WaitForSingleObject(m_mx, INFINITE); }  }
    //--------------------------------------------------------------------------
    void Unlock()
        {   if (m_mx)   { ::ReleaseMutex(m_mx); }   }
    //--------------------------------------------------------------------------
protected:
    HANDLE  m_mx;
};


///////////////////////////////////////////////////////////////////////////////
// class CtLockSM - using semaphore

class CtLockSM
{
public:
    //--------------------------------------------------------------------------
    CtLockSM(int maxcnt)    { m_sm = ::CreateSemaphore(0, maxcnt, maxcnt, 0); }
    //--------------------------------------------------------------------------
    ~CtLockSM()             { ::CloseHandle(m_sm); }
    //--------------------------------------------------------------------------
    bool TryLock()          { return m_sm ? (::WaitForSingleObject(m_sm, 0) == WAIT_OBJECT_0) : false;  }
    //--------------------------------------------------------------------------
    void Lock()             { if (m_sm) { ::WaitForSingleObject(m_sm, INFINITE); }  }
    //--------------------------------------------------------------------------
    void Unlock()
    {
        if (m_sm){
            LONG prevcnt = 0;
            ::ReleaseSemaphore(m_sm, 1, &prevcnt);
        }
    }
    //--------------------------------------------------------------------------
protected:
    HANDLE  m_sm;
};

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2023-03-21
    • 2023-03-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多