Как обеспечить потокобезопасность сервисов в DI контейнере (в частности Singleton)

Написал небольшое приложение с использованием DI для консоли. Приложение измменяет и выводит состояние синглтона из DI контейнера.

Program.cs

var serviceCollection = new ServiceCollection();

serviceCollection.AddSingleton<ISingletonTest, SingletonTest>();
serviceCollection.AddSingleton<App>();

var serviceProvider = serviceCollection.BuildServiceProvider();

var app = serviceProvider?.GetService<App>();
using (var scope = serviceProvider?.CreateScope())
{
    app?.RUN();
}

I/SingletonTest.cs

public interface ISingletonTest
{
    public int COUNT { get; }
    public void increase();
}
class SingletonTest : ISingletonTest
{
    private int _count = 0;
    public int COUNT { get => _count; }

    public void increase()
    {
        _count++;
    }
}

App.cs

internal class App
{
    private readonly ISingletonTest _singletonTest;
    public App(ISingletonTest singletonTest)
    {
        _singletonTest = singletonTest;
    }

    public void RUN()
    {
        List<Thread> lst = new List<Thread>();
        for (int i = 0; i < 100; i++)
        {
            lst.Add(new Thread(() =>
            {
                _singletonTest.increase();
                Console.WriteLine("In thread:" + Environment.CurrentManagedThreadId + " value of _singletonTest.COUNT = " + _singletonTest.COUNT);
            }));
        }

        foreach (var item in lst)
        {
            item.Start();
        }
        Console.ReadLine();
    }
}

И вывод программы вот такой

введите сюда описание изображения

и еще введите сюда описание изображения

Хочу разобраться почему так происходит, ведь ServiceCollection использует потокобезопасную коллекцию на основе IList. И по сути дела данные могут выводиться не по порядку, но их дублирования точно не должно быть. Да! Вывод иногда нормальный, но и бывает повторение чтения по несколько раз. Помогите разобраться с вопросом потокобезопасности в DI контейнере.


Ответы (1 шт):

Автор решения: aepot

Проблема здесь не в DI, а в том что сам класс реализован не потокобезопасно.

Вот например так можно.

public interface ISingletonTest
{
    public int Increase();
}

class SingletonTest : ISingletonTest
{
    private int _count = 0;

    public int Increase()
    {
        return Interlocked.Increment(ref _count);
    }
}

И вот так тестировать. Немного упростил код, но логика не поменялась.

internal class App
{
    private readonly ISingletonTest _singletonTest;

    public App(ISingletonTest singletonTest)
    {
        _singletonTest = singletonTest;
    }

    public void RUN()
    {
        for (int i = 0; i < 100; i++)
        {
            new Thread(() =>
            {
                int count = _singletonTest.Increase();
                Console.WriteLine("In thread:" + Environment.CurrentManagedThreadId + " value of _singletonTest.COUNT = " + count);
            }).Start();
        }


        Console.ReadLine();
    }
}

Вывод в консоль такой

