C++中的线程间通信

3

我有两个线程(应用程序的主线程和另一个线程)。我正在使用OpenGL绘制一些内容,并使用OpenGL键盘和鼠标回调。当我调用glutMainLoop()时,OpenGL会阻塞,因此我必须在后台进行一些计算,创建了另一个线程。现在,OpenGL回调应该向具有关键部分的另一个线程发送一些数据(例如按下的鼠标/键的x,y位置)。在关键部分运行时不应接受任何消息,但我希望在关键部分之后处理这些消息而不是丢弃这些消息。非OpenGL类看起来像这样:

void run()
{
    for (;;) {
        int currentTime = now();
        if (now() - previousTime > WAIT_INTERVAL) {
            previousTime = currentTime;
            tick();
        }
    }
}

void tick() {
    // critical section begins
    processor->step()
    // critical section ends
}

void receiveMessage(void *data) {
    processor->changeSomeData(data);
}

因此,如果从OpenGL线程调用receiveMessage()并且processor->step()正在运行,则应该推迟对changeSomeData()的调用,因为它会破坏整个计算过程。

我想使用以下类来同步线程:

Mutex.h:

#ifndef MUTEX_H
#define MUTEX_H

#include <Windows.h>

class Mutex;

#include "Lock.h"

class Mutex
{
public:
    Mutex();
    ~Mutex();
private:
    void acquire();
    void release();

    CRITICAL_SECTION criticalSection;

    friend class Lock;
};


#endif

Mutex.cpp:

#include "Mutex.h"

Mutex::Mutex()
{
    InitializeCriticalSection(&this->criticalSection);
}

Mutex::~Mutex()
{
    DeleteCriticalSection(&this->criticalSection);
}

void Mutex::acquire()
{
    EnterCriticalSection(&this->criticalSection);
}

void Mutex::release()
{
    LeaveCriticalSection(&this->criticalSection);
}

Lock.h:

#ifndef LOCK_H
#define LOCK_H

class Lock;

#include "Mutex.h"

class Lock
{
public:
    Lock(Mutex& mutex);
    ~Lock();
private:
    Mutex &mutex;
};

#endif

Lock.cpp

#include "Lock.h"

Lock::Lock(Mutex& mutex) : mutex(mutex)
{
    this->mutex.acquire();
}

Lock::~Lock ()
{
    this->mutex.release();
}

编辑:

这是整个项目:http://upload.visusnet.de/uploads/BlobbyWarriors-rev30.zip(约180 MB)。

编辑2:

这是 SVN 代码库:https://projects.fse.uni-due.de/svn/alexander-mueller-bloby-warriors/trunk/


我该怎么做?我尝试将Mutex对象添加到非OpenGL线程类中,并在关键部分之前和receiveMessage()的开头添加了Lock lock(mutex),但这些部分并没有互斥。 - Alexander Müller
很难理解你问题的解释。你能否发布更多的代码,准确地展示你的RAII锁类在哪里被实例化?另外,请发布另一个线程(运行OpenGL代码的线程)的桩程序,并展示你的锁是如何插入其中的。 - rcv
4个回答

4
哦... 不,不,不。在这种情况下,线程并不是你应该使用的。认真点,线程不是你在这个特定情况下的解决方案。让我们回到过去...
目前你正在使用GLUT,你说你需要线程来“避免在glutMainLoop()上锁定”。而且你不想锁定,因为你想在此期间进行一些计算。
现在停下来问问自己 - 你确定那些操作需要与OpenGL渲染异步地(作为一个整体)完成吗?如果是这样,你可以停止阅读本篇文章并查看其他文章,但我真诚地相信,在+-典型的实时OpenGL应用程序中可能不是这种情况。
所以...一个典型的OpenGL应用程序看起来像这样:
  • 处理事件
  • 执行计算
  • 重新绘制屏幕
