рекурсивно вызывающий метод (с целью повторного использования объекта)

#c#

#c#

Вопрос:

У меня есть довольно большой класс, который содержит множество полей (более 10), огромный массив (100 кб) и некоторые неуправляемые ресурсы. Позвольте мне объяснить на примере

 class ResourceIntensiveClass
{
    private object unmaganedResource; //let it be the expensive resource
    private byte[] buffer = new byte[1024 * 100]; //let it be the huge managed memory
    private Action<ResourceIntensiveClass> OnComplete;


    private void DoWork(object state)
    {
        //do long running task
        OnComplete(this); //notify callee that task completed so it can reuse same object for another task
    }

    public void Start(object dataRequiredForCurrentTask)
    {
        ThreadPool.QueueUserWorkItem(DoWork); //initiate long running work
    }
}
  

Проблема в том, что метод start никогда не возвращается после 10000-й итерации, вызывая переполнение стека. Я мог бы выполнить делегат onComplete в другом потоке, дающий возможность возврата метода Start, но, как вы знаете, это требует использования дополнительного процессорного времени и ресурсов. Итак, какой вариант для меня лучший?

Комментарии:

1. @matt b Start вызывает OnCompleteMethod, который вызывает Start.

2. @TakeMeAsGuest — этот код просто неправильно пахнет. Я не понимаю, как рекурсия помогает вам с производительностью?

3. Я также хотел бы знать, «почему» вы реализовали это таким образом.

4. в реальной ситуации метод start запускает другие методы, скажем, в threadpool, и oncomplete вызывается из этих методов, а не непосредственно из start.

Ответ №1:

Есть ли веская причина для рекурсивного выполнения ваших вычислений? Кажется, что простой цикл справился бы с задачей, тем самым устраняя необходимость в невероятно глубоких стеках. Этот дизайн кажется особенно проблематичным, поскольку вы полагаетесь на main() для настройки вашей рекурсии.

Комментарии:

1. да, Start инициирует другие методы в threadpool, фактически oncomplete вызывается в другом потоке. приведенный код — это просто иллюстрация того, что я сейчас делаю, а не реальной ситуации.

2. В этом случае вам, возможно, потребуется указать больше подробностей, поскольку приведенный выше код, вероятно, не следует писать таким, какой он есть. Более подробная информация помогла бы.

3. спасибо, но класс написан таким, какой он есть. он выполняет длительные задачи, используя как неуправляемые дорогостоящие ресурсы, так и управляемую большую память. проблема здесь не в моем коде, а в ситуации

4. «проблема здесь не в моем коде, а в ситуации» — длев и другие говорят здесь о том, что, основываясь на вашем примере, возможно, решение, которое вы закодировали, не очень подходит для данной ситуации

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

Ответ №2:

рекурсивные методы могут довольно быстро выйти из-под контроля. Вы рассматривали возможность использования Parallel Linq? вы могли бы сделать что-то вроде

(ваш массив).AsParallel().ForAll(элемент => item.callMethod());

вы также могли бы заглянуть в библиотеку параллельных задач (TPL)

с помощью задач вы можете определить действие и продолжить выполнение задачи.

С другой стороны, Reactive Framework (RX) может обрабатывать эти полные события асинхронным образом.

Комментарии:

1. я знаю о tpl, но я хочу полностью контролировать происходящее, потому что процесс не так прост. и я думаю, что rx обрабатывает oncomplete в другом потоке, что я тоже мог бы сделать. в любом случае спасибо

2. используя RX, вы можете выполнить oncomplete в том же потоке, из которого он был первоначально запущен. использование метода subscribeOn.

3. тогда та же ситуация возникнет и в rx.

Ответ №3:

Где вы меняете значение taskData , чтобы его длина могла когда-либо равняться currentTaskIndex ? Поскольку задачи, которые вы назначаете данным, никогда не меняются, они выполняются вечно…

Комментарии:

1. должен ли я копировать весь свой проект? использование потоков, понимание рекурсивной природы потока программы, использование делегатов и т.д., Но создание дизайна только для двух объектов. хм, такой поучительный ответ.

