Как правильно делать прерываемые подпрограммы
От: GhostCoders Россия  
Дата: 13.02.18 11:47
Оценка:
а также подпрограммы с показом текущео прогресса в %?

Есть некая подпрограмма, которая делает что-то полезное и долго.

Как ее безопасно отменить выполнение этой подпрограммы?

псевдо-код:
void process_school(School& school)
{
    for(class: school.classes())
    {
        process_class(class);
    }
}
void process_class(Class& class_)
{
    for(student: class_)
    {
        process_student(student);
    }
}
void process_stundent(Student& student)
{
    // ...
}


Скажем, в школе 1000 классов, и в кажом до 50 студентов. Время на обрабоку каждого студента — 10 сек.
Вообщем, функция (или метод) process_school() будет работать долго.

Клиент запускает эту подпрограмму в отдельном потоке.
Как безопасно реализовать прерываемость это подпрограммы?
Какие существуют общие подходы для этого?

Пока это реализовано при помощи класса хелпера, при помощи коллбэка (превдокод, вопрос в комментариях):
struct ICallback
{
    virtual bool ReportProgress(int percent) = 0; // На вход принимает текущий прогресс в %, на выходе true - продолжать процесс, false - завершить
};

ICallback* g_callback;

class progress_helper
{
    int m_expected_steps; // Ожидаемое число шагов в текущей подзадачи
    int m_current_step; // Текущий шаг в текущей подзадачи
public:
    int calculate_current_total_progress(); // Некий метод, который вычисляет и возвращает текущий прогресс (число от 0 до 100%)
    progress_helper(int expected_steps) : m_expected_steps(expected_steps), m_current_step(0)
    {
        if (!g_callback.ReportProgress(calculate_current_total_progress()))
        {
            throw std::runtime_error("required job abort"); // Или использовать свой специальный тип JobAbort
        }
    }
    void step()
    {
        ++m_current_step;
        update_current_progress(); // Некий метод, который обновляет текущий прогресс (число от 0 до 100%)
        if (!g_callback.ReportProgress(calculate_current_total_progress()))
        {
            throw std::runtime_error("required job abort"); // Или использовать свой специальный тип JobAbort
        }
    }
};

void process_school(School& school)
{
    progress_helper school_progress(school.get_class_count());
    for(class: school.classes())
    {
        process_class(class);
        school_progress.step();
    }
}
void process_class(Class& class_)
{
    progress_helper class_progress(school.get_student_count());
    for(student: class_)
    {
        process_student(student);
        class_progress.step();
    }
}
void process_stundent(Student& student)
{
    progress_helper student_progress(3);
    process_name(student.name);
    student_progress.step();
    process_surname(student.surname);
    student_progress.step();
    process_sex(student.sex); // :)
    student_progress.step();
}
// На самом деле создание классов progress_helper на стеке может
// каскадироваться, то есть, например, если в школе 10 классов,
// то progress_helper (в методе process_school) будет "тикать" 10 шагов,
// каждому шагу будет соответствовать 10% прогресса
// В функции process_class при создании progress_helper он уже будет
// "тикать" в диапозоне 10%, то есть первый раз от 0% до 10%, второй раз от 10% до 20% и так далее.
// Текущий диапозон прогресса дробится на число ожидаемых шагов, в хелперы вложенных функций уже работают 
// с текущим диапозоном "програсса" и делят уже его.
// Но то, как вычисляется прогрессс - это не суть важно.
// Важно то, что при вызове конструктора или метода step() progress_helper при помощи коллбэка (тоже не важно какого- указатель на виртуальный интерфейс, или лямбда)
// если в коллбэке решили (то есть на вызывающей стороне) - что хватит, пора закруглятся и вернули false,
// то выбрасывается некое исключение, и текущий поток заверщается естественным обраов (с перехватом ожидаемого исключения JobAbort).
// Не нужно в таком случае "убивать" поток жекстко при помощи средств ОС.
// Минус этого подхода в том, что нужно расставлять локальные переменные типа progress_helper на стеке каждой функции.
// Какие уже есть опробированные решения для этого?
Третий Рим должен пасть!
Re: Кооперативная отмена
От: Qbit86 Кипр
Дата: 13.02.18 13:03
Оценка: 2 (1) +2
Здравствуйте, GhostCoders, Вы писали:

GC>Как ее безопасно отменить выполнение этой подпрограммы?


Погугли по запросу «cooperative cancellation». В .NET используется такой подход. Есть CancellationToken — он передаётся в долгоиграющие методы, и есть его владелец CancellationTokenSource, он остаётся на вызывающей стороне, и может влиять на токен. Вызываемый метод периодически опрашивает `cancellationToken.IsCancellationRequestd` (в твоём примере внутри вложенного цикла, скажем). Если да, то аккуратно завершает выполнение. Вызывающая сторона может инициировать отмену: `cancellationTokenSource.Cancel()`, и дожидается, пока вызываемый метод своё выполнение свернёт и этот запрос удовлетворит.
Глаза у меня добрые, но рубашка — смирительная!
Re[2]: Кооперативная отмена
От: Qbit86 Кипр
Дата: 13.02.18 13:07
Оценка:
Но предложенный подход (с ручным шаганием по циклам и сохранением состояния обхода между вызовами шага) тоже можно, и обычно даже предпочтительнее.
Глаза у меня добрые, но рубашка — смирительная!
Re: Как правильно делать прерываемые подпрограммы
От: kov_serg Россия  
Дата: 13.02.18 13:12
Оценка: 4 (1)
Здравствуйте, GhostCoders, Вы писали:

