mbed-ce / mbed-os

Arm Mbed OS is a platform operating system designed for the internet of things
https://mbed.com
Other
37 stars 12 forks source link

How to return values when derefer execution? #259

Open lefebvresam opened 3 months ago

lefebvresam commented 3 months ago

I have the following example code:

EventQueue queue(32 * EVENTS_EVENT_SIZE);

bool bigger10(uint8_t& value) {
    printf("bigger10()\n");
    fflush(stdout);
    return value > 10;
}

void checkValue(uint8_t& value) {
    printf("check the value %u\n", value);
    if (queue.call([&](){bigger10(value);})) {
        printf("bigger than 10\n");
    } else {
        printf("not bigger than 10\n");
    }
    fflush(stdout);
}

int main()
{
    printf("\nMbed CE test\n");
    uint8_t value = 6;
    queue.call([&](){checkValue(value);});

    // execute events on the queue in main context
    printf("dispatch forever\n");
    fflush(stdout);
    queue.dispatch_forever();
}

Result:

Mbed CE test
dispatch forever
check the value 6
bigger than 10
bigger10()

The purpose is to derefer functions that use shared resources that are not thread safe (like SPI or printf) and that are triggered from different threads (like a thead that is handling touch events on the display). If you don't use an eventqueue it will crash certainly. Because the calls can be recursive (a touch performs an interpretation of a string sequence, that in turns performs an action that is interpreting another sequence, or must use SPI to read data etc.), I want to splitup them into derefered tasks that are executed in a chain.

This approach works as long as you do not use any return values and avoid any dependencies between the different 'blobs' in order to check the succesful execution of the new task. If you do like above, you get the wrong return value (like a value of a pointer instead of true/false) with faulty interpretation or errors that are never called. Putting a function on the eventqueue is execution after the current execution and never waiting or evaluation of any return value.

Is there any way to evaluate return values or to 'nest' different calls on the event queue? Or is it just contrary the idea of 'scheduled execution' that is per default an execution blob next to each other?

lefebvresam commented 3 months ago

The strange thing is that this is also not working:

void bigger10(uint8_t& value, bool& result, bool& done) {
    printf("bigger10()\n");
    fflush(stdout);
    done = true;
    result = value > 10;
}

void checkValue(uint8_t& value) {
    printf("check the value %u\n", value);
    bool done = false;
    bool result = true;
    queue.call([&](){bigger10(value, result, done);});
    while(!done); // wait for the evaluation
    if (result) {
        printf("bigger than 10\n");
    } else {
        printf("not bigger than 10\n");
    }
    fflush(stdout);
}

int main()
{
    printf("\nMbed CE test\n");

    uint8_t value = 6;
    queue.call([&](){checkValue(value);});

    // execute events on the queue in main context
    printf("dispatch forever\n");
    fflush(stdout);
    queue.dispatch_forever();
}

How can done become on true if it is set on true when the function already passed by this point?

Mbed CE test
dispatch forever
check the value 6
bigger than 10
bigger10()
JohnK1987 commented 3 months ago

I see no point to call any function via queue.call from previous call of queue.call and this is unexpected use case I think.

The reason why this becomes true is because this line is wrong

if (queue.call([&](){bigger10(value);}))

The if will take result of queue.call what will return an int id of current event. So this will return false only with not enough memory for new event.

lefebvresam commented 3 months ago

I need to do some further investigation but sometimes it's recursive and can be quite complex:

Interpreted commands can also result in updating widgets (SPI transactions to GPU), sending commands, printing logs, etc. Because these commands can also come from another thread that handles serial input all these actions using shared resources must be executed in a thread save way. What worked until now is inserting a reference to a global eventqueue in every thread and call all 'interprete' actions in the main context. Then they are not mixed up and scheduled to be executed after each other. The disadvantage is then that you can not perform calls and wait for return values in order to log errors. You have to see them as 'independent execution blobs'.

When you limit to one derefering for each physical event (touch press, touch release, gesture, etc.), the calling tree will be so big that you get a crash. So breaking up this three works but returning errors is not possible.

So the whole question is: where do you make the breakups? In my opinion the best is to do them each time you do an interprete wich will run on his hown without returning a pass/fail value, but not in the middle of an execution tree where return values are important to evaluate.

lefebvresam commented 3 months ago

Another idea is to use mutexes and make it thread save so I don't need an eventqueue at all. Sometimes SPI transactions are dependent on each other like rendering a page is a whole procedure of copying and showing the background etc. It may not be interrupted. Or reading a data index from SPI flash is one procedure. I could make a global lock object and get the lock each time there is SPI activity.

lefebvresam commented 3 months ago

I see no point to call any function via queue.call from previous call of queue.call and this is unexpected use case I think.

The reason why this becomes true is because this line is wrong

if (queue.call(&{bigger10(value);}))

The if will take result of queue.call what will return an int id of current event. So this will return false only with not enough memory for new event.

error: expected primary-expression before '{' token

What can you do with the 'handle'? Do some evaluation afterwards?

lefebvresam commented 3 months ago

In this context another issue popped up here. When derefered execution to the main thread becomes a very deep stack with a lot of memory allocations, it can be that the main thread stack size is too low which invokes unexpected crashes. A crash during execution of something that invokes peaks on the stack like:

    char b[sizeof(findex.hmac)*3+1];
    fill_n(b, sizeof(b), '\0');
    for (uint8_t i = 0, p = 0; i < sizeof(b)-3; i+=3)
        sprintf(b+i, "%02X ", findex.hmac[p++]);

Could be solved by increasing allocation:

"rtos.main-thread-stack-size": 10240,