higan-emu / libco

libco is a cooperative multithreading library written in C89.
Other
125 stars 25 forks source link

How to finish a program? #27

Closed hayyp closed 3 years ago

hayyp commented 3 years ago

Sorry for this dummy question. According to the usage document, a coentry() function must not return and should instead co_switch() to another cothread before it ends. But since it's now like an infinite loop of thread jumping, how should we "exit" such a program properly then (calling exit() works though, I am just not sure if it's also an undefined behavior). Just for example, my code is like

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#include "libco.h"

void cothr_a_job();
void main_cothr_job();

cothread_t main_cothr;

int main()
{
    if ((main_cothr = co_create(1024 * sizeof(void *), main_cothr_job))) {
        printf("main cothread started\n");
    } else {
        fprintf(stderr, "failed to create main cothread\n");
    }

    co_switch(main_cothr);

    return 0; // will never reach here
}

void main_cothr_job()
{
    printf("entering main cothread!\n");

    cothread_t a;
    if ((a = co_create(1024 * sizeof(void *), cothr_a_job))) {
        printf("thread A started\n");
    } else {
        fprintf(stderr, "failed to create thread A\n");
    }

    co_switch(a);
    printf("back to main cothread\n");
    co_delete(a);
    printf("thread A deleted\n");
    printf("leaving the program\n");
    exit(0); // is it the correct way to end such a program?
}

void cothr_a_job()
{
    printf("Hi from cothread A\n");
    co_switch(main_cothr);
    sleep(200);
}
namandixit commented 3 years ago

If you call co_active() from the main function, you will get a handle to a pseudo-coroutine which represents the CPU context of the default thread of execution. If you co_switch() to this handle from another coroutine, you will end up in the main function again, from where you can return from the program (or do whatever else you like).

The handle to the main pseudo-coroutine can be stored in a thread-local/global variable, so that other coroutines can also access it.

namandixit commented 3 years ago

Also, try not to make libc/system calls from the coroutines (like exit), since most OS don't expect you to be switching stacks at runtime.

hayyp commented 3 years ago

Thank you for you reply, that makes sense to me now, but maybe we should have added the pseudo-coroutine part to the usage document, too? As for system calls, I think it's a bit hard to avoid them if you are doing anything like servers or emulators?

namandixit commented 3 years ago

The docs were done by @Kawa-oneechan and @Screwtapello, so I'll leave that decision to them.

Regarding system calls, just to be safe, you should always make them from a default stack (whether that of the main thread, or of any threads started by threading libraries like pthread). To achieve this, you'll probably need to implement a Task system (aka Job system). I have implemented one using libco here which you can study for reference, but there is yet no documentation, very little testing, probably more complexity than you need, and it only works on Linux.

More resources to consult:

https://www.gdcvault.com/play/1022186/Parallelizing-the-Naughty-Dog-Engine

https://ourmachinery.com/post/fiber-based-job-system/

hayyp commented 3 years ago

Thank you so much for your help, and I think I can close this issue now. Happy new year!

Screwtapello commented 3 years ago

Sorry it's taken a while to get around to responding here.

Also, try not to make libc/system calls from the coroutines (like exit), since most OS don't expect you to be switching stacks at runtime.

I'd be mildly surprised if making system calls was an issue; I don't think they care much about the stack pointer. Calling C library functions, especially third-party libraries, might be an issue depending on how much stack-space they require, and how much stack space you've allocated. I think the Linux default stack allocation is like 8MB, so if you only give each co-routine a 4KB stack (for example), library functions that blindly assume they'll always have "enough" stack might get a nasty surprise.

The other thing I can think of is if you're using C++ RAII or some other scheme that expects stack-allocated objects to have a deallocation function automatically called, but the objects are on the stack in some other co-routine, you'll need some way to ensure they get properly cleaned up.

...maybe we should have added the pseudo-coroutine part to the usage document, too?

That's a great idea, a PR would be very welcome.

Alcaro commented 3 years ago

I wouldn't be too surprised if Windows syscalls check the stack pointer, and if wrong, deem the process hacked and terminate it. (I would, however, be quite surprised if Linux syscalls do that.)

And Windows is quite eager to dig through the stack - it's done every time it sees a segfault, div by zero, or other exception, whether there's a handler or not. Not common in C programs, but throwing a C++ exception under MSVC is, to my knowledge, a complete stack walk, even if caught properly. (MinGW implements exceptions differently, I think it's safe.) The fiber backend is safe, but also slow.

I have also seen a few people write libretro frontends in C# and have trouble with bsnes. I think the C# garbage collector occasionally walks the stack. (Not sure if using the fiber backend would solve that problem.)

hayyp commented 3 years ago

That's a great idea, a PR would be very welcome.

Just opened a PR. If you don't mind though, I would like to add an example file along with it.