Лучший способ сообщить о ходе обсуждения

У меня есть программа, которая использует потоки для последовательного выполнения трудоемких процессов. Я хочу иметь возможность отслеживать ход выполнения каждого потока аналогично тому, как это делает модель BackgroundWorker.ReportProgress/ProgressChanged. Я не могу использовать ThreadPool или BackgroundWorker из-за других ограничений, с которыми я сталкиваюсь. Каков наилучший способ разрешить/открыть эту функцию. Перегрузить класс Thread и добавить свойство/событие? Еще одно более элегантное решение?


person Joel B    schedule 06.10.2010    source источник
comment
Как вы создаете темы?   -  person Steve Townsend    schedule 06.10.2010
comment
@ Стив Таунсенд - у меня есть фабрика, которая создает их все заранее, и конечный автомат, который контролирует, когда они запускаются.   -  person Joel B    schedule 06.10.2010
comment
Каковы ограничения? Можете ли вы создать класс, чтобы обернуть поток и использовать этот класс? Это шаблон активного объекта.   -  person Ray Henry    schedule 06.10.2010
comment
@Ray Henry - Спасибо за название шаблона проектирования, я все еще учусь/новичок в них. Ограничения заключаются в том, что мне нужно иметь возможность гарантировать, что определенные потоки выполняются одновременно, а определенные потоки выполняются последовательно (относительно других) в зависимости от типа их задачи и предоставленной дополнительной информации (по сути, матрица потоков). Я думаю, что класс-оболочка - мой лучший вариант, но, возможно, несколько BackgoundWorkers, разрешающих только один рабочий элемент, также могут быть вариантом.   -  person Joel B    schedule 06.10.2010
comment
Используйте разные BackgroundWorker для каждого потока, который вы хотите запустить одновременно. Используйте один BackgroundWorker для заданий, которые вы хотите запускать последовательно. На самом деле, в обработчике WorkCompleted вы можете настроить запуск следующего BackgroundWorker. Кроме того, если вы используете .NET 4, посмотрите на пространство имен System.Threading.Tasks.   -  person Ray Henry    schedule 06.10.2010


Ответы (4)


Перегрузить класс Thread и добавить свойство/событие?

Если под «перегрузкой» вы на самом деле подразумеваете наследование, то нет. Thread запечатан, поэтому он не может быть унаследован, что означает, что вы не сможете добавлять к нему какие-либо свойства или события.

Еще одно более элегантное решение?

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

public class Worker
{
  private Thread m_Thread = new Thread(Run);

  public event EventHandler<ProgressEventArgs> Progress;

  public void Start()
  {
    m_Thread.Start();
  }

  private void Run()
  {
    while (true)
    {
      // Do some work.

      OnProgress(new ProgressEventArgs(...));

      // Do some work.
    }
  }

  private void OnProgress(ProgressEventArgs args)
  {

    // Get a copy of the multicast delegate so that we can do the
    // null check and invocation safely. This works because delegates are
    // immutable. Remember to create a memory barrier so that a fresh read
    // of the delegate occurs everytime. This is done via a simple lock below.
    EventHandler<ProgressEventArgs> local;
    lock (this)
    {
      var local = Progress;
    }
    if (local != null)
    {
      local(this, args);
    }
  }
}

Обновление:

Позвольте мне немного пояснить, почему в этой ситуации необходим барьер памяти. Барьер предотвращает перемещение чтения перед другими инструкциями. Наиболее вероятная оптимизация не от ЦП, а от JIT-компилятора, «поднимающего» чтение Progress за пределы цикла while. Это движение производит впечатление «несвежего» чтения. Вот полуреалистичная демонстрация проблемы.

class Program
{
    static event EventHandler Progress;

    static void Main(string[] args)
    {
        var thread = new Thread(
            () =>
            {
                var local = GetEvent();
                while (local == null)
                {
                    local = GetEvent();
                }
            });
        thread.Start();
        Thread.Sleep(1000);
        Progress += (s, a) => { Console.WriteLine("Progress"); };
        thread.Join();
        Console.WriteLine("Stopped");
        Console.ReadLine();
    }

    static EventHandler GetEvent()
    {
        //Thread.MemoryBarrier();
        var local = Progress;
        return local;
    }
}

Крайне важно, чтобы сборка Release запускалась без процесса vshost. Любой из них отключит оптимизацию, которая проявляет ошибку (я полагаю, что это невозможно воспроизвести в версиях 1.0 и 1.1 фреймворка из-за их более примитивной оптимизации). Ошибка в том, что «Остановлено» никогда не отображается, хотя это явно должно быть. Теперь раскомментируйте вызов Thread.MemoryBarrier и обратите внимание на изменение поведения. Также имейте в виду, что даже самые незначительные изменения в структуре этого кода в настоящее время препятствуют возможности компилятора выполнить рассматриваемую оптимизацию. Одним из таких изменений будет фактический вызов делегата. Другими словами, вы не можете в настоящее время воспроизвести устаревшую проблему чтения, используя проверку нуля, за которой следует шаблон вызова, но в спецификации CLI ничего нет (о чем я и так знаю ), который запрещает будущему гипотетическому JIT-компилятору повторно применять эту «подъемную» оптимизацию.

