C#线程模式,让我冲洗

时间:2010-03-18 21:37:59

标签: c# design-patterns .net-2.0 multithreading

我有一个实现Begin / End Invocation模式的类,我最初使用ThreadPool.QueueUserWorkItem()来处理我的工作。在线程上完成的工作不会循环,但需要花费一些时间来处理,因此工作本身不容易停止。

我现在有副作用,有人使用我的类调用Begin(带回调)很多次来进行大量处理,因此ThreadPool.QueueUserWorkItem创建了大量的线程来进行处理。这本身并不错,但有些情况下他们想要放弃处理并启动新流程,但他们被迫等待第一次请求完成。

由于ThreadPool.QueueUseWorkItem()不允许我取消线程,我试图想出一个更好的方法来排队工作,并且可能在我的类中使用显式的FlushQueue()方法来允许调用者放弃在队列中工作。

任何人都对符合我需求的线程模式有任何建议吗?

编辑:我目前正在定位2.0框架。我目前正在考虑使用Consumer / Producer队列。有没有人想过冲洗队列?

编辑2问题澄清: 因为每次调用者使用Begin with Callback时我都在我的类中使用Begin / End模式,所以我在线程池上创建了一个全新的线程。此调用只进行了很少的处理,而不是我想取消的地方。这是我希望停止的队列中未完成的工作。

ThreadPool默认情况下每个处理器创建250个线程的事实意味着如果你要求ThreadPool使用QueueUserWorkItem()排队大量项目,你最终会创建大量的并发线程,你无法停止

由于我对线程进行排队的方式,调用者不仅可以将CPU推送到100%而且还可以创建工作。

我在考虑使用Producer / Consumer模式,我可以将这些线程排入我自己的队列,这样我就可以调整我创建的线程数以避免CPU峰值创建所有并发线程。并且我可以允许我的类的调用者在放弃请求时刷新队列中的所有作业。

我目前正在努力实现这一点,但我认为这是一个让别人说看看这个代码的好地方,否则你将无法冲洗这个或冲洗不是你说的合适的术语。

4 个答案:

答案 0 :(得分:1)

编辑我的答案不适用,因为OP使用的是2.0。对于阅读此问题并使用4.0

的任何人,请离开并切换到CW

如果您使用的是C#4.0,或者可以对早期版本的并行框架之一进行依赖,则可以使用其内置的取消支持。它不像取消线程那么容易,但框架更可靠(取消线程非常有吸引力,但也非常危险)。

里德做了一篇很好的文章,你应该看看

答案 1 :(得分:1)

我过去使用的一种方法,虽然它当然不是最佳实践,但是为每个线程专用一个类实例,并在类上有一个中止标志。然后在该类上创建一个ThrowIfAborting方法,该方法从该线程定期调用(特别是如果该线程正在运行循环,则每次迭代都调用它)。如果已经设置了标志,ThrowIfAborting将只抛出一个异常,该异常在线程的main方法中被捕获。只需确保在中止时清理资源。

答案 2 :(得分:0)

通过在1个以上BackgroundWorker实例周围使用包装类,我已经解决了我认为是您的确切问题。

不幸的是,我无法发布我的全班,但这是基本概念及其局限性。

<强>用法: 您只需创建一个实例,并在想要取消旧工作人员并启动新工作人员时调用RunOrReplace(...)。如果旧工作人员忙,则要求取消,然后另一个工作人员用于立即执行您的请求。

