Компьютерный форум OSzone.net  

Компьютерный форум OSzone.net (http://forum.oszone.net/index.php)
-   Программирование и базы данных (http://forum.oszone.net/forumdisplay.php?f=21)
-   -   multithread & exception-safe (http://forum.oszone.net/showthread.php?t=95916)

pva 11-12-2007 15:37 695178

multithread & exception-safe
 
Доброго времени суток. Чисто теоретический вопрос: пусть есть процесс, который наплодил классов, потом разделился на несколько нитей, каждая из которых наделала своих классов. В некоторый момент в одной нити произошло исключение. Как оно правильно должно быить обработано (имеется ввиду последовательность раскрутки стека).
В однонитевом процессе всё понятно, раскручиваем до ближайшего блока try..catch либо до конца программы. А вот что делать, когда одну из нитей, вызвавшую исключение, докрутим до места её основания? Завершать все остальные нити или прекратить раскрутку?

BlackEric 11-12-2007 16:20 695207

Что вы понимаете под раскруткой?
Добавление в блок catch оператора trow?
Если так, то куда его тянуть зависит от логики программы и ни отчего более. IMHO конечно...

pva 12-12-2007 10:21 695682

Под раскруткой имеется ввиду последовательное разрушение объектов (unwind), например:
Код:

class MyForm : public Form
{
    Label  label1;
    Button button1;
public:
    MyForm();
    ~MyForm();
    void do_something_bad();
};

void MyForm::do_something_bad()
{
    vector<int> vector_of_10_ints(10);
    // ...
  // ну ошибся нечаянно
    vector_of_10_ints.at(5) = vector_of_10_ints.at(15);
}

void do_something()
{
    MyForm my_form;
    my_form.do_something_bad();
}

порядок раскрутки:

раскручивается do_something_bad
vector<int>::~vector<int>();

раскручивается do_something
MyForm::~MyForm();
Button::~Button();
Label::~Label();
Form::~Form();

т.к. исключение до сих пор не обработано,
дальше раскручивается функция, которая
вызвала do_something, и т.д.

насчёт логики - да, хочу этим найти пологичнее...

BlackEric 12-12-2007 13:42 695802

Цитата:

Цитата pva
vector<int> vector_of_10_ints(10); // ... // ну ошибся нечаянно vector_of_10_ints.at(5) = vector_of_10_ints.at(15); »

Это баг. И обработка исключений как ее здесь не ставь не поможет.

ivank 13-12-2007 00:44 696163

pva, Вообще логично брошенное исключение хоть как-то обработать. У нас во всём, что является многонитевым "корневая" функция потока всегда завёрнута в try{}catch(...){}, который ругается в лог и просто убивает нить, заодно вычеркнув её из пула (нити никогда не завершаются, а сохраняются для следующего задания). Но у нас специфика в том, что есть пул одинаковых обработчиков, и в логику зашито, что некоторые обработчики могут падать, не выдавая, соответственно, никакого результата.

Так что мне кажется, всё зависит от логики программы. Если падает какая-то критичная для работы нить, то уж ничего не попишешь - всё придётся по возможности безболезненно убить.

Хотя, в случае гуя, я думаю, библиотека должна быть по максимуму вылизана, а если исключение случается в нити или пользовательском обработчике события, то оно должно быть перехвачено (т.е. стек раскручивается до того места, из которого библиотека вызвала пользовательский код), чтобы вылез попап "произошла неизвестная ошибка, но мы тем не менее продолжаем работать". Примерно так, по-моему, поступает дельфи. А то будет обидно, если в некоем важно приложении криворукий автор не валидирует какое-нибудь текстовое поле перед его использованием и всё падает.

Как-то так.

pva 13-12-2007 07:14 696235

У меня вообще вопрос пошёл из следующей ситуации (как раз описанной ivank в предыдущем посте): Есть форма, нарисованная на билдере, кнопной, с таймером и запросом. Автоматика такая:
1. Нажали кнопку, она залипла
2. через одну секнду выключилась (disable), запустился запрос
3. запрос отработал, выдал данные, включил кнопка тлипла обратно и включилась

теперь в запросе происходит исключение, кнопка не возвращается из своего необычного состояния - потому что нет откатов
Вот если бы это всё выполнялось "одной функцией", то есть без разбивки на OnClick и OnTimer, выглядело бы так:
Код:

struct restore_pushed_t
{
  TButton* button; 
  restore_pushed_t(TButton* b) : button(b)
  {
    SendMessage(button->Handle, BM_SETSTATE, BS_PUSHED, 0);
  }
  ~restore_pushed_t()
  {
    SendMessage(button->Handle, BM_SETSTATE, 0, 0);
  }

struct restore_enabled_t
{
  TButton* button; 
  restore_enabled_t(TButton* b) : button(b)
  {
    button->Enabled = false;
  }
  ~restore_enabled_t()
  {
    button->Enabled = true;
  }
}

void Form1::OnClick(TObject* /*sender*/)
{
  restore_pushed_t restore_pushed(Button1);
  sleep(1000);

  restore_enabled_t restore_enabled(Button1);
  ...
}

Теперь что ни случись, кнопка вернётся в своё нормальное состояние, а исключение не убьёт программу, потому что оно отловится снаружи Form1::OnClick борландовской библиотекой. Если что-то вернуться не сможет (в одном из деструкторов вспомогательных классов выплывет исключение), будет автоматически вызвана функция unhandled(), которая вызовет abort().
1. Чтобы на время выполнения обработки не заблокировать процесс, вполне логично запихнуть всё это в отдельный TThread. Вроде бы теперь логично обрамлять основной цикл TThread скобками try-catch. А как вести себя, если 2 нити взаимосвязаны и сдой в обной должен разрушить другую?
2. Вариант с использованием событийной системы, чтобы не плодить нити. Разбиваем void Form1::OnClick(TObject* /*sender*/) на части:
Код:

struct restore_obj
{
  restore_obj() : fprev(gstack_top)
  {
    gstack_top = this;
  }

  virtual ~restore_obj()
  {
    gstack_top = fprev;
  }

  static void unwind() throw()
  {
    while (gstack_top) delete gstack_top;
  }
private:
  restore_obj* fprev;
  static restore_obj* gstack_top;
};

struct restore_pushed_t : restore_obj {...}
struct restore_enabled_t : restore_obj {...}

void Form1::OnClick(TObject* /*sender*/)
{
  try {
    // включает таймер в конструкторе
    // выключает в деструкторе
    new restore_pushed_t(Button1, Timer1);
  }
  catch(...)
  {
      // log errors
      restore_obj::unwind();
      throw;
  }
}

void Form1::OnTimer(TObject* /*sender*/)
{
  try {
    Timer1->Enabled = false;
    new restore_enabled_t(Button1);
    ...

      restore_obj::unwind(); // вернуть в рабочем режиме
  }
  catch(...)
  {
      // log errors
      restore_obj::unwind();
      throw;
  }
}

чтобы не писать каждый раз try..catch можно поместить restore_obj::unwind(); в OnException. Вроде теперь всё приятно работает. Но опять возникаеи вопрос: а если я сделаю несколько стеков? в одном произошло исключение, что делать со вторым? как логичнее быть?

Oleg_SK 23-12-2007 04:08 702138

Вот в этих статьях хорошо рассматривается работа SEH Win32, в том числе и в многопоточной среде:
1) http://www.wasm.ru/article.php?article=GordonExcept
2) http://www.wasm.ru/series.php?sid=7
Единственный нюанс: требуется мало-мальское знание Ассемблера.

pva 04-01-2008 20:55 709020

Oleg_SK, в принципе про структурные исключения хорошо расписано в SDK, который идёт с борландовскими компиляторами. Все компиляторы, которые я использовал, очень хорошо справляются с исключениями C++ в плане техники (то есть они отлавливаются). Но одной техники мало. Я приведу описанный выше пример ещё раз (скорее всего непонятно выразился):
Допустим, есть форма. На ней кнопка, таймер и какая-нибудь ерундень типа TPicture.
1. при нажатии кнопки выключаем её Button->Enabled = true и включаем таймер
2. при срабатывании таймера выключаем таймер и загружаем картинку. Потом включаем кнопку
Очевидно, что исключение может возникнуть в любом месте программы (по неизвестным причинам). Например файл (картинка) не найден или заблокирован другим процессом.
При возникновении исключения в событии 1 после отключения кнопки или в событии 2 до включения кнопки, кнопка остаётся отключенной навсегда. Восстановить ситуацию поможет только перезапуск приложения. А ведь исключения отработали нормально, по правилам и ситуация не такая уж небывалая...
Ищу лекарство от таких болезней.

DillerInc 05-01-2008 19:34 709634

pva, и всё же я бы советовал прочесть статьи на WASM'е(если вы этого конечно уже не сделали).
Мне терзают сомнения,что стандартные механизмы компиляторов типа try..except обладают достаточной гибкостью по сравнению c установленным вручную SEH.
Вы же можете по идее в установленной/ых callback-процедуре/ах делать точную проверку состояния программы.Для этого можно использовать хоть глобальные переменные в секции данных,которые будут выступать в качестве флагов/меток.
И после можно будет поправить контекст потока так,чтобы исполнение продолжилось из безопасного места.

podsyp 02-03-2008 13:31 751744

Здравствуйте.

Хочу заметить некоторую особенность многопоточного программироваия.
Если в запускаете из потока другой поток, в котором вдруг возникает исключительная ситуация, то отловить её в первом потоке нельзя. Т.е. например для C# если блок try - catch содержит функцию my_thread.Start(), а функция потока my_thread генерирует исключение, то упомянутый блок try - catch его не перехватывает. Для перехвата данного исключения необходимо предусмотреть свой try - catch в обработчике потока my_thread. Если новый поток создаётся по событию из пула потоков, то данные рассуждения применимы и к нему.

Удачи.

DillerInc 02-03-2008 19:13 751907

podsyp, а статьи почитать при этом лень?
SEH -- это потокозависимый механизм, т.е. свой для каждого потока.Поэтому если его не установить в потоке,то исключение,которое произойдёт в этом потоке,не будет поймано.

ivank 02-03-2008 23:35 752066

DillerInc, Во-первых SEH и плюсовые (или шарповые) исключения это всё-таки разные вещи (хотя в VC++ плюсовые исключения, вроде, сделаны поверх SEH. По-крайней мере, раньше были). Во-вторых, podsyp именно о потокозависимости и сказал.

DillerInc 03-03-2008 11:39 752281

Просто он это сказал так,словно бы Америку открыл.В то время как этому есть вполне нормальное объяснение.

pva 07-03-2008 12:28 755283

а что происходит, если во втором потоке выплыло исключение, но неперехватилось? убивается только поток или вся программа?

podsyp 07-03-2008 14:48 755384

Здравствуйте.

Высылаю код на С#, связанный с вопросом. Короче говоря - ваше исключение не перехватывается. Программа нормально не работает. А имеет она 1 или несколько потоков выполнения по моему не имеет значения. Удачи.
using System;
using System.Collections.Generic;
using System.Text;

namespace ExceptionTester
{
class Program
{
static void Main(string[] args)
{
try
{
Runner r = new Runner();
r.Start();//здесь запускается новый поток
}
catch (Exception ex)
{
Console.WriteLine(ex.ToString());
}
}
}
class Runner
{
private System.Threading.Thread t;
public Runner()
{
t = new System.Threading.Thread(new System.Threading.ThreadStart(thr_func));
}
public void Start()
{
t.Start();
}
private void thr_func()
{
Console.WriteLine("I am started!");
while (true)
{
System.Threading.Thread.Sleep(100);
Console.Read();
throw new Exception("never catched");
}
}
}

}

DillerInc 07-03-2008 17:31 755501

Цитата:

Цитата pva
а что происходит, если во втором потоке выплыло исключение, но неперехватилось? убивается только поток или вся программа? »

...похоже,что вся программа.Дело в том,что система,не найдя ни одного SEH-обработчика,готового обработать возникшее исключение,передаёт последнее т.н. финальному обработчику,который выставляется самой ОС.А ему походу уже всё равно,в каком потоке всё это произошло.

podsyp, вы статьи читали?Вы думаете,что этот "шарп" за вас всё сделает,а вам останется только нажать на кнопочку компиляция...??

podsyp 07-03-2008 19:44 755564

Мне кажется, что вы ищете проблему там, где её нет. Ну не нравится вам шарп, ну создайте вы аналогичную программу на С++. Да, слово SEH красивое. Но это не повод изучать компилятор, а тем более переделывать его. Я больше чем уверен, что в С++ что то похожее можно изобразить, но придётся иметь дело с WinAPI (CreateThread, Callback и т.д.) и код на десять строчек не получится как в шарпе. А суть в ворохе программного кода теряется. Поэтому и возникают вопросы: "А что будет если я буду перехватывать исключение, созданное в одном потоке, в другом потоке?" Многопоточность задумывалась как возможность одновременного независимого выполнения разных задач. Это примерно так, что у вас работает Word и Excel, Word грохается, а увидеть мы это должны в Excel. Знаю, знаю я перепутал процессы с потоками и всё такое. Но по моему, то как работает компилятор шарпа - это правильно. Да есть хороший приём, когда мы не видим throw, а есть блок try - catch. Он генерируется в методах классов, которые мы используем. Можем даже отнаследовавшись генерировать свои исключения. Видимо всё это и рождает подобные вопросы. Вроде thread.Start() и есть вызов метода. Это заблуждение. Извините за прямоту. Удачи.

DillerInc 08-03-2008 01:40 755737

Цитата:

Цитата podsyp
Мне кажется, что вы ищете проблему там, где её нет. »

...мне же кажется,что вы пытаетесь закрыть глаза на имеющуюся проблему.
Цитата:

Цитата podsyp
Да, слово SEH красивое. Но это не повод изучать компилятор, а тем более переделывать его. »

...причём здесь красивое слово?И компилятор здесь также непричём.SEH является сервисом,предоставляемым операционной системой.Просто он относительно плохо документирован.
Цитата:

Цитата podsyp
Я больше чем уверен, что в С++ что то похожее можно изобразить »

...никаких WinAPI.Используется простая компиляторная обёртка в виде блока _try/ _except/ _finally.

Короче,вам решать.Будете ли вы дальше продолжать "спать" под нежный шелест вашего ЯВУ,либо попытаетесь разобраться во всем,как следует.Во втором случае необходимо первым делом в принудительном порядке читать Гордона:
http://www.wasm.ru/article.php?article=GordonExcept
Ну,а дальше можно и Мэта Питрека.

podsyp 09-03-2008 12:57 756414

Здравствуйте.
Я не расчитываю встретить понимание, но всё же задумайтесь. Не заостряясь на в статье по поводу SEH решалась задача как из одного потока узнать что исключение произошло в другом. Вот как это делается в шарпе на примере исключения деления на ноль:

using System;

namespace ConsoleApplication4
{
public delegate void EventDeledate(object o, ExcEventArgs e);

public class ExcEventArgs : EventArgs
{
public string Message;
public ExcEventArgs(string message)
{
Message = message;
}
}

class Program
{
private static string _exc_message;
static void Main(string[] args)
{
_exc_message = "";
Console.WriteLine("main thread started");
Runner r = new Runner();
r.OnException += new EventDeledate(r_OnException);
r.Run();//запуск другого потока
System.Threading.Thread.Sleep(1000);//ожидание события в другом потоке
Console.WriteLine(_exc_message);//вывод информации о событии, произошедшей в другом потоке
Console.Read();
}

static void r_OnException(object o, ExcEventArgs e)
{
_exc_message = e.Message;
}
}
class Runner
{
private System.Threading.Thread _thread;
public event EventDeledate OnException;
public Runner()
{
_thread = new System.Threading.Thread(new System.Threading.ThreadStart(Thread_Func));
}
public void Run()
{
_thread.Start();
}
private void Thread_Func()
{
try
{
Console.WriteLine("Another thread started...");
int divisor = 0;
int res = 1 / divisor;//генерация исключения
}
catch (DivideByZeroException ex)
{
OnException(this, new ExcEventArgs("division by zero!"));//генерация события
}
}
}
}

Не надо менять службы операционной системы. Не надо менять компилятор. Надо только воспользоватся событиями. Майкрософт пишет очень хороший софт, но очень плохую литературу, объясняющую как этот софт устроен. Поверьте, ни одна фирма не будет разглашать суть и способы создания программного обеспечения. А задурить башку - всегда пожалуйста.

DillerInc 09-03-2008 17:18 756536

podsyp, эээ...вы не путайте,пожалуйста,вкусное со сладким.То,что вы пытаетесь тут растолковать,попадает уже под разряд синхронизации,а вовсе не внутрипоточной обработки исключений.
Да,такое извращение,как вы тут описали,может иметь место.Но представьте это на практике: вы запускаете два потока,один из которых ничего не делает,а только ждёт события,которое оповестит о произошедшей в другом потоке ошибке.Получается,что по логике возникновение ошибки в вашем коде является почти запланированным, т.е. вы специально пишете кривой код полный багов на славу "гениальных" творений мелкомягких?!

Хорошо,запланированные исключения иногда могут дать большие возможности,например,программа,которая трассирует сама себя с помощью исключения EXCEPTION_SINGLE_STEP.Но наверно ни одному трезвомыслящему программисту не придёт в голову использовать для этого объекты синхронизации именно в таком виде,как вы описали.

Если вы хотите отлаживать программу,то не изобретайте велосипед,используйте DebugAPI.Там вы и будете ловить все исключения,как отладочные события.
Цитата:

Цитата podsyp
Не надо менять службы операционной системы. Не надо менять компилятор. »

...вы хорошо читаете?Никто ничего менять не собирается.Достаточно просто воспользоваться тем,что есть.
Цитата:

Цитата podsyp
А задурить башку - всегда пожалуйста. »

...вам её к сожалению уже походу и задурили.

podsyp 10-03-2008 08:24 756834

Здравствуйте DillerInc. Похоже форум превращается в диалог. Вы пишете, что в коде, который я представил, поток являющийся точкой входа в программу, ничего не делает, а только ждёт запланированного исключения. Не согласен с этим. Вместо Sleep можно поставить другие операторы и вообще опрашивать состояние переменной _exc_message в цикле попутно выполняя полезную работу. Правда у имеющейся программы есть недостаток: о создавшемся исключении основной поток узнаёт только через секунду. Кстати DillerInc, напишите что нужно сделать, чтобы вызвавший поток мгновенно оповещался о наступлении исключения в другом потоке. В статье Гордона это упоминалось. О недостатках такого способа вы как раз написали. Удачи.

DillerInc 10-03-2008 12:17 756932

podsyp, касательно многопоточных приложений в статье Гордона упор делается на корректное завершение потоков,которые непричастны к возникновению ошибки.Речь идёт о завершении процесса.

* Для этого первым делом в главном потоке устанавливается свой финальный обработчик с помощью функции SetUnhandledExceptionFilter.
* Когда система будет намерена завершить процесс из-за необработанного исключения,она вызовет этот наш финальный обработчик.
* Далее используется глобальная переменная,которая в финальном обработчике выставляется,например,в значение TRUE.Потоки приложения должны в цикле полезной работы проверять значение этой переменной.Если значение переменной равно TRUE, то они должны освободить ресурсы и завершится.
* Завершение потока есть событие.Поэтому в финальном обработчике после установки переменной необходимо вызвать функцию WaitForMultipleObjects, чтобы ожидать завершения всех указанных потоков.
* Затем выход из финального обработчика,и процесс убивается системой.

Вот так вот.

podsyp 10-03-2008 19:00 757230

Я ждал от вас DillerInc этого ответа. Но по моему прелесть многопоточности в том, что в одном потоке вызывается и перехватывается исключение , а другие работают себе как ни в чём ни бывало. А так представьте - один поток у вас музыку играет, другой анимацию делает, а третий вычисления производит. И вдруг в ходе вычислений происходит деление на ноль. Так что - все остальные потоки должны при этом завершится, пусть даже с корректным освобождением ресурсов? Так было в старые времена, когда никто про Windows и слыхом не слыхивал, работает себе программа и случается ошибка. И всё встаёт. В вашем описанном случае тоже не всё здорово? Возможно это и необходимо для программ, в которых не предусмотренно использования try - catch в каждом потоке. Но вообще как мне кажется включать для каждого потока свой try - catch хорошая практика. Пока она не "узаконена" производителями компиляторов, но ведь например раньше и такого понятия как свойство не было. Зато призывов не обращатся к внутренним переменным класса напрямую - в любой книжке по программированию было навалом. Функция WaitForMultipleObjects судя по названию как раз и занимается тем, что сидит и ждёт пока что нибудь страшное не случится. Видимо её я как раз и имел в виду когда спрашивал о мгновенном оповещении. Только в шарпе у неё оболочка AutoResetEvent. Ожидание производится функцией WaitOne(), а событие порождается функцией Set(). Создаёте отдельный поток для ожидания события который всего и делает что асинхронно ждёт наступления события. А вы меня ещё укоряли как это так моя программа ожидает запланированного исключения. Альтернативный вариант - это использование событий, как в том варианте что я выкладывал. Преимущество событий перед AutoResetEvent в том что они помимо сигнализации о себе могут передать информацию. И при этом не нужно создавать глобальных переменных. Для маленьких программ создание глобальных переменных может быть не такое уж зло, но для больших - крайне не желательно. Программа становится как бы завязанной в один узел, её трудно разделить на независимые части, поскольку для всех переменные одни. Удачи.

DillerInc 10-03-2008 19:53 757310

В общем,мне надоело уже тут биться об стену.Если вы такой умный,то разбирайтесь сами со своими "до-диезами".

pva 13-03-2008 13:53 759402

DillerInc, 2 поста назад - свежий взгляд (для моих мозгов), это тема, чёто не подумал раньше как-то. Ведь действиельно, если поток ещё работоспособный, он должен закончиться естественным образом (то бишь по проверке isApplicationTerminated).
podsyp, я бы всё-таки почитал литературу от создателей С++ (только не через призму С#), всё задумывалось совсем не так, а потом ребята из sun всё извратили, хотя получилось неплохо.
Когда программа завершается (любым способом), операционка может почистить за ней мусор и перезапустить её при необходимости, так что ничего не встанет. Слишком самоуверенные программы (которые в случае необработанного искобчения продолжают работать) несут большой риск...


Время: 12:48.

Время: 12:48.
© OSzone.net 2001-