大多数GL窗口库都允许你将其作为自己的主循环实现,GLUT通过其“回调”有点模糊了这一点,但思路是一样的。
你仍然可以在应用程序中引入并行性,但它应该从第2步开始并在该级别上仍然是顺序的:“计算一帧计算,然后渲染这一帧”。这种方法很可能会为你节省很多麻烦
专业提示:更改你的库。GLUT已经过时并且不再维护。切换到GLFW(或SDL)用于窗口创建不需要太多代码工作量,与GLUT相反,你可以自己定义主循环,这似乎是你想要在这里实现的。 (此外,它们倾向于更方便的输入和窗口事件处理等。)
一些具有恒定时间步长的实时物理学的典型伪代码,而不会干扰渲染(通常假设你想要运行物理学比渲染更频繁):
var accum = 0
const PHYSICS_TIMESTEP = 20
while (runMainLoop) {
    var dt = getTimeFromLastFrame

    accum += dt
    while (accum > PHYSICS_TIMESTEP) {
        accum -= PHYSICS_TIMESTEP
        tickPhysicsSimulation(PHYSICS_TIMESTEP)
    }

    tickAnyOtherLogic(dt)
    render()
}

可能的扩展是将accum的值作为额外的“外推”值仅用于渲染,这将允许在模拟物理时更少地进行(使用更大的DT),可能比每个渲染帧更少地进行。同时,保持图形表示平滑。


我的之前版本使用主循环进行计算,但这会大大降低FPS。我需要做的计算是物理计算。我正在尝试在另一个线程中渲染物理世界,以便图形与物理分离。 - Alexander Müller
...而主要问题是物理计算必须在一个时间间隔内完成,这个时间间隔需要始终保持相同。我将整个源代码添加到我的初始帖子中... - Alexander Müller
我已经编辑了我的帖子,展示了通常在没有多线程的情况下如何完成。 - Kos
1
我发布的代码不就是这种情况吗?如果渲染需要很长时间,那么在下一帧中,您仍将拥有与实际所需相同数量的物理模拟迭代次数,同时避免了同步的额外成本。还要注意,OpenGL调用实际上是异步的,并且除非您调用glFinish(),否则不会阻塞。 - Kos
1
我注意到的最明显的问题是在你的deltaTime计算中 - this->previousTicks 应该在计算deltaTime之后立即设置为新值,而你在它们之间进行了许多计算,因此“省略”了很多时间。请看:http://pastebin.com/fmSvuAFT - Kos
显示剩余5条评论

1
在主线程中:锁定互斥量,将包含所需信息的结构/对象添加到某种FIFO数据结构中,解锁互斥量,然后(可选)通过信号、条件变量或向套接字写入一个字节等方式唤醒后台线程。
在后台线程中:(可选)阻塞直到被主线程唤醒,然后锁定互斥量,从FIFO头部弹出第一个项目,解锁互斥量,处理该项目,重复执行。

1
关键区域和互斥锁是有问题的。它们只应该由库设计者使用,通常甚至不用(因为对于可重用的代码,额外的努力获得无锁的额外扩展性是值得的)。
相反,您应该使用线程安全队列。 Windows 提供了很多选择:
- 线程消息队列(`PostMessage`) - 邮槽 - 消息模式管道 - 数据报套接字 - SList API
这些都是高度优化的,并且比设计自己的队列容易得多。
以上就是您的一些选择。

2
我认为你的第一段有点疯狂。也许对于最高级别的代码来说并不是,当然对于未经训练的开发人员来说也不是,但是对于许多类型的开发来说,互斥锁是生活中的事实。 - asveikau
"临界区和互斥锁是不好的。" 我非常不同意。 - John Dibling
@up - 这种方法(听起来可能有点滑稽)与一种名为“无锁”(lock free)的 MT 范式有关,我想。 - Kos
1
@John:这取决于你将它们与什么进行比较。它们确实很糟糕。只是有时候,没有非糟糕的替代方案存在。;) 它们是一种糟糕的实现同步的方式。不可组合且容易出错,有什么不喜欢的呢?;) 我认为@Ben的意思并不是说尝试在没有同步的情况下实现并发是更好的选择。 - jalf
这可能是一个阴谋,通过告诉计算机科学专业的学生可以在外部有效地添加并发性来保持老的并行程序员获得高薪。但事实上不行。并发数据结构的操作集与顺序执行模型中使用的操作集是不同的。结果是asveikau认为因为很多人使用它(“这是生活的一部分”),所以这一定是正确的做法。 - Ben Voigt
显示剩余5条评论

1

我不建议再使用GLUT了 - 它非常过时且非常受限制。但是如果你一定要使用它,你可能需要研究一下glutIdleFunc。当GLUT处于空闲状态时,它将持续调用此回调函数 - 您可以使用此函数在主线程中执行后台处理。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接