如何在C#中处理许多并发的交互实体

时间:2013-07-19 14:50:12

标签: c# .net concurrency

我正在开发一个具有许多可以同时交互的不同实体的应用程序。我想知道这些实体以线程安全的方式相互交互的最佳方式是什么。

为了演示一些简化的代码,请考虑每个实体都有自己的光纤和一些状态:

class Fiber
{
    private ActionBlock<Action> _workQueue;

    public Fiber()
    {
        _workQueue = new ActionBlock<Action>((a) => a());
    }

    public void Enqueue(Action a)
    {
        _workQueue.Post(a);
    }

    public void Stop()
    {
        _workQueue.Complete();
    }
}

class EntityState
{
    public int x { get; set; }
}

class Entity
{
    private Fiber _fiber = new Fiber();

    public EntityState State { get; set; }

    // ...
}

假设动作被任意排队到实体光纤上。一个这样的动作可能是实体必须修改另一个实体的状态。我已经考虑过以线程安全方式执行此操作的两个选项。

选项1:仅允许通过线程安全包装器进行状态突变,I.E。

class Entity
{
    private Fiber _fiber = new Fiber();

    private ReaderWriterLockSlim _stateLock = new ReaderWriterLockSlim();
    private EntityState _state = new EntityState();

    public T ReadState<T>(Func<EntityState, T> reader)
    {
        T result = default(T);

        _stateLock.EnterReadLock();
        result = reader(_state);
        _stateLock.ExitReadLock();

        return result;
    }

    public void WriteState(Action<EntityState> writer)
    {
        _stateLock.EnterWriteLock();
        writer(_state);
        _stateLock.ExitWriteLock();
    }

    // ...
}

选项2:只允许状态变异,将其调度到拥有实体的光纤上并返回一个Future,以便mutator可以看到变异发生的时间,I.E。

class Future<T>
{
    public T Value { get; set; }
}

class Entity
{
    private Fiber _fiber = new Fiber();

    private EntityState _state = new EntityState();

    public Future<T> AccessState<T>(Func<EntityState, T> accessor)
    {
        Future<T> future = new Future<T>();

        _fiber.Enqueue(() => future.Value = accessor(_state));

        return future;
    }

    // ...
}

我还没考虑过其他什么选择?有没有办法做到这一点?我应该这样做吗?

3 个答案:

答案 0 :(得分:1)

您的所有选择都会给您带来痛苦。

  1. 即使代码在技术上是正确的,您也会遇到域中的逻辑竞争条件。订单之类的东西可以在付款之前发货。
  2. 这会伤害可维护性。线程代码很难调试,难以测试,难以阅读。当它与应用程序交错时,这会变得更加复杂。
  3. 正确的方法是将线程代码与应用程序代码完全分开。将任务放在单线程光纤中。任务在所有涉及的实体中同步执行所有作业。任务完成后,您可以异步执行IO。 我已经为这种方法写了a library

答案 1 :(得分:1)

您可以将突变排入拥有光纤,然后将其延续到您自己的光纤中。这样你就没有任何明确的锁。

但是:这种光纤方法并不比在访问实体之前获取lock更好。 (锁在内部包含一个队列。)

此外,您不能以这种方式进行跨实体交易。使用锁方法,您可以收集参与事务的所有实体的锁,将它们排序为总订单并将它们全部锁定。这为您提供了没有死锁的跨实体事务。

答案 2 :(得分:0)

目前,我最终选择了2或更少,但是如果访问状态不会阻止我同步设置结果。即。

using System;
using System.Threading;

namespace Server.Utility
{
    public class ThreadSafeWrapper<ObjectType>
    {
        private ObjectType m_object;
        private Fiber m_fiber;

        public ThreadSafeWrapper(ObjectType obj, Fiber fiber)
        {
            m_object = obj;
            m_fiber = fiber;
        }

        public Future<ReturnType> Transaction<ReturnType>(Func<ObjectType, ReturnType> accessor)
        {
            Future<ReturnType> future = new Future<ReturnType>();

            ReturnType synchronousResult = default(ReturnType);
            if (Monitor.TryEnter(m_object))
            {
                synchronousResult = accessor(m_object);
                Monitor.Exit(m_object);

                future.SetResult(synchronousResult);
            }
            else
            {
                m_fiber.Enqueue(() =>
                {
                    ReturnType result = default(ReturnType);
                    lock (m_object)
                    {
                        result = accessor(m_object);
                    }
                    future.SetResult(result);
                });
            }

            return future;
        }

        public void Transaction(Action<ObjectType> accessor)
        {
            if (Monitor.TryEnter(m_object))
            {
                accessor(m_object);
                Monitor.Exit(m_object);
            }
            else
            {
                m_fiber.Enqueue(() =>
                {
                    lock (m_object)
                    {
                        accessor(m_object);
                    }
                });
            }
        }
    }
}