У меня есть программа, которая использует потоки для последовательного выполнения трудоемких процессов. Я хочу иметь возможность отслеживать ход выполнения каждого потока аналогично тому, как это делает модель BackgroundWorker.ReportProgress
/ProgressChanged
. Я не могу использовать ThreadPool
или BackgroundWorker
из-за других ограничений, с которыми я сталкиваюсь. Каков наилучший способ разрешить/открыть эту функцию. Перегрузить класс Thread
и добавить свойство/событие? Еще одно более элегантное решение?
Лучший способ сообщить о ходе обсуждения
Ответы (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-компилятору повторно применять эту «подъемную» оптимизацию.
var local = ...
stackoverflow.com/questions/ 1773680/
- person Andras Vass; 06.10.2010
Progress
было свежим всегда (не в следующий раз, а сейчас). Мне все равно, поднимаются ли чтения this
и args
за пределы оператора if
и выше чтения Progress
. Кстати, я мог бы использовать lock
, но тогда наивный программист мог бы удалить его позже, потому что он показался бессмысленным. Это один из немногих сценариев, где я действительно рекомендую Thread.MemoryBarrier
, потому что это делает намерение очевидным.
- person Brian Gideon; 07.10.2010
Progress
на каждой итерации цикла даже без явного барьера. Но, нет никакой гарантии.
- person Brian Gideon; 07.10.2010
Main()
после var local = GetEvent();
и один в цикле while
после local = GetEvent();
. Это можно выразить в GetEvent()
, поместив мембар между операторами присваивания и возврата.
- person Andras Vass; 07.10.2010
VolatileRead()
(если бы была перегрузка для событий....)
- person Andras Vass; 07.10.2010
local
можно оптимизировать. Это будет означать if (Progress != null) Progress()
, что явно не то, что задумано. Просто добавлю еще один замечательный пост Джо Даффи: bluebytesoftware.com/blog/2008 /06/13/
- person Andras Vass; 07.10.2010
volatileread
, и поместите это в правильное место.
- person Andras Vass; 07.10.2010
local
является ключевым здесь. И я вижу вашу точку зрения. Это определенно не то, о чем я думал. Я был больше сосредоточен на том, чтобы чтение выводилось за пределы цикла. Кажется, должен быть барьер между чтением и проверкой. Но если вы просто переместите его, то теперь вы позволите поднять начальное чтение Progress
, верно? Я собираюсь пойти дальше и изменить свой пример, чтобы включить полное ограждение с обеих сторон чтения. Это все исправит, и все еще остается более самодокументируемым, чем изолированный оператор lock
.
- person Brian Gideon; 07.10.2010
Thread.MemoryBarrier
(тот, что над чтением), потому что только начальное чтение может устареть, но как только цикл возвращается во второй раз, должно было произойти обновление. Я полностью с вами в этом.
- person Brian Gideon; 08.10.2010
Я попробовал это некоторое время назад, и это сработало для меня.
- Создайте
List
-подобный класс с блокировками. - Пусть ваши потоки добавляют данные в экземпляр созданного вами класса.
- Поместите таймер в форму или туда, где вы хотите записывать журнал/прогресс.
- Напишите код в событии
Timer.Tick
для чтения сообщений, выводимых потоками.
Вы также можете ознакомиться с асинхронным шаблоном на основе событий.
Обеспечьте каждый поток обратным вызовом, который возвращает объект состояния. Вы можете использовать ManagedThreadId
потока для отслеживания отдельных потоков, например, используя его в качестве ключа к Dictionary<int, object>
. Вы можете вызвать обратный вызов из множества мест в цикле обработки потока или вызвать его из таймера, запускаемого из потока.
Вы также можете использовать аргумент возврата в обратном вызове, чтобы сигнализировать потоку о приостановке или остановке.
Я использовал обратные вызовы с большим успехом.