2

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

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

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:仅允许通过线程安全包装器 IE 进行状态突变

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 来允许状态更改,以便更改器可以看到更改发生的时间,即

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;
    }

    // ...
}

我还没有考虑过哪些其他选择?有没有好的方法来做到这一点?我应该这样做吗?

4

3 回答 3

1

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

  1. 即使代码在技术上是正确的,您也会在您的域中遇到逻辑竞争条件。订单之类的东西可以在付款之前发货。
  2. 它损害了可维护性。线程代码难以调试、难以测试、难以阅读。当它与应用程序交错时,这变得更加复杂。

正确的方法是将线程代码与应用程序代码完全分离。将任务放在单线程光纤中。任务在所有涉及的实体中同步执行所有作业。任务完成后,可以异步执行IO。我已经为这种方法编写了一个库

于 2013-07-20T18:58:42.397 回答
1

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

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

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

于 2013-07-20T19:28:00.090 回答
0

For the time being, I ended up going with option 2 more or less, but I set the result synchronously if accessing the state won't block. I.E.

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);
                    }
                });
            }
        }
    }
}
于 2013-07-20T19:44:14.940 回答