2

How can I implement a non-blocking library API in c, without the use of threads?

In short, I have a library that I wrote that issues some read/write calls via a serial controller in order to get data the client of the library needs. These calls to the serial device, via a proprietary driver, are blocking, and I can't change them.

Without the use of threads in my library, or writing a system service to co-exist with my library, is there any way to "wrap" my library API calls so they are non-blocking (i.e. like co-routines in python)? The desired end result is a simple, synchronous, non-blocking API call to query the status of the library with minimal wait involved.

Thank you.

Doe John
  • 21
  • 2
  • 1
    Can you give more information about the platform, and how did you write to the serial controler? – cmdLP Jan 08 '18 at 13:54
  • Would you consider the time a 300bd line needs to transfer 1MB data consider being a blocker? – alk Jan 08 '18 at 13:58
  • Do you have the option of moving the blocking code into a separate process? (Though I suppose that might fall under "writing a system service" in this case) – Hasturkun Jan 08 '18 at 14:02
  • You must read into software interrupts or timer functions. You can look for `setcontext`, which allows to run code, while the output keeps the same value for a given frequency. – cmdLP Jan 08 '18 at 14:02
  • 4
    Is the serial line available through a standard file descriptor useable throuth `read` and `write` calls. If yes, maybe you could use select to see whether the next call will block or not. – Serge Ballesta Jan 08 '18 at 14:03
  • @cmdLP see the linux tag? apart from that, how do *more* blocking calls and/or some periodic function scheduling help with the question asked here? –  Jan 08 '18 at 14:04
  • @SergeBallesta good catch, might be the best solution **if** applicable. –  Jan 08 '18 at 14:05
  • There also still is [AIO](http://man7.org/linux/man-pages/man7/aio.7.html). If "wrapped" it could be made looking synchronously. – alk Jan 08 '18 at 14:13
  • @DoeJohn, if you feel any of the answers given provide the information you needed please consider clicking the check mark next to the appropriate one (not necessarily my one) to show this question as answered. If no answer provides the info you were looking for, please clarify further. Thanks! – Toby Feb 23 '18 at 11:00

3 Answers3

4

Short answer: no.

You cannot change the delay as it is inherent to the operation requested (sending data takes a certain amount of time) so you cannot make the call shorter.

Therefore your choice is either to wait for a call to complete or not wait for it.

You cannot skip waiting for it without some sort of threading as this is how the processor exposes the abstraction of it doing two things at once (i.e. sending data on the serial port and continuing on with more code)... thus you will need threads to achieve this.

Toby
  • 9,696
  • 16
  • 68
  • 132
  • Does `c` not have co-routines? – Doe John Jan 08 '18 at 13:54
  • 2
    No, it has not (portably). But see [this answer](https://stackoverflow.com/a/48141185/841108) – Basile Starynkevitch Jan 08 '18 at 13:55
  • 3
    co-routines and similar async stuff is nowadays typically implemented *on top of* threads. I guess python does the same thing. You'd use some thread pool for this ... –  Jan 08 '18 at 13:55
  • 1
    If your referring to Knuth's ASM concept - it doesn't really translate to C with any kind of efficiency you'd want.... I guess you could call to send one byte, do some more work until that byte is sent, then start another byte sending.... but that would be horribly complicated for anything past a very basic system (unless you have interrupts maybe - but then I guess you wouldn't be asking). If you mean as in higher level languages, again, no. https://en.wikipedia.org/wiki/Coroutine#Implementations_for_C – Toby Jan 08 '18 at 13:57
  • 1
    @DoeJohn Also note that a blocking syscall (which you seem to use) is implemented by the kernel by blocking the *calling thread* until there's a result to return, so there's obviously no way around another thread on the lowest level talking to the kernel (if you want to keep doing something). –  Jan 08 '18 at 13:58
  • 1
    Re coroutines, see also: https://www.chiark.greenend.org.uk/~sgtatham/coroutines.html – Toby Jan 08 '18 at 14:03
1

I think first of all you should change you lib to make all calls non-blocking. Here is good explanation Linux Blocking vs. non Blocking Serial Read

Most close technique to python's co-routines in C is Protothread. It implements simple cooperative multitasking without using threads

Daniel
  • 78
  • 4
  • 3
    OP specifically mentions that the _driver_ provides calls that are blocking and cannot be changed – Toby Jan 08 '18 at 14:02
  • 2
    If the "proprietary driver" only offers blocking calls, like suggested in the question, there's no way to "fix" this in the library... –  Jan 08 '18 at 14:02
  • "*It implements simple cooperative multitasking without using threads*" <-- so what happens on a blocking syscall? ;) –  Jan 08 '18 at 14:07
  • I agree. When I start write the answer there was no comment in bold that lib cannot be changed. If someone blocks thread there is no way to 'unblock' it. May be just play with timeouts but seems its a really ugly hack – Daniel Jan 08 '18 at 14:12
0

Its probably better to do this with threads in general. However, there is a way that will work with limitations for simple applications.

Please forgive the use of a C++11 lambda, I think it makes things a little clearer here.

namespace 
{
   sigjmp_buf context;
} // namespace

void nonBlockingCall(int timeoutInSeconds)
{
    struct sigaction* oldAction = nullptr;
    struct sigaction newAction;
    if (sigsetjmp(::context,0) == 0)
    {
        // install a simple lambda as signal handler for the alarm that
        // effectively makes the call time out.
        // (e.g. if the call gets stuck inside something like poll() )
        newAction.sa_handler = [] (int) {
           siglongjmp(::context,1);
        };
        sigaction(SIGALRM,&newAction,oldAction);
        alarm(timeoutInSeconds); //timeout by raising SIGALM
        BLOCKING_LIBRARY_CALL
        alarm(0); //cancel alarm
        //call did not time out
    }
    else
    {
        // timer expired during your call (SIGALM was raised)
    }
    sigaction(SIGALRM,oldAction,nullptr);
}

Limitations:

  • This is unsafe for multi-threaded code. If you have multi-threaded code it is better to have the timer in a monitoring thread and then kill the blocked thread.

  • timers and signals from elsewhere could interfere.

  • Unless BLOCKING_LIBRARY_CALL documents its behaviour very well you may be in undefined behaviour land.

    • It will not free resources properly if interrupted.
    • It might install signal handlers or masks or raise signals itself.
  • If using this idiom in C++ rather than C you must not allow any objects to be constructed or destroyed between setjmp and longjmp.

Others may find additional issues with this idiom.

I thought I'd seen this in Steven's somewhere, and indeed it is discussed in the signals chapter where it discusses using alarm() to implement sleep().

Bruce Adams
  • 4,953
  • 4
  • 48
  • 111