person Brian Gideon    schedule 06.10.2010
comment
@Brian: возможно, вы захотите рассмотреть возможность перемещения барьера после var local = ... stackoverflow.com/questions/ 1773680/ - person Andras Vass; 06.10.2010
comment
@andras: Нет, это действительно смутило бы людей. В данном случае совершенно неважно, где он находится. Но цель здесь в том, чтобы чтение Progress было свежим всегда (не в следующий раз, а сейчас). Мне все равно, поднимаются ли чтения this и args за пределы оператора if и выше чтения Progress. Кстати, я мог бы использовать lock, но тогда наивный программист мог бы удалить его позже, потому что он показался бессмысленным. Это один из немногих сценариев, где я действительно рекомендую Thread.MemoryBarrier, потому что это делает намерение очевидным. - person Brian Gideon; 07.10.2010
comment
@Brian: многие люди неправильно понимают мембары, я думаю: connect.microsoft.com/VisualStudio/feedback/ - person Andras Vass; 07.10.2010
comment
@andras: Да, барьеры памяти, безусловно, одна из самых запутанных тем (помимо системы подоходного налога в США). Я действительно прочитал все те ссылки, которые вы разместили ранее. Недавно я даже разместил комментарий в блоге Эрика с просьбой разъяснить этот вопрос. - person Brian Gideon; 07.10.2010
comment
@andras: Имейте в виду, что здесь задействованы две модели памяти: модель, определяемая аппаратным обеспечением (архитектура ЦП), и модель, определяемая программным обеспечением (CLR). Модель, с которой вы должны программировать, является более слабой из двух. В этом случае это почти всегда CLR, поскольку, как вы сказали, x86/x64 на самом деле имеют довольно сильные модели. Это, вероятно, причина, по которой вы не увидите барьеры памяти, испускаемые в скомпилированном коде для этих процессоров. Но это не мешает CLR выполнять собственное переупорядочение. - person Brian Gideon; 07.10.2010
comment
@andras: Так нужен барьер или нет? Да, но только на очень технических и теоретических основаниях. Все версии CLR до 4.0 вероятно будут видеть свежие* чтения Progress на каждой итерации цикла даже без явного барьера. Но, нет никакой гарантии. - person Brian Gideon; 07.10.2010
comment
@Brian: в вашем обновлении, если вы проверите места, где мембар необходим для предотвращения переупорядочения на уровне JIT или сборки, вы найдете два из них: один в Main() после var local = GetEvent(); и один в цикле while после local = GetEvent();. Это можно выразить в GetEvent(), поместив мембар между операторами присваивания и возврата. - person Andras Vass; 07.10.2010
comment
@Brian: это - не случайно - именно то, чем был бы VolatileRead() (если бы была перегрузка для событий....) - person Andras Vass; 07.10.2010
comment
@Brian: более конкретный пример для вашего первого кода: если вы оставите мембар там, где вы его поместили, может произойти теоретическая оптимизация, а именно, local можно оптимизировать. Это будет означать if (Progress != null) Progress(), что явно не то, что задумано. Просто добавлю еще один замечательный пост Джо Даффи: bluebytesoftware.com/blog/2008 /06/13/ - person Andras Vass; 07.10.2010
comment
@Brian: :) после изучения моей истории: code.logos.com/blog /2008/11/events_and_threads_part_4.html (конечно, чисто теоретически :) Я хотел предположить, но, скорее всего, не смог, так это то, что мембар находится не в том месте, так что либо полностью его оставьте, либо если вы хотите быть абсолютно правильным с теоретической точки зрения, сделайте именно то, что делает volatileread, и поместите это в правильное место. - person Andras Vass; 07.10.2010
comment
@Brian: я нашел еще один пример правильного размещения мембара при загрузке (точно так же, как в вашем первом примере, между назначением и проверкой): заставить его работать с явными барьерами памяти в части cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html (отличные авторы тоже. :) - person Andras Vass; 07.10.2010
comment
@Brian Gideon - это именно та функциональность, которую я собираюсь использовать для исправления. Большое спасибо за время, которое вы потратили на этот ответ. - person Joel B; 07.10.2010
comment
@Brian: я не призываю избегать мембара - всегда лучше перестраховаться, чем сожалеть. Я пытался сказать, что вы поместили его не в то место. Так совпало, что в первом примере он работает - но первый работает и без него. Также по совпадению это работает и во втором примере, поскольку JIT не выполняет теоретическую оптимизацию, которую он мог бы сделать (например, потому что это привело бы к дополнительному чтению, которое запрещено в неофициальной памяти MS .NET 2.0+). модель). Иметь его там, где он сейчас, — не лучшее место для него. - person Andras Vass; 07.10.2010
comment
@andras: Я думаю, что ваш комментарий об оптимизации local является ключевым здесь. И я вижу вашу точку зрения. Это определенно не то, о чем я думал. Я был больше сосредоточен на том, чтобы чтение выводилось за пределы цикла. Кажется, должен быть барьер между чтением и проверкой. Но если вы просто переместите его, то теперь вы позволите поднять начальное чтение Progress, верно? Я собираюсь пойти дальше и изменить свой пример, чтобы включить полное ограждение с обеих сторон чтения. Это все исправит, и все еще остается более самодокументируемым, чем изолированный оператор lock. - person Brian Gideon; 07.10.2010
comment
@ Брайан: спасибо, что все еще терпишь меня. :) Я ценю. Если вы переместите барьер, то ничего странного не произойдет. Да, чтение может всплывать вверх, но не может плыть вниз и преодолевать барьер. Что касается всплытия: это не имеет большого значения. Предположим, что нагрузка попадает в буфер и обслуживается на 70 циклов раньше. Итак, то, что вы видите, это старые данные за 70 циклов. Теперь предположим, что вы поставили барьер перед чтением. Барьер ожидает, пока буферы хранения и загрузки опустеют. Проходит 100 циклов. К моменту получения якобы свежих данных прошло 100 циклов. Он еще свежий? :) - person Andras Vass; 08.10.2010
comment
@Brian: Думаю, возможно, лучше забыть о барьерах как о способе предоставления свежих данных - имхо, это лучший способ думать о них, как следует из их названия, о барьерах или заборах - и размещать их там, где вы не хотите читать и пишет, чтобы пройти либо вверх, либо вниз либо процессором, либо компилятором. (Думаю, Джо Даффи был прав, когда сказал, что даже описание MSDN в некотором смысле вводит в заблуждение.) - person Andras Vass; 08.10.2010
comment
@Brian: перемещение забора на строку вниз сразу после присвоения локальной переменной является достаточным и правильным для любого из ваших примеров. (Я хотел бы исправить, что чтение исключения из локальной оптимизации var является чисто теоретическим. Это была легальная оптимизация для x64 JIT: blogs.msdn.com/b/grantri/archive/2004/09/07/226355.aspx ) - person Andras Vass; 08.10.2010
comment
@andras: Вы абсолютно правы. Не имеет значения, удаляется ли первый Thread.MemoryBarrier (тот, что над чтением), потому что только начальное чтение может устареть, но как только цикл возвращается во второй раз, должно было произойти обновление. Я полностью с вами в этом. - person Brian Gideon; 08.10.2010
comment
@andras: Кстати, вы должны присоединиться к комментариям в блоге Эрика о поточно-ориентированном шаблоне вызова событий. Я написал о своем беспокойстве по этому поводу пару недель назад, и с тех пор никто не прокомментировал. - person Brian Gideon; 08.10.2010
comment
@Brian: я присоединюсь, спасибо, что подняли этот вопрос. :) Я думаю, что пример, который я привел для проблемы свежести, был не самым лучшим, может быть, этот лучше (но, пожалуйста, не стесняйтесь выдвигать любые возражения): учтите, что поток вытесняется сразу после чтения значения локальному. К тому времени, когда вы исследуете его в состоянии, это, как вы выразились, устаревшее значение. Обратите внимание, что значения устарели - это, конечно, неправильное слово здесь - в любом случае, и вы мало что можете с этим поделать. Барьеры памяти не помогут в этом. Тоже, впрочем, неважно. - person Andras Vass; 09.10.2010
comment
У меня гораздо больше вопросов, чем я пришел сюда сейчас. Было бы здорово получить более прямое объяснение того, что делает предлагаемое решение и почему оно необходимо. - person xr280xr; 24.04.2021

Я попробовал это некоторое время назад, и это сработало для меня.

  1. Создайте List-подобный класс с блокировками.
  2. Пусть ваши потоки добавляют данные в экземпляр созданного вами класса.
  3. Поместите таймер в форму или туда, где вы хотите записывать журнал/прогресс.
  4. Напишите код в событии Timer.Tick для чтения сообщений, выводимых потоками.
person Alex Essilfie    schedule 06.10.2010

Вы также можете ознакомиться с асинхронным шаблоном на основе событий.

person YWE    schedule 06.10.2010
comment
+1 - Я буду использовать решение Брайана для быстрого исправления и в конечном итоге перейду к более четному асинхронному шаблону для моего конечного продукта. Спасибо за ссылку! - person Joel B; 07.10.2010

Обеспечьте каждый поток обратным вызовом, который возвращает объект состояния. Вы можете использовать ManagedThreadId потока для отслеживания отдельных потоков, например, используя его в качестве ключа к Dictionary<int, object>. Вы можете вызвать обратный вызов из множества мест в цикле обработки потока или вызвать его из таймера, запускаемого из потока.

Вы также можете использовать аргумент возврата в обратном вызове, чтобы сигнализировать потоку о приостановке или остановке.

Я использовал обратные вызовы с большим успехом.

person Ed Power    schedule 06.10.2010