【问题标题】:setjmp and longjump to implement threadssetjmp 和 longjump 实现线程
【发布时间】:2016-01-29 05:27:24
【问题描述】:

我有一个关于使用 setjmp 和 longjump 创建可以相互独立运行的函数堆栈的问题。参考this question

这里 B() 的函数堆栈似乎在 A 的堆栈之上,所以当 A 超出范围时,我尝试长跳转到 B() 代码段错误。修改后的代码是这样的

#include <stdio.h>
#include <setjmp.h>
#include <iostream>
using namespace std;

jmp_buf bufferA, bufferB;

void routineB(); // forward declaration 

void routineA()
{
    int r ;

    printf("(A1)\n");

    r = setjmp(bufferA);
    if (r == 0) routineB();

    printf("(A2) r=%d\n",r);

    r = setjmp(bufferA);
    if (r == 0) longjmp(bufferB, 20001);

    printf("(A3) r=%d\n",r);

    r = setjmp(bufferA);
    if (r == 0) longjmp(bufferB, 20002);

    printf("(A4) r=%d\n",r);
}

void routineB()
{
    int r;

    printf("(B1)\n");

    r = setjmp(bufferB);
    if (r == 0) longjmp(bufferA, 10001);

    printf("(B2) r=%d\n", r);

    r = setjmp(bufferB);
    if (r == 0) longjmp(bufferA, 10002);

    printf("(B3) r=%d\n", r);

    r = setjmp(bufferB);
    if (r == 0) longjmp(bufferA, 10003);

    cout << "WHAT" << endl;
}


int main(int argc, char **argv) 
{
    routineA();
    longjmp(bufferB, 123123);
    return 0;
}

我在想,我们可以让每个协程(在本例中为 B 和 A)跳转回一个主函数,然后如果协程还活着,那么它们又会跳转到其中一个协程。这会起作用还是也会出现段错误,因为一些可能已死的协程堆栈位于想要运行的堆栈之上?

如果它看起来应该是段错误,那么如何在 C++ 中实现这种独立的 couroutines(当其他人死亡并且不相互依赖时可以运行)?

注意:我不想使用 swapcontext、makecontext、setcontext 和 getcontext,因为它们已被弃用。这篇文章的目的是帮助我找到这些功能的替代品

提前致谢!