2. Это зависит от того, что вы пытаетесь сделать… Очевидным выбором было бы удалить задачу из массива и убедиться, что `currentTaskIndex’ == 0, когда она будет завершена. Затем вы можете вернуть метод и выйти из рекурсии. В качестве альтернативы вы могли бы использовать цикл ‘for’, чтобы сделать это, что было бы проще 😉

3. 1-в реальном коде нет currentTaskIndex.2-тогда я могу вернуться туда, куда? в эфир? DoWork выполняется в threadpool. он просто возвращается в пул потоков.

4. Ознакомьтесь с этой страницей о потоковой обработке в C # … прошу прощения, если это слишком просто для вас, я больше знаком с Java. suite101.com/article.cfm/c_sharp/96436

5. спасибо, но я не имею никакого отношения к прерыванию потока, приостановке, interrupt. кстати, их следует использовать в качестве последнего средства, и почти во всех ситуациях их следует и «можно» избежать, используя другие методы.

Ответ №4:

Я бы предположил, что проблема возникает из-за использования здесь оператора предварительного увеличения:

  if(c.CurrentCount < 10000)
    c.Start(  c.CurrentCount);
  

Я не уверен в семантике предварительного приращения в C #, возможно, значение, передаваемое при вызове метода, не соответствует вашим ожиданиям.

Но поскольку ваш Start(int) метод в любом случае присваивает значение входных данных this.CurrentCount в качестве первого шага, вы должны безопасно заменить это на:

  if(c.CurrentCount < 10000)
    c.Start(c.CurrentCount   1);
  

Нет смысла назначать c.CurrentCount дважды.

Комментарии:

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

2. Я не уверен, к какой части моего вопроса вы обращаетесь? Я предлагаю использовать foo 1 вместо foo . Или вы утверждаете, что реальный код не подходит start( foo) ? Если это так, вы можете захотеть переработать свой пример кода, чтобы он был ближе к реальному.

3. Боюсь, это не помогает, поскольку в этом новом примере не показана рекурсия — должны ли слушатели OnComplete() вызывать DoWork() снова?

4. не будет ThreadPool.QueueUserWorkItem(DoWork) возвращаться после постановки рабочего элемента в очередь? На самом деле это даже не рекурсия, тогда, если это так.

5. да, он возвращается немедленно. но, скажем, метод subscriber, скажем, метод OnCompleteHandler, Start будет вызван снова.

Ответ №5:

При использовании пула потоков я предполагаю, что вы защищаете счетчики (c.CurrentCount), в противном случае одновременное увеличение приведет к увеличению активности, а не только к 10000 исполнениям.

Ответ №6:

Есть удобный инструмент под названием ManualResetEvent, который может упростить вам жизнь.

Поместите ManualResetEvent в свой класс и добавьте общедоступное OnComplete событие.

Когда вы объявляете свой класс, вы можете подключить OnComplete событие к какому-либо месту в вашем коде или не подключать его и игнорировать.

Это помогло бы вашему пользовательскому классу иметь более правильную форму.

Когда ваш длительный процесс будет завершен (я предполагаю, что это в потоке), просто вызовите метод Set ManualResetEvent .

Что касается запуска вашего метода long, он должен выполняться в потоке, который использует ManualResetEvent способом, аналогичным приведенному ниже:

 private void DoWork(object state)
{
    ManualResetEvent mre = new ManualResetEvent(false);
    Thread thread1 = new Thread(
      () => {
      //do long running task
      mre.Set();
    );
    thread1.IsBackground = true;
    thread1.Name = "Screen Capture";
    thread1.Start();
    mre.WaitOne();
    OnComplete(this); //notify callee that task completed so it can reuse same object for another task
}
  

Комментарии:

1. это требует создания потока для каждой задачи переключения в режим ядра ненужной блокировки. чтобы быть более понятным, длительно выполняющаяся задача использует порт завершения ввода-вывода, которому необязательно нужен поток для каждой задачи.