报告线程进度的最佳方法

6
我有一个程序,使用线程按顺序执行耗时进程。我希望能够监视每个线程的进度,类似于BackgroundWorker.ReportProgress/ProgressChanged模型的方式。由于其他限制,我不能使用ThreadPoolBackgroundWorker。如何最好地允许/公开此功能?重载Thread类并添加属性/事件?还有更优雅的解决方案吗?

你是如何创建线程的? - Steve Townsend
@ Steve Townsend - 我有一个工厂预先创建它们,以及一个状态机来控制何时运行它们。 - Joel B
有哪些限制?你能否创建一个类来包装线程并使用这个类?这就是Active Object模式。 - Ray Henry
@Ray Henry - 感谢您提供设计模式名称,我仍在学习/新手。约束条件是我需要确保某些线程并发运行,而某些线程按顺序运行(相对于其他线程),这取决于它们的任务类型和提供的其他信息(实际上是一个线程矩阵)。我认为包装器类是最佳选择,但也许允许仅有单个工作项的多个BackgroundWorkers也是一种选择。 - Joel B
1
为了同时运行想要运行的每个线程,请使用不同的BackgroundWorker。对于要按顺序运行的作业,请使用单个BackgroundWorker。实际上,在WorkCompleted处理程序中,您可以设置下一个要运行的BackgroundWorker。另外,如果您正在使用.NET 4,则请查看System.Threading.Tasks命名空间。 - Ray Henry
4个回答

11

如何重载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);
    }
  }
}

更新:

让我更清楚地解释为什么在这种情况下需要内存屏障。屏障可以防止读取在其他指令之前被移动。最有可能的优化不是来自CPU,而是来自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中也不可重现,因为它们有更原始的优化)。问题是即使明显应该显示“Stopped”,但其不会被显示。现在,请取消对Thread.MemoryBarrier的调用并注意行为上的变化。还要记住,即使对此代码结构进行最微小的更改,也可能当前阻止编译器进行所需的优化。这样的更改之一是实际调用委托。换句话说,您目前无法使用空值检查后跟调用模式来重现过时读取的问题,但我不知道CLI规范中是否有任何禁止未来假设的JIT编译器重新应用该“提升”优化的规定。


@andras:是的,内存屏障绝对是最令人困惑的话题之一(除了美国所得税制度)。我实际上已经阅读了你发布的所有链接。最近我甚至在Eric的博客上发表了一条评论,请求澄清这一点。 - Brian Gideon
@Brian Gideon - 这正是我将用于我的修复的功能。非常感谢您为此答案付出的时间。 - Joel B
@Brian:将栅栏向下移动一行,紧接着对本地变量的赋值是你所举例子中任何情况下都足够且正确的。我想纠正一下,“消除对本地变量的读取”优化仅仅是理论上的。这在x64 JIT上是一种合法的优化方式:http://blogs.msdn.com/b/grantri/archive/2004/09/07/226355.aspx - Andras Vass
我现在有比来这里时更多的问题。如果能有一个更直接的解释,说明建议的解决方案是做什么的以及为什么需要它,那就太好了。 - xr280xr
显示剩余16条评论

1

我之前试过这个方法,对我有效。

  1. 创建一个带锁的类,类似于 List
  2. 让你的线程将数据添加到你创建的类的实例中。
  3. 在你的窗体或任何你想要记录日志/进度的地方放置一个计时器。
  4. Timer.Tick 事件中编写代码来读取线程输出的消息。

1

+1 - 我会使用Brian的解决方案来进行快速修复,最终将转向更基于事件的异步模式来完成我的最终产品。感谢提供的链接! - Joel B

0

为每个线程提供一个返回状态对象的回调函数。您可以使用线程的ManagedThreadId来跟踪不同的线程,例如将其用作Dictionary<int, object>的键。您可以从线程的处理循环中的多个位置调用回调函数,或者从线程内部触发的定时器中调用它。

您还可以使用回调函数的返回参数来向线程发出暂停或停止的信号。

我已经成功地使用了回调函数。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接