mardi 14 juillet 2020

Architecture decision: return std::future or provide a callback?

I am designing an API interface for Arinc429 devices. Various suppliers provide different features within devices, so I decided to have a set of common and expected methods that must be implemented for every entity. In particular, I have a question regarding the output channel. It provides a method for a single output of a byte array, and the output itself is asynchronous after the method is called. One of the devices implements interrupts which can be caught via OS utilities (WaitForSingleObject in Windows).

So in this particular case I have an object which implements IArinc429Device, it:

  • implements method CaptureInterrupt that invokes a particular channel's callback when interrupt occurs;
  • holds a thread that runs CaptureInterrupt;
void ArincPCI429_3::CaptureInterrupt()
{
    HANDLE hEvent = CreateEvent(nullptr, TRUE, FALSE, "PCI429_3Interrupt");
    // register event handle within device via DeviceIoControl

    while (true)
    {
        DWORD waitRes = WaitForSingleObject(hEvent, INFINITE);
        ResetEvent(hEvent);

        // get output channel index which has generated an interrupt
        size_t channelIndex = ...;

        // private implementation holds pointers to output channels
        pImpl_->GetChannelOut(channelIndex)->InvokeInterrupCallback();
    }
}


But another device does not implement interrupts, so I have to "busy-wait" (sleep for the calculated expected time, then loop sleeping for small periods of time to adjust possible inaccuracies).

An object that implements interface IArinc429ChannelOutput:

  • implements method SingleOutput, that initiates asynchronous output;
  • implements method WaitOutputFinished, that waits until channel is running, then modifies its state to stopped;
  • holds a thread that runs WaitOutputFinished;

void ArincECE206_1ChannelOutput::WaitOutputFinished(size_t words)
{
    // calculate expected period of time to sleep, using amount of words to transfer and channel output speed
    std::chrono::microseconds timeToSleep = ...;

    // 99% is enough
    std::this_thread::sleep_for(timeToSleep);

    // if channel is still running, wait for 1 more word.
    timeToSleep = Arinc429Values::TimeToTransfer(
            refImpl_.outputFreqs[ChannelIndex()],
            1);

    while(IsRunning())
    {
        std::this_thread::sleep_for(timeToSleep);
        ++additionalWords;
    }

    mode_ = Arinc429OutMode::stopped;

    if (callbackOnFinishedOutput_)
        callbackOnFinishedOutput_();
}

Here is part of an API for output channel

struct ARINC429_API IArinc429ChannelOutput
{
    // 0 based index
    virtual size_t ChannelIndex() const = 0;
    virtual Arinc429OutMode OutputMode() const = 0;
    virtual Arinc429Freq Frequency() const = 0;
    virtual size_t BufferSize() const = 0;

    virtual void SetFinishOutputCallback(std::function<void()>&& fCallBack) = 0;

    // elements exceeding BufferSize are ignored
    // TODO: return future?
    virtual bool SingleOutput(array_view<const uint32_t> wordArray) = 0;

    virtual void StopOutput() = 0;

    virtual ~IArinc429ChannelOutput() = default;
};

Given the asynchronous nature of output, I think it would be convenient to return std::future from SingleOutput. And I see no problem in doing so for the second type of an Arinc429 device, since separate channel objects own their own separate waiting threads.

I chose to add a callback on finished output from the beginning since interrupts are implemented for the first device. Also callbacks are convenient to emit Qt signals from.

But std::future is more handy for synchronization and can be used to wait for an output to finish. Though it is also manageable using callbacks and condition variables, I do not find that approach rather convenient.

What option to choose?

a. Define routine that registers and uses callbacks.

b. Define std::future as a return type for SingleOutput.

c. Define both. Is this reasonable or even possible? It implies calling std::promise<R>::set_value and than calling.


Another question is about implementation.

I don't see a clear and simple way to implement returning std::future in case of a device that implements interrupts, since there is a common interrupt event and capturing thread for all the channels.

How to provide futures for multiple output channel objects which all reside in different threads? See ArincPCI429_3::CaptureInterrupt()

Aucun commentaire:

Enregistrer un commentaire