In thread:15 value of _singletonTest.COUNT = 3
In thread:13 value of _singletonTest.COUNT = 1
In thread:14 value of _singletonTest.COUNT = 2
In thread:16 value of _singletonTest.COUNT = 4
In thread:17 value of _singletonTest.COUNT = 5
In thread:18 value of _singletonTest.COUNT = 6
In thread:19 value of _singletonTest.COUNT = 7
In thread:20 value of _singletonTest.COUNT = 8
In thread:21 value of _singletonTest.COUNT = 9
In thread:22 value of _singletonTest.COUNT = 10
In thread:23 value of _singletonTest.COUNT = 11
In thread:24 value of _singletonTest.COUNT = 12
In thread:25 value of _singletonTest.COUNT = 13
In thread:26 value of _singletonTest.COUNT = 14
In thread:27 value of _singletonTest.COUNT = 15
In thread:28 value of _singletonTest.COUNT = 16
In thread:29 value of _singletonTest.COUNT = 17
In thread:30 value of _singletonTest.COUNT = 18
In thread:31 value of _singletonTest.COUNT = 19
In thread:32 value of _singletonTest.COUNT = 20
In thread:33 value of _singletonTest.COUNT = 21
In thread:34 value of _singletonTest.COUNT = 22
In thread:35 value of _singletonTest.COUNT = 23
In thread:36 value of _singletonTest.COUNT = 24
In thread:37 value of _singletonTest.COUNT = 25
In thread:38 value of _singletonTest.COUNT = 26
In thread:39 value of _singletonTest.COUNT = 27
In thread:40 value of _singletonTest.COUNT = 28
In thread:41 value of _singletonTest.COUNT = 29
In thread:42 value of _singletonTest.COUNT = 30
In thread:43 value of _singletonTest.COUNT = 31
In thread:44 value of _singletonTest.COUNT = 32
In thread:45 value of _singletonTest.COUNT = 33
In thread:46 value of _singletonTest.COUNT = 34
In thread:47 value of _singletonTest.COUNT = 35
In thread:48 value of _singletonTest.COUNT = 36
In thread:49 value of _singletonTest.COUNT = 37
In thread:50 value of _singletonTest.COUNT = 38
In thread:51 value of _singletonTest.COUNT = 39
In thread:52 value of _singletonTest.COUNT = 40
In thread:53 value of _singletonTest.COUNT = 41
In thread:54 value of _singletonTest.COUNT = 42
In thread:55 value of _singletonTest.COUNT = 43
In thread:56 value of _singletonTest.COUNT = 44
In thread:57 value of _singletonTest.COUNT = 45
In thread:58 value of _singletonTest.COUNT = 46
In thread:59 value of _singletonTest.COUNT = 47
In thread:60 value of _singletonTest.COUNT = 48
In thread:61 value of _singletonTest.COUNT = 49
In thread:62 value of _singletonTest.COUNT = 50
In thread:63 value of _singletonTest.COUNT = 51
In thread:64 value of _singletonTest.COUNT = 52
In thread:65 value of _singletonTest.COUNT = 53
In thread:66 value of _singletonTest.COUNT = 54
In thread:67 value of _singletonTest.COUNT = 55
In thread:68 value of _singletonTest.COUNT = 56
In thread:69 value of _singletonTest.COUNT = 57
In thread:71 value of _singletonTest.COUNT = 58
In thread:70 value of _singletonTest.COUNT = 59
In thread:72 value of _singletonTest.COUNT = 60
In thread:74 value of _singletonTest.COUNT = 61
In thread:73 value of _singletonTest.COUNT = 62
In thread:75 value of _singletonTest.COUNT = 63
In thread:76 value of _singletonTest.COUNT = 64
In thread:77 value of _singletonTest.COUNT = 65
In thread:78 value of _singletonTest.COUNT = 66
In thread:79 value of _singletonTest.COUNT = 67
In thread:80 value of _singletonTest.COUNT = 68
In thread:81 value of _singletonTest.COUNT = 69
In thread:82 value of _singletonTest.COUNT = 70
In thread:83 value of _singletonTest.COUNT = 71
In thread:84 value of _singletonTest.COUNT = 72
In thread:86 value of _singletonTest.COUNT = 73
In thread:85 value of _singletonTest.COUNT = 74
In thread:87 value of _singletonTest.COUNT = 75
In thread:88 value of _singletonTest.COUNT = 76
In thread:89 value of _singletonTest.COUNT = 77
In thread:90 value of _singletonTest.COUNT = 78
In thread:91 value of _singletonTest.COUNT = 79
In thread:92 value of _singletonTest.COUNT = 80
In thread:93 value of _singletonTest.COUNT = 81
In thread:94 value of _singletonTest.COUNT = 82
In thread:95 value of _singletonTest.COUNT = 83
In thread:96 value of _singletonTest.COUNT = 84
In thread:97 value of _singletonTest.COUNT = 85
In thread:98 value of _singletonTest.COUNT = 86
In thread:99 value of _singletonTest.COUNT = 87
In thread:100 value of _singletonTest.COUNT = 88
In thread:101 value of _singletonTest.COUNT = 89
In thread:102 value of _singletonTest.COUNT = 90
In thread:103 value of _singletonTest.COUNT = 91
In thread:104 value of _singletonTest.COUNT = 92
In thread:105 value of _singletonTest.COUNT = 93
In thread:106 value of _singletonTest.COUNT = 94
In thread:107 value of _singletonTest.COUNT = 95
In thread:108 value of _singletonTest.COUNT = 96
In thread:109 value of _singletonTest.COUNT = 97
In thread:110 value of _singletonTest.COUNT = 98
In thread:111 value of _singletonTest.COUNT = 99
In thread:112 value of _singletonTest.COUNT = 100

Порядок выполнения потоков не гарантирован как-бы изначально, но повторяющихся чисел больше нет.

А вообще вы не синглтон проверяете, а просто потокобезопасность класса в самом классе. Я бы предложил проверять так.

internal class App
{
    private readonly ISingletonTest _singletonTest;
    public App(ISingletonTest singletonTest)
    {
        _singletonTest = singletonTest;
    }

    public void Run()
    {
        int count = _singletonTest.Increase();
        Console.WriteLine("In thread:" + Environment.CurrentManagedThreadId + " value of _singletonTest count = " + count);
    }
}
var serviceCollection = new ServiceCollection();

serviceCollection.AddSingleton<ISingletonTest, SingletonTest>();
serviceCollection.AddSingleton<App>();

var serviceProvider = serviceCollection.BuildServiceProvider();

for (int i = 0; i < 100; i++)
{
    new Thread(() =>
    {
        var app = serviceProvider?.GetService<App>();
        using (var scope = serviceProvider?.CreateScope())
        {
            app?.Run();
        }
    }).Start();
}
Console.ReadLine();

Вывод в консоль такой же за исключением того что числа больше перемешаны местами, показывать не буду здесь.

То есть теперь если вы синглтон уберёте.

serviceCollection.AddTransient<ISingletonTest, SingletonTest>();
serviceCollection.AddTransient<App>();

То в консоль выведутся только единички. А в вашем тесте AddTransient ничего не поменяет, потому что App как и её зависимость резолвятся только однократно. При чём, если вы любоё AddTransient поменяете на AddSingleton, то будут числа от 2 до 100, так как в одном случае у вас будет cинглтон App, который отрезолвит свою зависимость один раз, ну потому что он один. Во втором случае все экземпляры App будут резолвить один и тот же синглтон SingletonTest. Наверное это то что вы изначально задумали, и есть, вот так правильно будет тестить, что ISingletonTest - действительно синглтон:

serviceCollection.AddSingleton<ISingletonTest, SingletonTest>();
serviceCollection.AddTransient<App>();
→ Ссылка