【问题讨论】:

  • 您不能longjmp() 进入调用堆栈中比当前执行函数更深的函数。
  • 至少对于 c++(我也相信 C),引用的答案是错误的。您正在调用未定义的行为。
  • 可以使用 setjmp/longjmp 在 C 中实现线程,我记得在大学课堂上这样做过。如果您真的需要它,我会深入研究我的大脑以找出方法。我的第一个想法是你需要分配堆栈空间并调整 jmp_buf,并且为了让它变得更好,你需要一个警报或其他东西(除非你只是想改变正在运行的线程),这将触发一个 longjmp 到下一个正在运行的线程.
  • how can one implement such independent couroutines (which can run when others have died and don't depend on each other) 使用 setjmp/longjmp 你不能。你想要的是 preemptive 多任务(或多线程),只有使用某种特权“主管”才能实现。尽管您可以捕获“深层错误”,即某种“C 异常处理”。
  • @user441802,我不确定你的意思,用这种方法实现线程当然是可能的。我认为,协程不仅仅意味着线程。

标签: c++ multithreading coroutine setjmp


【解决方案1】:

在 Windows 上快速而肮脏的 hack。对不起,混乱的命名和混乱的代码。

    // mythreads.cpp : Defines the entry point for the console application.
//

#include <setjmp.h>
#include <stdlib.h>
#include <stdio.h>
#include <stdint.h>
#include "List.h"

#include <Windows.h>

struct thread_t 
{
  util::Node node;
  jmp_buf buf;
};

util::List thread_list;
thread_t* runningThread = NULL;

void schedule(void);

void thread_yield()
{
  if (setjmp(runningThread->buf) == 0)
  {
    thread_list.push_back(&runningThread->node);
    schedule();
  }
  else
  {
    /* EMPTY */
  }
}

void myThread1(void *args)
{
  printf("myThread1\n");
}

void startThread(void(*func)(void*), void *args)
{
  thread_t* newThread = (thread_t*)malloc(sizeof(thread_t));

  // Create new stack here! This part is Windows specific. Find the current stack
  // and copy the contents. One might do more stuff here, e.g. change the return address 
  // of this function to be a thread_exit function.
  NT_TIB* tib = (NT_TIB*)__readfsdword(0x18);
  uint8_t* stackBottom = (uint8_t*)tib->StackLimit;
  uint8_t* stackTop = (uint8_t*)tib->StackBase;

  size_t stack_size = stackTop - stackBottom;
  uint8_t *new_stack = (uint8_t*)malloc(stackTop - stackBottom);
  memcpy(new_stack, stackBottom, stack_size);

  _JUMP_BUFFER *jp_buf = (_JUMP_BUFFER*)newThread->buf;

  if (setjmp(newThread->buf) == 0)
  {
    // Modify necessary registers to point to new stack. I may have 
    // forgotten a bunch of registers here, you must do your own homework on
    // which registers to copy.
    size_t sp_offset = jp_buf->Esp - (unsigned long)stackBottom;
    jp_buf->Esp = (size_t)new_stack + sp_offset;
    size_t si_offset = jp_buf->Esi - (unsigned long)stackBottom;
    jp_buf->Esi = (size_t)new_stack + si_offset;
    size_t bp_offset = jp_buf->Ebp - (unsigned long)stackBottom;
    jp_buf->Ebp = (size_t)new_stack + bp_offset;
    thread_list.push_back(&newThread->node);
  }
  else
  {
    /* This is where the new thread will start to execute */
    func(args);
  }
}

void schedule(void)
{
  if (runningThread != NULL)
  {

  }

  if (thread_list.size() > 0)
  {
    thread_t* t = (thread_t*)thread_list.pop_front();
    runningThread = t;
    longjmp(t->buf, 1);
  }
}

void initThreading()
{
  thread_list.init();

  thread_t* mainThread = (thread_t*)malloc(sizeof(thread_t));

  if (setjmp(mainThread->buf) == 0)
  {
    thread_list.push_back(&mainThread->node);
    schedule();
  }
  else
  {
    /* This is where the main thread will start to execute again */
    printf("Main thread running!\n");
  }
}

int main()
{
  initThreading();

  startThread(myThread1, NULL);

  thread_yield();

  printf("Main thread exiting!\n");
}

此外,Microsoft 和 setjmp/longjmp 可能不是很好的匹配项。如果我附近有一个 Unix 盒子,我会在那个盒子上做。

而且我可能需要更多地研究堆栈副本以确保它正常工作。

编辑:要检查堆栈更改后要修改的寄存器,可以查阅编译器手册。

代码在 main() 中通过设置线程框架开始。它通过将您的主线程(此时唯一的线程)视为一个线程来做到这一点。因此,存储主线程的上下文并将其放入 thread_list。然后我们 schedule() 让一个线程运行。这是在 initThreading() 中完成的。由于唯一运行的线程是主线程,我们将在 main() 函数中继续。

main 函数接下来要做的就是 startThread,它带有一个函数指针作为参数(以及要发送到 func 的 arg NULL)。 startThread() 函数的作用是为新线程的堆栈分配内存(每个线程都需要自己的堆栈)。分配后,我们将正在运行的线程的上下文保存到新的线程上下文缓冲区 (jmp_buf) 中,并更改堆栈指针(以及应更改的其他寄存器),使其指向新创建的堆栈。然后我们将这个新线程添加到等待线程列表中。然后我们从函数返回。我们仍在主线程中。

在主线程中,我们执行 thread_yield(),它说“好吧,我不想再跑了,让别人跑吧!”。 thread_yield 函数存储当前上下文并将主线程放在 thread_list 的后面。然后我们安排。

Schedule() 从 thread_list 中获取下一个线程,并对保存在线程 buf (jmp_buf) 中的上下文执行 longjmp。在这种情况下,线程将继续在我们存储上下文的 setjmp 的 else 子句中运行。

  else
    {
        /* This is where the new thread will start to execute */
        func(args);
      }

它将一直运行,直到我们执行 thread_yield,然后我们将执行相同的操作,但取而代之的是使用主线程并对其保存的缓冲区执行 longjmp 等等……

如果想要花哨,想要时间片,可以实现一些警报来保存当前线程上下文,然后调用 schedule()。

有更清楚的吗?

【讨论】:

  • 抱歉,我很难理解这一点。你能试着解释一下吗?或者您能否将我链接到您的大学班级,该班级实现了这一点并给出了解释?
  • 啊,我 20 年前上过那门课……我会编辑我的答案来尝试解释。
  • 感谢您的解释!我在您的代码中唯一感到困惑的部分是您在 jmp_buf 中修改堆栈的部分。那里发生了什么?
  • 嗯,您需要每个线程都使用自己的堆栈执行。因此,您的新线程需要一个新堆栈。在为此分配内存之后,您需要在上下文中更改一些内容,以便新线程开始使用新堆栈。你在哪个平台? Windows、Linux 还是其他?
  • 我还没有分析你的代码,但是你知道这是否可以保证工作,或者它是否恰好工作 - 在 c++ 中有很多情况下调用 longjmp 会调用未定义的行为。特别有趣的是来自cppref 的引用:*“如果用 throw 替换 std::longjmp 和用 catch 替换 setjmp 将对任何自动对象执行一个非平凡的析构函数,那么这种 std::longjmp 的行为是未定义的。*”不过,我还没有检查过标准。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-09-05
  • 1970-01-01
  • 2016-11-09
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多