C# - 使用 XUnit 测试集合的并发性会提供不同的结果

问题描述 投票:0回答:1

我有一个名为 Client manager 的并发类,它使用哈希集和 ReadWriterLock 通过其 Guid 注册我的“客户端”:

internal class ClientManager : IClientManager
{
    private readonly HashSet<Guid> _clients = new HashSet<Guid>();
    private readonly ReaderWriterLock _lock = new ReaderWriterLock();

    public IEnumerable<Guid> Clients
    {
        get
        {
            try
            {
                _lock.AcquireReaderLock(100.Milliseconds());
                var result = _clients.ToList();
                return result;
            }
            finally
            {
                _lock.ReleaseReaderLock();
            }
        }
    }

    public void AddClient(Guid clientId, string login, string appName)
    {
        try
        {
            _lock.AcquireWriterLock(100.Milliseconds());
            _clients.Add(clientId);
        }
        finally
        {
            _lock.ReleaseWriterLock();
        }
    }

    public void RemoveClient(Guid clientId)
    {
        try
        {
            _lock.AcquireWriterLock(100.Milliseconds());
            _clients.Remove(clientId);
        }
        finally
        {
            _lock.ReleaseWriterLock();
        }
    }
}

我创建了一个单元测试,它使用三个任务来访问哈希集/添加客户端/删除客户端。在单元测试结束时,我希望哈希集中剩下 0 个条目:

    [Fact]
    public void ClientManager_Should_Handle_Concurent_Operations()
    {
        var clientManager = new ClientManager();
        var isDoneAdding = false;
        var addTask = new Task(() =>
        {
            for (var i = 0; i < 10000; i++)
            {
                //Adding client should enter write lock
                clientManager.AddClient(Guid.NewGuid(), $"login_{i}", $"appName_{i}");
            }

            isDoneAdding = true;
        });

        var accessTask = new Task(() =>
        {
            for (var i = 0; i < 10000; i++)
            {
              //Getting clients should enter read lock
               var clients = clientManager.Clients;
            }
        });

        var removeTask = new Task(() =>
        {

            for (var i = 0; i < 10000; i++)
            {
                var existingGuid = clientManager.Clients.FirstOrDefault();
               if (existingGuid != default)
               {
                   //Removing client should enter write lock
                   clientManager.RemoveClient(existingGuid);
               }
            }

            //refactor that
            while (clientManager.Clients.Count() > 0 && isDoneAdding)
            {
                var existingGuid = clientManager.Clients.FirstOrDefault();
                if (existingGuid != default)
                {
                    clientManager.RemoveClient(existingGuid);
                }
            }
        });

        accessTask.Start();
        addTask.Start();
        removeTask.Start();

        Task.WaitAll(accessTask, addTask, removeTask);
        Assert.True(clientManager.Clients.Count() == 0);
    }

单元测试总是失败: 测试失败

我更改了并发字典的哈希集和锁定系统,如下所示:

internal class ClientManager : IClientManager
{
    //We don't need the value here so just put a byte to reduce memory footprint
    private readonly ConcurrentDictionary<Guid, byte> _clients = new ConcurrentDictionary<Guid, byte>();
    private readonly ReaderWriterLock _lock = new ReaderWriterLock();

    public IEnumerable<Guid> Clients
    {
        get
        {
            return _clients.Keys;
        }
    }

    public void AddClient(Guid clientId, string login, string appName)
    {
        _clients.TryAdd(clientId, default);
    }

    public void RemoveClient(Guid clientId)
    {
        _clients.Remove(clientId, out _);
    }
}

单元测试连续成功320次:测试成功

我不明白哈希集锁定系统缺少什么才能正常工作,有人可以帮助我了解发生了什么吗?

感谢您的阅读!

c# multithreading unit-testing concurrency xunit
1个回答
0
投票

存在很多问题,但此特定测试未通过的根本原因是您标记为重构的代码:

while (clientManager.Clients.Count() > 0 && isDoneAdding) {
    var existingGuid = clientManager.Clients.FirstOrDefault();
    if (existingGuid != default) {
        clientManager.RemoveClient(existingGuid);
    }
}

需要写成这样:

//still refactor that
while (!isDoneAdding) ; // spinwait for the adding to end 
while (clientManager.Clients.Count() > 0 ) {
    var existingGuid = clientManager.Clients.FirstOrDefault();
    if (existingGuid != default) {
        clientManager.RemoveClient(existingGuid);
    }
}

其他问题:

  1. ReaderWriterLock
    已弃用,取而代之的是
    ReaderWriterLockSlim
  2. 您正在
    IEnumerable<Guid> Clients
    中返回一个副本 (ToList),它将很快变得过时。
  3. 您的测试应该关注测量同时发生的不确定数量的写入/访问,而不是我们可以成功耗尽结构。

如果不是出于教育目的,我会研究在某个地方找到经过实战测试的 ConcurrentHashSet 实现。

© www.soinside.com 2019 - 2024. All rights reserved.