【问题标题】:Prevent changes in resources while calculations are in progress防止在计算过程中更改资源
【发布时间】:2018-09-21 21:33:22
【问题描述】:

我有一个运行速度很慢的应用程序,我正在尝试加快它的速度。 我对并发系统很陌生,所以我有点卡在这里。

简而言之,我可以将系统呈现为以下类:

一些正在处理的资源

public class Resource
{
    public int Capacity { get; set; } = 1000;
}

消费者

public class Consumer
{
    private readonly int _sleep;

    public Consumer(int sleep)
    {
        _sleep = sleep;
    }

    public void ConsumeResource(Resource resource)
    {
        var capture = resource.Capacity;
        Thread.Sleep(_sleep);   // some calsulations and stuff
        if (resource.Capacity != capture)
            throw new SystemException("Something went wrong");
        resource.Capacity -= 1;
    }
}

以及完成这项工作的资源管理器

public class ResourceManager
{
    private readonly List<Consumer> _consumers;
    private readonly Resource _resource;

    public ResourceManager(List<Consumer> consumers)
    {
        _consumers = consumers;
        _resource = new Resource();
    }

    public void Process()
    {
        Parallel.For(0, _consumers.Count, i =>
        {
            var consumer = _consumers[i];
            consumer.ConsumeResource(_resource);
        });
    }
}

如您所见,消费者依赖于资源状态。如果您使用以下代码运行此模拟

static void Main(string[] args)
{
    var consumers = new List<Consumer>
    {
        new Consumer(1000),
        new Consumer(900),
        new Consumer(800),
        new Consumer(700),
        new Consumer(600),
    };

    var resourceManager = new ResourceManager(consumers);
    resourceManager.Process();
}

你会看到,当资源容量发生变化时,一切都会中断。

我想不出任何其他的例子,而且它缺少一些细节。

  • 首先,Resource类的实例很多,所以锁定访问 它不会放弃使代码并发的所有努力。
  • 其次,在实际应用中这个问题非常少见,所以我可以 在那里牺牲一点性能。

我猜这个问题可以通过正确放置 locks 来解决,但我没有正确放置它们。

据我了解locks 的概念,它可以防止从不同线程同时调用锁定的代码。将lock 放在Consumer::ConsumeResource 中没有帮助,就像将它放在Resource::Capacity 设置器中一样。我需要在消费者处理资源时以某种方式锁定资源的修改。

我希望我能有效地解释我的问题。这对我来说都是全新的,所以如果需要,我会尽量让事情变得更具体。


经过深思熟虑,我想出了一个有点草率的解决方案。

我决定使用消费者的 id 为消费者锁定资源属性,并手动等待轮到下一个消费者:

public class Resource
{
    private int Capacity { get; set; } = 1000;

    private Guid? _currentConsumer;

    public int GetCapacity(Guid? id)
    {
        while (id.HasValue && _currentConsumer.HasValue && id != _currentConsumer)
        {
            Thread.Sleep(5);
        }

        _currentConsumer = id;
        return Capacity;
    }

    public void SetCapacity(int cap, Guid id)
    {
        if (_currentConsumer.HasValue && id != _currentConsumer)
            return;

        Capacity = cap;
        _currentConsumer = null;
    }
}

public class Consumer
{
    private readonly int _sleep;

    private Guid _id = Guid.NewGuid();

    public Consumer(int sleep)
    {
        _sleep = sleep;
    }

    public void ConsumeResource(Resource resource)
    {
        var capture = resource.GetCapacity(_id);
        Thread.Sleep(_sleep);   // some calsulations and stuff
        if (resource.GetCapacity(_id) != capture)
            throw new SystemException("Something went wrong");
        resource.SetCapacity(resource.GetCapacity(_id) - 1, _id);
    }
}

这种方式按预期工作,但我感觉它也可以用locks 实现。

【问题讨论】:

    标签: c# .net concurrency locking task-parallel-library


    【解决方案1】:

    在对locks 等进行了一些研究之后,我编写了那个小助手类:

    public class ConcurrentAccessProvider<TObject>
    {
        private readonly Func<TObject> _getter;
        private readonly Action<TObject> _setter;
        private readonly object _lock = new object();
    
        public ConcurrentAccessProvider(Func<TObject> getter, Action<TObject> setter)
        {
            _getter = getter;
            _setter = setter;
        }
    
        public TObject Get()
        {
            lock (_lock)
            {
                return _getter();
            }
        }
    
        public void Set(TObject value)
        {
            lock (_lock)
            {
                _setter(value);
            }
        }
    
        public void Access(Action accessAction)
        {
            lock (_lock)
            {
                accessAction();
            }
        }
    }
    

    因此,我重写了 ResourceConsumer 以使其成为线程安全的:

    public class Resource
    {
        public ConcurrentAccessProvider<int> CapacityAccessProvider { get; }
        private int _capacity;
    
        public Resource()
        {
            CapacityAccessProvider = new ConcurrentAccessProvider<int>(() => _capacity, val => _capacity = val);
        }
    
        public int Capacity
        {
            get => CapacityAccessProvider.Get();
            set => CapacityAccessProvider.Set(value);
        }
    }
    
    public class Consumer
    {
        private readonly int _sleep;
    
        public Consumer(int sleep)
        {
            _sleep = sleep;
        }
    
        public void ConsumeResource(Resource resource)
        {
            resource.CapacityAccessProvider.Access(() =>
            {
                var capture = resource.Capacity;
                Thread.Sleep(_sleep);   // some calsulations and stuff
                if (resource.Capacity != capture)
                    throw new SystemException("Something went wrong");
                resource.Capacity -= 1;
    
                Console.WriteLine(resource.Capacity);
            });
        }
    }
    

    在提供的示例中,这些操作有效地扼杀了所有可能的并发利润,但这是因为只有一个 Resource 实例。在现实世界的应用程序中,当有数千个资源并且只有几个冲突案例时,这将很好。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2019-08-23
      • 2016-07-26
      • 1970-01-01
      • 2020-02-05
      • 1970-01-01
      • 2014-11-30
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多