【问题标题】:Read and write same memory from different threads从不同的线程读取和写入相同的内存
【发布时间】:2015-04-08 15:56:42
【问题描述】:

我有一个异步发送请求的简单类。

public class MyClass
{
    private readonly ISender _sender;

    public MyClass(ISender sender)
    {
        _sender = sender;
    }

    public Task<string> SendAsync(string input, CancellationToken cancellationToken)
    {
        return _sender.SendAsync(input, cancellationToken);
    }
}

public interface ISender
{
    Task<string> SendAsync(string input, CancellationToken cancellationToken);
}

一切看起来都很简单,直到满足以下要求:_sender 可以在运行时更改。

MyClass 的新实现:

public class MyClass
{
    private readonly ISender _sender;

    public MyClass(ISender sender)
    {
        _sender = sender;
    }

    public Task<string> SendAsync(string input, CancellationToken cancellationToken)
    {
        return _sender.SendAsync(input, cancellationToken);
    }

    public void SenderChanged(object unused, SenderEventArgs e)
    {
        ISender previous = Interlocked.Exchange(ref _sender, SenderFactory.Create(e.NewSenderConfig));
        previous.Dispose();
    }
}

显然这段代码不是线程安全的。我需要在 SendAsyncSenderChanged 中引入 lock 以确保 _sender 始终是最新的对象。 但是我希望 SenderChanged 每天被调用一次,假设 SendAsync(读取 _sender 对象)被调用 10000/秒。
Lock 和上下文切换会破坏这段代码的性能。

有没有办法通过低级锁定来处理这个问题?或者知道上述要求,您将如何解决这个问题?

【问题讨论】:

  • 请注意,无竞争锁的成本非常很小。即使每秒调用 10,000 次,考虑到锁在被占用时(大约)始终是空闲的,这很可能不会成为问题。
  • 您好 Servy,SendAsync 将从多个线程调用,每个线程必须等待获取锁。这会导致上下文切换。
  • 你能不能不让每个线程都有自己的发件人,并且他们不都试图共享一个?我不希望它的发送操作首先被多个线程调用是安全的。
  • 当新的发件人进来时,您以前的发件人的预期用例是什么?您的代码表明它将被处置,但如果它处于异步操作的过程中,那么 应该 发生什么?它应该完成、中止等吗?
  • 因为听起来你可以通过改变设计来解决这个问题。您需要一个操作队列,并且您的发件人正在每个工作单元对该队列进行操作。然后,当新的发件人进来时,您只需阻止旧发件人将工作排入本地队列(我认为使用标记变量),然后当您的本地队列从旧发件人清空时,无需等待即可将其处理掉。编辑:一种生产者/消费者模式

标签: c# .net multithreading async-await thread-synchronization


【解决方案1】:

通常的方法是使用读写锁,特别是ReaderWriterLockSlim。这是一个类似监视器的锁,针对频繁读取访问和不频繁写入访问进行了优化,并且它支持多个并发读取器和单个写入器,这似乎正是您的用例。

但是,它的成本似乎并不高。我编写了两个测试 - 一个使用 ReaderWriterLockSlim 来正确执行操作,另一个使用您的实现,唯一的变化是处理异常重试循环。就我而言,我更改了 20 次发件人,每 10 秒一次。这比您提议的用例要短得多,但确实可以作为性能差异的估计值。

最后:

  • 读取器/写入器锁能够通过每毫秒 2878 个工作单位。
  • bare with retry 能够通过每毫秒 9940 个工作单位。

“工作单元”正在调用调用Thread.SpinWait(100) 的DoWork 方法。如果您想自己测试,代码会发布在下面。

编辑:

我调整了Thread.SpinWait() 调用以改变锁定时间与“工作”时间之间的平衡。在我的机器上旋转等待大约 900-1000,两种实现运行相同,大约 1000 个工作单位/毫秒。从上面的结果中应该很明显,但我确实只想运行健全性检查。

原来的结果表明,我们可以使用锁每秒处理大约 280 万个请求;至少在我的 4 核 Intel CPU 机器上,“Intel Core 2 Quad CPU Q9650 @ 3.00 GHz”。鉴于您正在争取每秒 10k 个请求,看起来在锁定开始成为 CPU 使用率的很大一部分之前,您已经有了大约一个数量级的余量。