public class BackgroundWorkerReplaceable : IDisposable
{

BackgroupWorker activeWorker = null;
object activeWorkerSyncRoot = new object();
List<BackgroupWorker> workerPool = new List<BackgroupWorker>();

DoWorkEventHandler doWork;
RunWorkerCompletedEventHandler runWorkerCompleted;

public bool IsBusy
{
    get { return activeWorker != null ? activeWorker.IsBusy; : false }
}

public BackgroundWorkerReplaceable(DoWorkEventHandler doWork, RunWorkerCompletedEventHandler runWorkerCompleted)
{
    this.doWork = doWork;
    this.runWorkerCompleted = runWorkerCompleted;
    ResetActiveWorker();
}

public void RunOrReplace(Object param, ...) // Overloads could include ProgressChangedEventHandler and other stuff
{
    try
    {
        lock(activeWorkerSyncRoot)
        {
            if(activeWorker.IsBusy)
            {
                ResetActiveWorker();
            }

            // This works because if IsBusy was false above, there is no way for it to become true without another thread obtaining a lock
            if(!activeWorker.IsBusy)
            {
                // Optionally handle ProgressChangedEventHandler and other features (under the lock!)

                // Work on this new param
                activeWorker.RunWorkerAsync(param);
            }
            else
            { // This should never happen since we create new workers when there's none available!
                throw new LogicException(...); // assert or similar
            }
        }
    }
    catch(...) // InvalidOperationException and Exception
    { // In my experience, it's safe to just show the user an error and ignore these, but that's going to depend on what you use this for and where you want the exception handling to be
    }
}

public void Cancel()
{
    ResetActiveWorker();
}

public void Dispose()
{ // You should implement a proper Dispose/Finalizer pattern
    if(activeWorker != null)
    {
        activeWorker.CancelAsync();
    }
    foreach(BackgroundWorker worker in workerPool)
    {
        worker.CancelAsync();
        worker.Dispose();
        // perhaps use a for loop instead so you can set worker to null?  This might help the GC, but it's probably not needed
    }
}

void ResetActiveWorker()
{
    lock(activeWorkerSyncRoot)
    {
        if(activeWorker == null)
        {
            activeWorker = GetAvailableWorker();
        }
        else if(activeWorker.IsBusy)
        { // Current worker is busy - issue a cancel and set another active worker
            activeWorker.CancelAsync(); // Make sure WorkerSupportsCancellation must be set to true [Link9372]
            // Optionally handle ProgressEventHandler -=
            activeWorker = GetAvailableWorker(); // Ensure that the activeWorker is available
        }
        //else - do nothing, activeWorker is already ready for work!
    }
}
BackgroupdWorker GetAvailableWorker()
{
    // Loop through workerPool and return a worker if IsBusy is false
    // if the loop exits without returning...
    if(activeWorker != null)
    {
        workerPool.Add(activeWorker); // Save the old worker for possible future use
    }
    return GenerateNewWorker();
}
BackgroundWorker GenerateNewWorker()
{
    BackgroundWorker worker = new BackgroundWorker();
    worker.WorkerSupportsCancellation = true; // [Link9372]
    //worker.WorkerReportsProgress
    worker.DoWork += doWork;
    worker.RunWorkerCompleted += runWorkerCompleted;
    // Other stuff
    return worker;
}

} // class

<强>临/ CON

这样做的好处是可以在启动新执行时具有非常低的延迟,因为新线程不必等待旧线程完成

这是以从未获得GC的BackgroundWorker对象的理论永无止境的增长为代价的。但是,在实践中,下面的代码会尝试回收旧工作程序,因此通常不会遇到大量理想线程。如果您因为打算如何使用这个类而担心这个问题,那么您可以实现一个触发CleanUpExcessWorkers(...)方法的Timer,或者让ResetActiveWorker()执行此清理(以更长的RunOrReplace(。)为代价。 ..)延迟)。

使用它的主要成本正是为什么它有用 - 它不等待前一个线程退出,例如,如果DoWork正在执行数据库调用并且您执行RunOrReplace(...)10次快速连续,当线程可能没有立即取消数据库调用 - 所以你将运行10个查询,使所有这些查询变慢!这通常适用于Oracle,只会导致轻微延迟,但我没有其他数据库的经验(为了加快清理速度,我让被取消的工作人员告诉Oracle取消命令)。正确使用下面描述的EventArgs主要解决了这个问题。

另一个小成本是,无论BackgroundWorker执行的任何代码必须与此概念兼容 - 它必须能够安全地从被取消中恢复。 DoWorkEventArgs和RunWorkerCompletedEventArgs具有应该使用的取消/取消属性。例如,如果您在DoWork方法中执行数据库调用(主要是我使用此类的方法),则需要确保定期检查这些属性并执行相应的清理。

答案 3 :(得分:0)

您可以将Begin / End模式扩展为Begin / Cancel / End模式。 Cancel方法可以设置工作线程定期轮询的 cancel 标志。当工作线程检测到取消请求时,它可以停止工作,根据需要清理资源,并报告操作取消作为End参数的一部分。

相关问题