【问题标题】:"Fixed" / "Load Balanced" C# thread pool?“固定”/“负载平衡”C# 线程池?
【发布时间】:2016-02-10 02:04:36
【问题描述】:

我有一个“昂贵”的第 3 方组件。该组件不是线程安全的。所述组件托管在 WCF 服务内部(目前),所以......每次调用进入服务时,我都必须更新组件。

我想做的是拥有一个由 16 个线程组成的池,每个线程启动自己的组件副本,并有一种机制来调用该方法并将其分发到 16 个线程之一并拥有返回值。

所以很简单,比如:

var response = threadPool.CallMethod(param1, param2);

调用阻塞直到它得到响应是可以的,因为我需要响应才能继续。

有什么建议吗?也许我想多了,由 16 个线程服务的 ConcurrentQueue 可以完成这项工作,但现在确定方法返回值将如何返回给调用者?

【问题讨论】:

  • 需要了解更多关于组件的信息。 1)它是一个COM组件吗? 2) 您如何托管 WCF 服务? IIS?在 Windows 服务中?控制台应用程序? 3)WCF服务的实例控制是什么?辛格尔顿?每次通话?每个会话?
  • 除了@MickyD 所说的之外,线程化调用以创建组件的新实例并不能解决您的线程安全问题(我认为这实际上会使情况变得更糟)。
  • 听起来你真正需要的是对象池,而不是线程池。
  • @MickyD,我相信它是围绕 C++ 库的 C# 互操作包装器。 WCF 托管在 IIS 中。默认线程。
  • @AWinkle... 不尝试创建线程。尝试新建 X 个实例并在应用程序的整个生命周期内保持它们的活动状态,并将调用分发给它们。

标签: c# multithreading wcf


【解决方案1】:

WCF 已经使用线程池来管理它的资源,所以如果你在上面添加一层线程管理,它只会变得很糟糕。尽可能避免这样做,因为您会在服务调用中发生争用。

在您的情况下,我会做的只是使用 single ThreadLocal 或线程静态,它会被您的昂贵对象初始化一次。此后它将可用于线程池线程。

这是假设您的对象在 MTA 线程上正常;我猜它来自您的帖子,因为听起来目前一切正常,但速度很慢。

担心会创建太多对象,并且随着池变得太大,您会使用太多内存。但是,在执行其他任何操作之前,请先查看实际情况是否如此。这是一个非常简单的策略,实施起来很容易试用。仅在您确实需要时才变得更复杂。

【讨论】:

  • 是的,我想我要找的是对象池,而不是线程池。该对象不是线程安全的。如果多个线程访问同一个实例,它就会崩溃。 ThreadLocal 没有用,因为 WCF 服务会创建和销毁线程。
  • 您在实践中实际测试过吗?很可能是这种情况,但这个人认为它只是从线程池中提取线程:stackoverflow.com/questions/5288991/destroy-a-wcf-thread。他说不要使用线程静态来存储状态,但这里你不是在存储状态。
【解决方案2】:

首先,我同意@briantyler:ThreadLocal<T> 或线程静态字段可能是您想要的。您应该以此为起点,如果它不能满足您的需求,请考虑其他选择。

一个复杂但灵活的替代方案是单例对象池。在最简单的形式中,您的池类型将如下所示:

public sealed class ObjectPool<T>
{
    private readonly ConcurrentQueue<T> __objects = new ConcurrentQueue<T>();
    private readonly Func<T> __factory;

    public ObjectPool(Func<T> factory)
    {
        __factory = factory;
    }

    public T Get()
    {
        T obj;
        return __objects.TryDequeue(out obj) ? obj : __factory();
    }

    public void Return(T obj)
    {
        __objects.Enqueue(obj);
    }
}

如果您在原始类或结构(即ObjectPool&lt;MyComponent&gt;)方面考虑类型T,这似乎不是非常有用,因为池没有内置任何线程控件。但您可以替换你的类型 T 为 Lazy&lt;T&gt; 或 Task&lt;T&gt; monad,得到你想要的。

池初始化:

Func<Task<MyComponent>> factory = () => Task.Run(() => new MyComponent());
ObjectPool<Task<MyComponent>> pool = new ObjectPool<Task<MyComponent>>(factory);

// "Pre-warm up" the pool with 16 concurrent tasks.
// This starts the tasks on the thread pool and
// returns immediately without blocking.
for (int i = 0; i < 16; i++) {
    pool.Return(pool.Get());
}

用法:

// Get a pooled task or create a new one. The task may
// have already completed, in which case Result will
// be available immediately. If the task is still
// in flight, accessing its Result will block.
Task<MyComponent> task = pool.Get();

try
{
    MyComponent component = task.Result; // Alternatively you can "await task"

    // Do something with component.
}
finally
{
    pool.Return(task);
}

这种方法比在ThreadLocal 或线程静态字段中维护您的组件更复杂,但如果您需要做一些花哨的事情,例如限制池实例的数量,池抽象会非常有用。

编辑

带有Get 的基本“固定X 实例集”池实现,一旦池耗尽就会阻塞:

public sealed class ObjectPool<T>
{
    private readonly Queue<T> __objects;

    public ObjectPool(IEnumerable<T> items)
    {
        __objects = new Queue<T>(items);
    }

    public T Get()
    {
        lock (__objects)
        {
            while (__objects.Count == 0) {
                Monitor.Wait(__objects);
            }

            return __objects.Dequeue();
        }
    }

    public void Return(T obj)
    {
        lock (__objects)
        {
            __objects.Enqueue(obj);

            Monitor.Pulse(__objects);
        }
    }
}

【讨论】:

  • 是的,我认为这是要走的路。我只需要调整对象池以拥有一组固定的 X 实例,而不是无限制地动态创建它们。
  • @SledgeHammer,在这种情况下,您可以使用 BlockingCollection&lt;T&gt; 作为您的对象池。如果您遇到所有池对象已被租出的情况,下一个消费者将阻塞Take(),直到返回其中一个实例(通过Add)。
  • 我还会考虑一个池,如果池中的实例计数超过所需值,Return 会丢弃返回的实例。这允许在绝对必要时分配新实例,但将收集任何多余的实例,从而防止无限增长。
  • 不幸的是,这并没有达到预期的效果:(。现在在单线程环境中运行 25 次需要 10 秒。如果我创建一个静态实例(未受保护),它会下降到 6 秒,所以我寄予厚望——但是将实例从线程安全池中拉出与新建一个新实例大致相同。我用 25 将 0 到 500 毫秒缩短了,所以我想它不会真正产生差异甚至数百万,因为这不会扩大规模。
猜你喜欢
  • 1970-01-01
  • 2012-08-28
  • 2017-09-21
  • 2012-12-16
  • 2022-01-10
  • 1970-01-01
  • 2012-09-13
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多