7 min read

OpenThreads basics

OpenThreads is a lightweight, cross-platform thread API for OSG classes and applications. It supports the fundamental elements required by a multithreaded program, that is, the thread object (OpenThreads::Thread), the mutex for locking data that may be shared by different threads (OpenThreads::Mutex), barrier (OpenThreads::Barrier), and condition (OpenThreads::Condition). The latter two are often used for thread synchronization.

To create a new thread for certain purposes, we have to derive the OpenThreads::Thread base class and re-implement some of its virtual methods. There are also some global functions for conveniently handling threads and thread attributes, for example:

  • The GetNumberOfProcessors() function gets the number of processors available for use.
  • The SetProcessorAffinityOfCurrentThread() function sets the processor affinity (that is, which processor is used to execute this thread) of the current thread. It should be called when the thread is currently running.
  • The CurrentThread() static method of OpenThreads::Thread returns a pointer to the current running thread instance.
  • The YieldCurrentThread() static method of OpenThreads::Thread yields the current thread and lets other threads take over the control of the processor.
  • The microSleep() static method of OpenThreads::Thread makes the current thread sleep for a specified number of microseconds. It can be used in single-threaded applications, too.

Time for action – using a separate data receiver thread

In this example, we will design a new thread with the OpenThreads library and use it to read characters from the standard input. At the same time, the main process, that is, the OSG viewer and rendering backend will try retrieving the input characters and displaying them on the screen with the osgText library. The entire program can only quit normally when the data thread and main process are both completed.

  1. Include the necessary headers:

    #include
    #include
    #include
    #include
    #include


    
    
  2. Declare our new DataReceiverThread class as being derived from OpenThreads::Thread. Two virtual methods should be implemented to ensure that the thread can work properly: the cancel() method defines the cancelling process of the thread, and the run() method defines what action happens from the beginning to the end of the thread. We also define a mutex variable for interprocess synchronization, and make use of the singleton pattern for convenience:

    class DataReceiverThread : public OpenThreads::Thread
    {
    public:
    static DataReceiverThread* instance()
    {
    static DataReceiverThread s_thread;
    return &s_thread;
    }
    virtual int cancel();
    virtual void run();

    void addToContent( int ch );
    bool getContent( std::string& str );
    protected:
    OpenThreads::Mutex _mutex;
    std::string _content;
    bool _done;
    bool _dirty;
    };

    
    
  3. The cancelling work is simple: set the variable _done (which is checked repeatedly during the run() implementation to true) and wait until the thread finishes:

    int DataReceiverThread::cancel()
    {
    _done = true;
    while( isRunning() ) YieldCurrentThread();
    return 0;
    }

    
    
  4. The run() method is the core of a thread class. It usually includes a loop in which actual actions are executed all the time. In our data receiver thread, we use std::cin.get() to read characters from the keyboard input and decide if it can be added to the member string _content. When _done is set to true, the run() method will meet the end of its lifetime, and so does the whole thread:

    void DataReceiverThread::run()
    {
    _done = false;
    _dirty = true;
    do
    {
    YieldCurrentThread();

    int ch = 0;
    std::cin.get(ch);
    switch (ch)
    {
    case 0: break; // We don’t want ” to be added
    case 9: _done = true; break; // ASCII code of Tab = 9
    default: addToContent(ch); break;
    }
    } while( !_done );
    }

    
    
  5. Be careful of the std::cin.get() function: it firstly reads one or more characters from the user input, until the Enter key is pressed and a ‘n‘ is received. Then it picks characters one by one from the buffer, and continues to add them to the member string. When all characters in the buffer are traversed, it clears the buffer and waits for user input again.
  6. The customized addToContent() method adds a new character to _content. This method is sure to be called in the data receiver thread, so we have to lock the mutex object while changing the _content variable, to prevent other threads and the main process from dirtying it:

    void DataReceiverThread::addToContent( int ch )
    {
    OpenThreads::ScopedLock<:mutex> lock(_mutex);
    _content += ch;
    _dirty = true;
    }

    
    
  7. The customized getContent() method is used to obtain the _content variable and add it to the input string argument. This method, the opposite of the previous addToContent() method, must only be called by the following OSG callback implementation. The scoped locking operation of the mutex object will make the entire work thread-safe, as is done in addToContent():

    bool getContent( std::string& str )
    {
    OpenThreads::ScopedLock<:mutex> lock(_mutex);
    if ( _dirty )
    {
    str += _content;
    _dirty = false;
    return true;
    }
    return false;
    }

    
    
  8. The thread implementation is finished. Now let’s go back to rendering. What we want here is a text object that can dynamically change its content according to the string data received from the main process. An update callback of the text object is necessary to realize such functionality. In the virtual update() method of the customized update callback (it is for drawables, so osg::NodeCallback is not needed here), we simply retrieve the osgText::Text object and the receiver thread instance, and then reset the displayed texts:
    class UpdateTextCallback : public osg::Drawable::UpdateCallback
    {
    public:
      virtual void update( osg::NodeVisitor* nv,
                  osg::Drawable* drawable )
      {
        osgText::Text* text =
             static_cast<:text>(drawable);
        if ( text )
        {
          std::string str("# ");
          if ( DataReceiverThread::instance()->getContent(str) )
               text->setText( str );
        }
      }
    };
  9. In the main entry, we first create the osgText::Text drawable and apply a new instance of our text updating callback. The setAxisAlignment() here defines the text as a billboard in the scene, and setDataVariance() ensures that the text object is “dynamic” during updating and drawing. There is also a setInitialBound() method, which accepts an osg::BoundingBox variable as the argument. It forces the definition of the minimum bounding box of the drawable and computes the initial view matrix according to it:

    osg::ref_ptr<:text> text = new osgText::Text;
    text->setFont( “fonts/arial.ttf” );
    text->setAxisAlignment( osgText::TextBase::SCREEN );
    text->setDataVariance( osg::Object::DYNAMIC );
    text->setInitialBound(
    osg::BoundingBox(osg::Vec3(), osg::Vec3(400.0f, 20.0f, 20.0f))
    );
    text->setUpdateCallback( new UpdateTextCallback );

    
    
  10. Add the text object to an osg::Geode node and turn off lighting. Before starting the viewer, we also have to make sure that the scene is rendered in a fixed-size window. That’s because we have to also use the console window for keyboard entry:

    osg::ref_ptr<:geode> geode = new osg::Geode;
    geode->addDrawable( text.get() );
    geode->getOrCreateStateSet()->setMode(
    GL_LIGHTING, osg::StateAttribute::OFF );
    osgViewer::Viewer viewer;
    viewer.setSceneData( geode.get() );
    viewer.setUpViewInWindow( 50, 50, 640, 480 );

    
    
  11. Start the data receiver thread before the viewer runs, and quit it after that:

    DataReceiverThread::instance()->startThread();
    viewer.run();
    DataReceiverThread::instance()->cancel();
    return 0;

    
    
  12. Two windows will appear if you are compiling your project with your subsystem console. Set focus to the console window and type some characters. Press Enter when you are finished, and then press Tab followed by Enter in order to quit the receiver thread:

    OpenSceneGraph

  13. You will notice that the same characters come out in the OSG rendering window. This can be treated as a very basic text editor, with the text source in a separate receiver thread, and the drawing interface implemented in the OSG scene graph:

    OpenSceneGraph

What just happened?

It is very common that applications use separate threads to load huge files from disk or from the Local Area Network (LAN). Other applications use threads to continuously receive data from the network service and client computers, or user-defined input devices including GPS and radar signals, which is of great speed and efficiency. Extra data handling threads can even specify an affinity processor to work on, and thus make use of today’s dual-core and quad-core CPUs.

The OpenThreads library provides a minimal and complete object-oriented thread interface for OSG developers, and even general C++ threading programmers. It is used by the osgViewer library to implement multithreaded scene updating, culling, and drawing, which is the secret of highly efficient rendering in OSG. Note here, that multithreaded rendering doesn’t simply mean executing OpenGL calls in different threads because the related rendering context (HGLRC under Win32) is thread-specific. One OpenGL context can only be current in one thread (using wglMakeCurrent() function). Thus, one OSG rendering window which wraps only one OpenGL context will never be activated and accept OpenGL calls synchronously in multiple threads. It requires an accurate control of the threading model to make everything work well.

LEAVE A REPLY

Please enter your comment!
Please enter your name here