setjmp и longjump для реализации потоков

У меня был вопрос об использовании setjmp и longjump для создания стеков функций, которые могут работать независимо друг от друга. Со ссылкой на этот вопрос

Здесь стек функций для B(), кажется, находится поверх стека для A, поэтому, когда A выходит за рамки, и я пытаюсь перейти к B(), код segfaults. Измененный код выглядит так

#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) возвращается, а затем, в свою очередь, переходит к одной из сопрограмм, если они живы. Будет ли это работать или это также приведет к segfault, потому что некоторые из стеков для сопрограмм, которые, возможно, мертвы, находятся поверх тех, которые хотят работать?

Если кажется, что он должен segfault, то как можно реализовать такие независимые сопрограммы (которые могут работать, когда другие умерли и не зависят друг от друга) на C++?

ПРИМЕЧАНИЕ. Я не хочу использовать swapcontext, makecontext, setcontext и getcontext, поскольку они устарели. Цель этого поста — помочь мне найти альтернативы этим функциям.

Заранее спасибо!


person Curious    schedule 29.10.2015    source источник
comment
вы не можете longjmp() войти в функцию, находящуюся глубже в стеке вызовов, чем выполняемая в данный момент функция.   -  person The Paramagnetic Croissant    schedule 29.10.2015
comment
По крайней мере, для С++ (и я считаю, что C тоже) указанный ответ просто неверен. Вы вызываете неопределенное поведение.   -  person MikeMB    schedule 29.10.2015
comment
Можно реализовать многопоточность в C, используя setjmp/longjmp, я помню, как делал это в университете. Если вам это действительно нужно, я покопаюсь в своем мозгу, чтобы узнать, как это сделать. Моя первая мысль заключается в том, что вам нужно выделить пространство стека и настроить jmp_buf, а также, чтобы сделать его приятным, вам нужен сигнал тревоги или что-то еще (если вы просто не хотите уступать, чтобы изменить текущий поток), который вызовет longjmp для следующего работающего потока .   -  person mattiash    schedule 29.10.2015
comment
how can one implement such independent couroutines (which can run when others have died and don't depend on each other) с помощью setjmp/longjmp нельзя. Вам нужна упреждающая многозадачность (или многопоточность), которая возможна только при использовании какого-либо привилегированного супервизора. Хотя вы можете поймать глубокие ошибки, т.е. своего рода обработку исключений C.   -  person Matt    schedule 29.10.2015
comment
@ user441802, я не уверен, что вы имеете в виду. С таким подходом, безусловно, можно реализовать многопоточность. Я предполагаю, что сопрограммы подразумевают нечто большее, чем просто многопоточность.   -  person mattiash    schedule 29.10.2015


Ответы (1)


Быстрый и грязный взлом Windows. Извините за грязное имя и грязный код.

    // 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 с указателем на функцию в качестве параметра (и аргументом NULL, который должен быть отправлен в func). Что делает функция startThread(), так это то, что она выделяет память для стека для нового потока (каждому потоку нужен свой собственный стек). После выделения мы сохраняем контекст выполняющегося потока в новый буфер контекста потока (jmp_buf) и меняем указатели стека (и все другие регистры, которые должны быть изменены) так, чтобы они указывали на только что созданный стек. Затем мы добавляем этот новый поток в список ожидающих потоков. И тогда мы возвращаемся из функции. Мы по-прежнему в основной ветке.

В основном потоке мы делаем thread_yield(), который говорит: «Хорошо, я больше не хочу бегать, пусть бежит кто-нибудь другой!». Функция thread_yield сохраняет текущий контекст и помещает основной поток в конец списка thread_list. Затем мы планируем.

Schedule() берет следующий поток из thread_list и выполняет longjmp контекст, сохраненный в потоке buf (jmp_buf). В этом случае поток будет продолжать работать в предложении else setjmp, где мы сохранили контекст.

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

Он будет работать до тех пор, пока мы не выполним thread_yield, а затем проделаем то же самое, но вместо этого возьмем основной поток и выполним longjmp в его сохраненном буфере и так далее и тому подобное…

Если кто-то хочет проявить фантазию и хочет получить временные интервалы, можно реализовать некоторый сигнал тревоги, чтобы сохранить текущий контекст потока, а затем вызвать schedule().

Есть яснее?

person mattiash    schedule 29.10.2015
comment
Извините, мне очень трудно понять это. Не могли бы вы попытаться объяснить? Или вы могли бы связать меня с вашим университетским классом, который реализовал это и имел объяснение? - person Curious; 29.10.2015
comment
Ах, я прошел этот курс 20 лет назад ... Я отредактирую свой ответ, чтобы попытаться объяснить. - person mattiash; 29.10.2015
comment
Спасибо за объяснение! Единственная часть вашего кода, которая меня смутила, — это часть, в которой вы изменяете стек в файле jmp_buf. Что там происходит? - person Curious; 29.10.2015
comment
Ну, вам нужно, чтобы каждый поток выполнялся со своим собственным стеком. Итак, вам нужен новый стек для вашего нового потока. После выделения памяти для этого вам нужно изменить пару вещей в контексте, чтобы новый поток начал использовать новый стек. На какой платформе вы находитесь? Винда, Линукс или что-то другое? - person mattiash; 29.10.2015
comment
Вот хороший пост о базовых указателях стека и т. д. -they-point" title="что такое базовый указатель и указатель стека, на что они указывают">stackoverflow.com/questions/1395591/ - person mattiash; 29.10.2015
comment
Итак, у меня был один вопрос относительно setjmp и longjmp. Когда вызывается longjmp и стек функции B находится поверх стека функции A, уничтожается ли стек для функции B? Итак, когда происходит longjmp из B в A, может ли A запустить другой поток в форме вызова setjmp и функции к другой функции? Извините, если это было запутанно. Пожалуйста, дайте мне знать, если я могу прояснить это для вас - person Curious; 29.10.2015
comment
Может быть, вам нужно уточнить это больше. Как и в исходном коде, вы возвращаетесь к позиции стека, которая больше недействительна. - person mattiash; 30.10.2015
comment
Я не анализировал ваш код, но знаете ли вы, гарантированно ли это сработает или просто сработает - в С++ много случаев, когда вызов longjmp вызывает неопределенное поведение. Особенно интересна цитата из cppref: *При замене стандартного: :longjmp с throw и setjmp с catch будут выполнять нетривиальный деструктор для любого автоматического объекта, поведение такого std::longjmp не определено. * Я еще не проверял стандарт. - person MikeMB; 02.11.2015
comment
@MikeMB, я ничего не гарантирую. Это быстрое и грязное решение, которое работает, но оно описывает необходимые шаги для создания такого решения. С таким решением я бы остановился на C, но у меня была реализация C++ списка без STL, и я просто хотел ее использовать. В C все проще естественно. Я почти уверен, что ваша цитата верна, я думаю, что видел это раньше. Но, как я уже сказал, 10-минутное решение, чтобы наметить необходимые шаги. - person mattiash; 02.11.2015