GC>а также подпрограммы с показом текущео прогресса в %?


GC>Есть некая подпрограмма, которая делает что-то полезное и долго.

Думаю что следует изменть подход к задаче.
Следует составлять план задачи которая будет выполняться. Тогда и время проще оценивать, можно будет остановить и возобновить обработку.
Еще вместо процентов лучше использовать долю выполнения от 0 до 1.

  оценка времени
#include <math.h>

struct Disp {
    void reset() { m=d=0; n=0; }
    void add(double x) { n+=1; if (n>1) d=d*(n-2)/(n-1)+(x-m)*(x-m)/n; m+=(x-m)/n; }
    double getN() const { return n; } // кол-во            n
    double getM() const { return m; } // среднее           sum(x[i])/n
    double getD() const { return d; } // оценка дисперсии  sum((x[i]-mean)^2)/(n-1)
    Disp() { reset(); }
private:
    double n,m,d;
};

// оценка времени оставшегося до v=1 от момента t
// LinearEstimator le;
// le.add(0,0.0);
// le.add(1,0.1);
// le.add(2,0.2);
// le.add(3,0.3);
// double remains=le.estimate_timeleft(7); // remains = 3
struct LinearEstimator {
    void reset();
    void add(double t,double v);
    double estimate_timeleft(double t,double *err=0,double v=1.0); // -1 if no assumption
    LinearEstimator() { reset(); }
protected:
    double A00,A01,A11,b0,b1,b2,lt,lv;
    double SS,c1,c0; Disp disp;
    void update(double t,double v);
};

//-----------------------------------------------------------------------------

void LinearEstimator::reset() {
    A00=A01=A11=b0=b1=b2=lt=lv=0;
    SS=c1=c0=0; disp.reset();
}
void LinearEstimator::add(double t,double v) {
    A00+=1; A01+=t; A11+=t*t;
    b0+=v;  b1+=v*t; b2+=v*v;
    lt=t; lv=v; update(t,v);
}
void LinearEstimator::update(double t,double v) {
    if (A00<=1) return;
    double d=A00*A11-A01*A01; //if (fabs(d)<EPS) return;
    double a0=(b0*A11-b1*A01)/d, a1=(A00*b1-A01*b0)/d;
    double d0=a0-c0, d1=a1-c1, t1=v-c1*t-c0, t2=d1*t+d0, t3=A11*d1*d1+2*A01*d0*d1+A00*d0*d0;
    SS+=t1*(t1-2*t2)+t3; c1=a1; c0=a0;
}
double LinearEstimator::estimate_timeleft(double t,double *err,double v) { // -1 if no assumption
    if (err) *err=-1; if (lv>=v || A00<2) return -1;
    double d=A00*A11-A01*A01; //if (fabs(d)<EPS) return -1;
    double a0=(b0*A11-b1*A01)/d, a1=(A00*b1-A01*b0)/d; //if (fabs(a1)<EPS) return -1;
    double x=(v-a0)/a1, left=x-t;
    if (err) {
        disp.add(x);
        if (A00>2) {
            double q=sqrt(fabs(SS/(A00-1)))/a1, p=sqrt( disp.getD() );
            *err=hypot( q, p );
        }
    }
    return left;
}
Re: Как правильно делать прерываемые подпрограммы
От: loginx  
Дата: 13.02.18 16:27
Оценка:
Здравствуйте, GhostCoders, Вы писали:

GC>а также подпрограммы с показом текущео прогресса в %?


GC>Есть некая подпрограмма, которая делает что-то полезное и долго.


GC>Как ее безопасно отменить выполнение этой подпрограммы?


а что если запускать ее в виртуальной машине и управлять старт/стоп/пауза/возобновить
процессом через тот API и те средства что есть у виртуальной машины?
Re[2]: Как правильно делать прерываемые подпрограммы
От: Mr.Delphist  
Дата: 15.02.18 06:59
Оценка:
Здравствуйте, loginx, Вы писали:

L>а что если запускать ее в виртуальной машине и управлять старт/стоп/пауза/возобновить

L>процессом через тот API и те средства что есть у виртуальной машины?

Ну, можно на C# написать — там есть yield return
Re[3]: Как правильно делать прерываемые подпрограммы
От: loginx  
Дата: 15.02.18 09:42
Оценка:
Здравствуйте, Mr.Delphist, Вы писали:

MD>Здравствуйте, loginx, Вы писали:


L>>а что если запускать ее в виртуальной машине и управлять старт/стоп/пауза/возобновить

L>>процессом через тот API и те средства что есть у виртуальной машины?

MD>Ну, можно на C# написать — там есть yield return


это не то, как ты их внедришь внутрь чужой мат. либы нэйтивной — речь же о сложных длительных вычислениях
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.