简体   繁体   中英

CPP: Is this a reasonable way to go about thread safe FIFIO message queues

Please be gentle - first time I've posted here. Currently teaching myself C++. Wanted to play with threads and passing messages of any type between them. Not got to the thread part yet - just working on the messaging queue element. Below is the code I've put together that works - at least in a single main() thread. Before I start creating custom data objects and thread functions - I wanted to get a consensus if what I have done so far is viable (if not not fully aligned with convention/standards or wholly efficient) - so really just looking for direction if this is going to create unnecessary complexities later down the line with threads.

So two code files:

messageQueue.h which includes the implementation as well as header as couldn't get the linker to work with the implementation being in a separate cpp file.

#pragma once
#include<queue>         // for queue
#include<thread>        // for smart pointers
#include<mutex>         // for locking of the queue when doing push(), front() and pop()



/*
Purpose:
To provide a thread safe method of of passing messages between threads in a FIFO queue manner.
Currently this uses unique_ptr which enables a one to one publisher/consumer application, if
we need one to many publisher/consumer model then will need to investigate maintaining a collection 
of subscribers (consumers) and use shared_ptr.
Usage notes:
Message objects need to be created using make_unique not using new - e.g.
    auto messagePtr = std::make_unique<message_t<std::string>>("this is a message");
Call this way: 
    messageQueue<objectType> messageQueueName;
    messageQueueName.publish(std::move(messagePtr));
    messagePtr = messageQueueName.consume();
*/

template<class T>
class message_t
{
public:
    message_t(T message) : m_message{ message } {}

private:
    T m_message;
};

template<class T>
class messageQueue
{
public:
    messageQueue() {};
    void publish(std::unique_ptr<message_t<T>> messagePtr);
    std::unique_ptr<message_t<T>> consume();
    bool hasData();
private:
    std::queue < std::unique_ptr<message_t<T>>> m_queue;
    std::mutex m_mutex;

};

// Had to add the implementation into the header file as templated classes have link issues when in separate cpp files :-(

template<class T>
void messageQueue<T>::publish(std::unique_ptr<message_t<T>> messagePtr) {
    std::lock_guard<std::mutex> lock(m_mutex); 
    m_queue.push(std::move(messagePtr));
};

template<class T>
std::unique_ptr<message_t<T>> messageQueue<T>::consume() {
    std::unique_ptr<message_t<T>> retVal;
    {
        std::lock_guard<std::mutex> lock(m_mutex);
        if (!m_queue.empty()) { retVal = std::move(m_queue.front()); }
        m_queue.pop();
    }

    return retVal;
};

template<class T>
bool messageQueue<T>::hasData() {
    std::lock_guard<std::mutex> lock(m_mutex);
    return(!m_queue.empty());
};

main.cpp - just has a very simple test case in it.

#include "messageQueue.h"
#include<string>
#include<iostream>


int main(void) {
    messageQueue<std::string> textQueue;                        // Create the message queue 
    auto messagePtr = std::make_unique<message_t<std::string>>("this is a message");    // Create a message and get its unique pointer
    std::cout << "messagePtr= " << messagePtr << std::endl;     // output the pointer value
    textQueue.publish(std::move(messagePtr));                   // Push the message pointer onto the queue : messagePtr is now 0
    auto newMessagePtr = std::make_unique<message_t<std::string>>("this is another message");   // Create a second message and get its unique pointer
    std::cout << "newMessagePtr= " << newMessagePtr << std::endl;   // output the new message pointer value
    std::cout << "Consuming messagePtr into newMessagePtr" << std::endl;    //comment
    if (textQueue.hasData()) { newMessagePtr = textQueue.consume(); }   // Pull the original pointer off the queue and assign to new message pointer
    std::cout << "newMessagePtr= " << newMessagePtr << std::endl;   // output the pointer in the new message pointer - should be first pointer value


    return(0);
}

One issue I see is that if message queue is empty, you still pop from it.

And one more conceptual issue: in a multithreaded environemnt hasData() is never meaningful. Whatever it returns it may immediately be invalidated by another thread. So no code can really depend on the return value, unless you accept the uncertainty of its correctness.

More common approach is to simply try_consume the value unconditionally (and return nullptr if there is no message) - which you basically already implemented as consume .

And another approach is to have a blocking consume , which halts current thread until some other thread writes into the queue. This however requires using signaling, eg through std::condition_variable or atomics. This has its pitfalls, but if you are learning it is worth spending the time to understand how it works.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM