实现用户级线程库 - 来自 makecontext 的 return 值

Implementing a user level thread library - return value from makecontext

我看到了一些关于用户线程库的问题,但 none 似乎回答了我的问题。我能够创建线程,运行 它们,取消它们,然后退出它们。由于某种原因我不能做的是获取 return 数据的线程。

当我初始化我的线程库时,我将我的退出线程上下文设置如下:

getcontext(&threadEnd);
threadEnd.uc_stack.ss_sp = (char *)malloc(SIGSTKSZ);
threadEnd.uc_stack.ss_size = SIGSTKSZ;
makecontext(&threadEnd, (void *) thread_exit, 1, &ReturnValue);

我创建一个线程并分配如下:

thread->myContext.uc_stack.ss_sp = (char *) malloc(SIGSTKSZ);
thread->myContext.uc_stack.ss_size = SIGSTKSZ;
thread->myContext.uc_link = &threadEnd;

当函数returns和thread_exit()被调用时:

    void thread_exit(void* retval){
    int* test;
    if (retval != NULL)
    {
        test = (int*) retval;
        printf("Returned value: %i\n", *test);
        fflush(stdout);
    }

打印输出总是“返回值:0”

被调用的函数是return指向一个整数的指针。

我做错了什么?

您需要一些简单的同步来等待一个线程。工作线程设置变量测试和信号。

// Pseudo code
// worker thread
retval = &VALUE; // retval is pointer?
SignalEvent( hEvent );

和主线程:

// Pseudo code
// main thread
int* test;

hEvent = CreateEvent();

WaitOnEvent( hEvent );

if (retval != NULL)
{
    test = (int*) retval;
    printf("Returned value: %i\n", *test);
    fflush(stdout);
}

对于许多线程,您需要决定是等待它们中的任何一个,还是等待它们全部发出信号,或者您想要等到所有信号发出,然后检查值。此外,我们通常会考虑等待事件的线程是否在等待时做一些空闲循环并可能做一些其他工作(更复杂的情况)。

如果您使用 C++,我可以提供一个 C++ 11 条件变量的示例,但对于 C,它通常是 POSIX 或 Win32 API,但我们可以使用伪代码。 Win32 SetEvent and WaitForSingleObject. This POSIX topic pretty much covers condition variables 适合你。

请注意,随着线程完成,一个线程也会触发信号,因此在 Win32 中,您可以直接在线程句柄上等待,但这种方法可能不可移植。此外,线程及其同步不是 C 语言标准的一部分,因此仅适用于提供两者的某些 OS 才有意义。

如果您在 GBD makecontext 中单步执行程序,请不要保存用于创建上下文的函数的 return。

我的实验示例:(观察 rax 寄存器):

在 return 语句:

thread1 (arg=0x1) at test_create_join.c:14
14      return (void *)11;
Value returned is  = 19
(gdb) info registers
rax            0x13 19
---

在 return 之后:

(gdb) step
15  }
(gdb) info registers
rax            0xb  11

内部上下文切换:

__start_context () at ../sysdeps/unix/sysv/linux/x86_64/__start_context.S:32
32  ../sysdeps/unix/sysv/linux/x86_64/__start_context.S: No such file or directory.
(gdb) info registers
rax            0xb  11

你可以看到一些指令 return 值被保留但在几步之后它变成了 0。显然它特定于 x86_64 arch 但我认为它可能与大多数 arch 相同(即makecontext 的行为)

现在,如果您的线程函数需要 return 值,您可以采用另一种方法。只需为 运行 线程创建一个线程处理程序,然后使用该处理程序创建新的上下文。在这里你可以得到你想要 运行 作为线程的函数的 return 并将它保存在你的线程控制块结构中供以后使用。

typedef struct {
    thread_start_routine start;
    void *arg;
} entry_point;

static void thread_runner(void *args) {
    exit_critical_section();

    entry_point *entry = (entry_point *) args;

    // run the thread and get the exit code
    current->retcode = entry->start(entry->arg);
}

首先,当向 makecontext 提供参数时,最后一个应该始终是 NULL:

makecontext(&threadEnd, (void *) thread_exit, 1, &ReturnValue, NULL);

我遇到了同样的问题,我的处理方式有点不同,即我没有使用 uc_link,而是存储了 start_routine,即 funcargthread 结构并使用包装函数:一个 thread_runner 实际调用线程的函数并存储 return 值:

makecontext(&thread->ctx, (void *) thread_runner, 1, thread, NULL);

线程运行器所在的位置:

void thread_runner(thread_t *thread){
  void **retval = thread->func(thread->arg);
  if(retval != NULL){
    thread_exit(*retval);
  } else {
    thread_exit(NULL);
  }
}

我们不应该在 makecontext() 的参数列表中传递指针。它们必须是手册中指定的整数:

the function func is called, and passed the series of integer (int) arguments that follow argc; the caller must specify the number of these arguments in argc.

在某些架构上,整数与指针具有相同的大小(例如 32 位),但在其他架构上,指针是 64 位,而整数是 32 位长。在最近的 GLIBC/Linux x86_64 架构中,参数在上下文中存储为“long long”整数。因此,这可能会起作用,因为这会使参数存储为 64 位值(与指针的大小兼容),但这不可移植。因此,这可以解释为什么您没有通过“&ReturnValue”指针获得正确的值。