From d8012c545673d1ce0013579ff3bfa623306209f4 Mon Sep 17 00:00:00 2001 From: Georgii Borovinskikh <117642191+georgii-borovinskikh-sonarsource@users.noreply.github.com> Date: Mon, 19 Feb 2024 16:29:59 +0100 Subject: [PATCH] Add IAsyncLock implementation (#5234) Fixes #5232 --- .../AsyncLockFactoryTests.cs | 48 ++++++++++++ .../AsyncLockTests.cs | 58 ++++++++++++++ src/Infrastructure.VS/AsyncLock.cs | 77 +++++++++++++++++++ 3 files changed, 183 insertions(+) create mode 100644 src/Infrastructure.VS.UnitTests/AsyncLockFactoryTests.cs create mode 100644 src/Infrastructure.VS.UnitTests/AsyncLockTests.cs create mode 100644 src/Infrastructure.VS/AsyncLock.cs diff --git a/src/Infrastructure.VS.UnitTests/AsyncLockFactoryTests.cs b/src/Infrastructure.VS.UnitTests/AsyncLockFactoryTests.cs new file mode 100644 index 0000000000..81b0a8903b --- /dev/null +++ b/src/Infrastructure.VS.UnitTests/AsyncLockFactoryTests.cs @@ -0,0 +1,48 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using FluentAssertions; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using SonarLint.VisualStudio.Core.Synchronization; +using SonarLint.VisualStudio.TestInfrastructure; + +namespace SonarLint.VisualStudio.Infrastructure.VS.UnitTests; + +[TestClass] +public class AsyncLockFactoryTests +{ + [TestMethod] + public void MefCtor_CheckIsExported() + { + MefTestHelpers.CheckTypeCanBeImported(); + } + + [TestMethod] + public void MefCtor_CheckIsSingleton() + { + MefTestHelpers.CheckIsSingletonMefComponent(); + } + + [TestMethod] + public void Create_ReturnsValue() + { + new AsyncLockFactory().Create().Should().NotBeNull(); + } +} diff --git a/src/Infrastructure.VS.UnitTests/AsyncLockTests.cs b/src/Infrastructure.VS.UnitTests/AsyncLockTests.cs new file mode 100644 index 0000000000..3b3703198a --- /dev/null +++ b/src/Infrastructure.VS.UnitTests/AsyncLockTests.cs @@ -0,0 +1,58 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using SonarLint.VisualStudio.Core.Synchronization; + +namespace SonarLint.VisualStudio.Infrastructure.VS.UnitTests; + +[TestClass] +public class AsyncLockTests +{ + [TestMethod] + public async Task SmokeTest() + { + var testSubject = new AsyncLock(); + + var numbers = new List { 0 }; + var threadNumbers = new List(); + + await Task.WhenAll(Enumerable.Range(0, 10).Select(number => Task.Run(() => AddIncrement(numbers, threadNumbers, number, testSubject)))); + + threadNumbers.Should().NotBeAscendingInOrder(); // checks multiple threads actually ran in parallel and not in sequence + numbers.Should().BeInAscendingOrder(); // checks threads were correctly synchronized + } + + private static async Task AddIncrement(List numbers, List threadNumbers, int assignedThreadNumber, IAsyncLock asyncLock) + { + for (var i = 0; i < 10000; i++) + { + using (await asyncLock.AcquireAsync()) + { + threadNumbers.Add(assignedThreadNumber); + numbers.Add(numbers[numbers.Count - 1] + 1); + } + } + } +} diff --git a/src/Infrastructure.VS/AsyncLock.cs b/src/Infrastructure.VS/AsyncLock.cs new file mode 100644 index 0000000000..8c207b55e2 --- /dev/null +++ b/src/Infrastructure.VS/AsyncLock.cs @@ -0,0 +1,77 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.ComponentModel.Composition; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; +using SonarLint.VisualStudio.Core.Synchronization; + +namespace SonarLint.VisualStudio.Infrastructure.VS; + + +[Export(typeof(IAsyncLockFactory))] +[PartCreationPolicy(CreationPolicy.Shared)] +internal class AsyncLockFactory : IAsyncLockFactory +{ + public IAsyncLock Create() + { + return new AsyncLock(); + } +} + +[ExcludeFromCodeCoverage] +internal sealed class AsyncLock : IAsyncLock +{ + private readonly SemaphoreSlim semaphoreSlim = new (1, 1); + + public IReleaseAsyncLock Acquire() + { + semaphoreSlim.Wait(); + + return new AsyncLockToken(this); + } + + public async Task AcquireAsync() + { + await semaphoreSlim.WaitAsync(); + + return new AsyncLockToken(this); + } + + private void Release() => semaphoreSlim.Release(); + + public void Dispose() => semaphoreSlim.Dispose(); + + private sealed class AsyncLockToken : IReleaseAsyncLock + { + private readonly AsyncLock asyncLock; + + public AsyncLockToken(AsyncLock asyncLock) + { + this.asyncLock = asyncLock; + } + + public void Dispose() + { + asyncLock.Release(); + } + } +}