#define USE_READERWRITER

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace TestProject
{
    static class Program
    {
        /// <summary>
        /// The main entry point for the application.
        /// </summary>
        [STAThread]
        static void Main()
        {
            SenderDispatch dispatch = new SenderDispatch();

            List<Worker> workers = new List<Worker>();

            workers.Add( new Worker( dispatch, "A" ) );
            workers.Add( new Worker( dispatch, "B" ) );
            workers.Add( new Worker( dispatch, "C" ) );
            workers.Add( new Worker( dispatch, "D" ) );

            Thread.CurrentThread.Name = "Main thread";
            Process.GetCurrentProcess().PriorityClass = ProcessPriorityClass.High;

            Stopwatch watch = new Stopwatch();

            watch.Start();
            workers.ForEach( x => x.Start() );

            for( int i = 0; i < 20; i++ )
            {
                Thread.Sleep( 10000 );
                dispatch.NewSender();
            }

            Console.WriteLine( "Stopping..." );

            workers.ForEach( x => x.Stop() );
            watch.Stop();

            Console.WriteLine( "Stopped" );

            long sum = workers.Sum( x => x.FinalCount );

            string message = 
                "Sum of worker iterations: " + sum.ToString( "n0" ) + "\r\n" +
                "Total time:               " + ( watch.ElapsedMilliseconds / 1000.0 ).ToString( "0.000" ) + "\r\n" +
                "Iterations/ms:            " + sum / watch.ElapsedMilliseconds;

            MessageBox.Show( message );
        }
    }

    public class Worker
    {
        private SenderDispatch dispatcher;
        private Thread thread;
        private bool working;

        private string workerName;

        public Worker( SenderDispatch dispatcher, string workerName )
        {
            this.dispatcher = dispatcher;
            this.workerName = workerName;

            this.working = false;
        }

        public long FinalCount { get; private set; }

        public void Start()
        {
            this.thread = new Thread( Run );
            this.thread.Name = "Worker " + this.workerName;

            this.working = true;
            this.thread.Start();
        }

        private void Run()
        {
            long state = 0;

            while( this.working )
            {
                this.dispatcher.DoOperation( workerName, state );
                state++;
            }

            this.FinalCount = state;
        }

        public void Stop()
        {
            this.working = false;

            this.thread.Join();
        }
    }

    public class SenderDispatch
    {
        private Sender sender;

        private ReaderWriterLockSlim senderLock;

        public SenderDispatch()
        {
            this.sender = new Sender();
            this.senderLock = new ReaderWriterLockSlim( LockRecursionPolicy.NoRecursion );
        }

        public void DoOperation( string workerName, long value )
        {

#if USE_READERWRITER
            this.senderLock.EnterReadLock();
            try
            {
                this.sender.DoOperation( workerName, value );
            }
            finally
            {
                this.senderLock.ExitReadLock();
            }
#else 
            bool done = false;

            do
            {
                try
                {
                    this.sender.DoOperation( workerName, value );
                    done = true;
                }
                catch (ObjectDisposedException) { }
            }
            while( !done );
#endif

        }

        public void NewSender()
        {
            Sender prevSender;
            Sender newSender;

            newSender = new Sender();

#if USE_READERWRITER
            this.senderLock.EnterWriteLock();
            try
            {
                prevSender = Interlocked.Exchange( ref this.sender, newSender );
            }
            finally
            {
                this.senderLock.ExitWriteLock();
            }
#else
            prevSender = Interlocked.Exchange( ref this.sender, newSender );
            prevSender.Dispose();

#endif
            prevSender.Dispose();

        }
    }

    public class Sender : IDisposable
    {
        private bool disposed;

        public Sender()
        {
            this.disposed = false;
        }

        public void DoOperation( string workerName, long value )
        {
            if( this.disposed )
            {
                throw new ObjectDisposedException( 
                    "Sender",
                    string.Format( "Worker {0} tried to queue work item {1}", workerName, value ) 
                );
            }

            Thread.SpinWait( 100 );
        }

        public void Dispose()
        {
            this.disposed = true;
        }
    }
}

【讨论】:

  • 这是一个很好的答案。谢谢!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-07-15
  • 1970-01-01
  • 2014-05-22
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多