Lock-free IOQueue (#6154)
Second half of https://github.com/aspnet/AspNetCore/pull/4060
This commit is contained in:
parent
394ef0ab93
commit
639d290b19
|
|
@ -10,23 +10,18 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.Internal
|
||||||
{
|
{
|
||||||
public class IOQueue : PipeScheduler, IThreadPoolWorkItem
|
public class IOQueue : PipeScheduler, IThreadPoolWorkItem
|
||||||
{
|
{
|
||||||
private readonly object _workSync = new object();
|
|
||||||
private readonly ConcurrentQueue<Work> _workItems = new ConcurrentQueue<Work>();
|
private readonly ConcurrentQueue<Work> _workItems = new ConcurrentQueue<Work>();
|
||||||
private bool _doingWork;
|
private int _doingWork;
|
||||||
|
|
||||||
public override void Schedule(Action<object> action, object state)
|
public override void Schedule(Action<object> action, object state)
|
||||||
{
|
{
|
||||||
var work = new Work(action, state);
|
_workItems.Enqueue(new Work(action, state));
|
||||||
|
|
||||||
_workItems.Enqueue(work);
|
// Set working if it wasn't (via atomic Interlocked).
|
||||||
|
if (Interlocked.CompareExchange(ref _doingWork, 1, 0) == 0)
|
||||||
lock (_workSync)
|
|
||||||
{
|
{
|
||||||
if (!_doingWork)
|
// Wasn't working, schedule.
|
||||||
{
|
System.Threading.ThreadPool.UnsafeQueueUserWorkItem(this, preferLocal: false);
|
||||||
System.Threading.ThreadPool.UnsafeQueueUserWorkItem(this, preferLocal: false);
|
|
||||||
_doingWork = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -39,14 +34,31 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.Internal
|
||||||
item.Callback(item.State);
|
item.Callback(item.State);
|
||||||
}
|
}
|
||||||
|
|
||||||
lock (_workSync)
|
// All work done.
|
||||||
|
|
||||||
|
// Set _doingWork (0 == false) prior to checking IsEmpty to catch any missed work in interim.
|
||||||
|
// This doesn't need to be volatile due to the following barrier (i.e. it is volatile).
|
||||||
|
_doingWork = 0;
|
||||||
|
|
||||||
|
// Ensure _doingWork is written before IsEmpty is read.
|
||||||
|
// As they are two different memory locations, we insert a barrier to guarantee ordering.
|
||||||
|
Thread.MemoryBarrier();
|
||||||
|
|
||||||
|
// Check if there is work to do
|
||||||
|
if (_workItems.IsEmpty)
|
||||||
{
|
{
|
||||||
if (_workItems.IsEmpty)
|
// Nothing to do, exit.
|
||||||
{
|
break;
|
||||||
_doingWork = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Is work, can we set it as active again (via atomic Interlocked), prior to scheduling?
|
||||||
|
if (Interlocked.Exchange(ref _doingWork, 1) == 1)
|
||||||
|
{
|
||||||
|
// Execute has been rescheduled already, exit.
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Is work, wasn't already scheduled so continue loop.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets
|
||||||
{
|
{
|
||||||
internal sealed class SocketTransport : ITransport
|
internal sealed class SocketTransport : ITransport
|
||||||
{
|
{
|
||||||
private static readonly PipeScheduler[] ThreadPoolSchedulerArray = new PipeScheduler[] { PipeScheduler.ThreadPool };
|
|
||||||
|
|
||||||
private readonly MemoryPool<byte> _memoryPool;
|
private readonly MemoryPool<byte> _memoryPool;
|
||||||
private readonly IEndPointInformation _endPointInformation;
|
private readonly IEndPointInformation _endPointInformation;
|
||||||
private readonly IConnectionDispatcher _dispatcher;
|
private readonly IConnectionDispatcher _dispatcher;
|
||||||
|
|
@ -65,8 +63,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_numSchedulers = ThreadPoolSchedulerArray.Length;
|
var directScheduler = new PipeScheduler[] { PipeScheduler.ThreadPool };
|
||||||
_schedulers = ThreadPoolSchedulerArray;
|
_numSchedulers = directScheduler.Length;
|
||||||
|
_schedulers = directScheduler;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,119 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.IO.Pipelines;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using BenchmarkDotNet.Attributes;
|
||||||
|
using Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.Internal;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Server.Kestrel.Performance
|
||||||
|
{
|
||||||
|
public class SchedulerBenchmark
|
||||||
|
{
|
||||||
|
private const int InnerLoopCount = 1024;
|
||||||
|
private const int OuterLoopCount = 64;
|
||||||
|
private const int OperationsPerInvoke = InnerLoopCount * OuterLoopCount;
|
||||||
|
|
||||||
|
private readonly static int IOQueueCount = Math.Min(Environment.ProcessorCount, 16);
|
||||||
|
|
||||||
|
private PipeScheduler[] _ioQueueSchedulers;
|
||||||
|
private PipeScheduler[] _threadPoolSchedulers;
|
||||||
|
private PipeScheduler[] _inlineSchedulers;
|
||||||
|
|
||||||
|
private SemaphoreSlim _semaphore = new SemaphoreSlim(0);
|
||||||
|
private int _totalToReport;
|
||||||
|
private PaddedInteger[] _counters = new PaddedInteger[OuterLoopCount];
|
||||||
|
|
||||||
|
private Func<int, ParallelLoopState, PipeScheduler[], PipeScheduler[]> _parallelAction;
|
||||||
|
private Action<object> _action;
|
||||||
|
|
||||||
|
[GlobalSetup]
|
||||||
|
public void Setup()
|
||||||
|
{
|
||||||
|
_parallelAction = ParallelBody;
|
||||||
|
_action = new Action<object>(ScheduledAction);
|
||||||
|
|
||||||
|
_inlineSchedulers = new PipeScheduler[IOQueueCount];
|
||||||
|
for (var i = 0; i < _inlineSchedulers.Length; i++)
|
||||||
|
{
|
||||||
|
_inlineSchedulers[i] = PipeScheduler.Inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
_threadPoolSchedulers = new PipeScheduler[IOQueueCount];
|
||||||
|
for (var i = 0; i < _threadPoolSchedulers.Length; i++)
|
||||||
|
{
|
||||||
|
_threadPoolSchedulers[i] = PipeScheduler.ThreadPool;
|
||||||
|
}
|
||||||
|
|
||||||
|
_ioQueueSchedulers = new PipeScheduler[IOQueueCount];
|
||||||
|
for (var i = 0; i < _ioQueueSchedulers.Length; i++)
|
||||||
|
{
|
||||||
|
_ioQueueSchedulers[i] = new IOQueue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[IterationSetup]
|
||||||
|
public void IterationSetup()
|
||||||
|
{
|
||||||
|
_totalToReport = OuterLoopCount;
|
||||||
|
|
||||||
|
for (var i = 0; i < _counters.Length; i++)
|
||||||
|
{
|
||||||
|
_counters[i].Remaining = InnerLoopCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Benchmark(OperationsPerInvoke = OperationsPerInvoke, Baseline = true)]
|
||||||
|
public void ThreadPoolScheduler() => Schedule(_threadPoolSchedulers);
|
||||||
|
|
||||||
|
[Benchmark(OperationsPerInvoke = OperationsPerInvoke)]
|
||||||
|
public void IOQueueScheduler() => Schedule(_ioQueueSchedulers);
|
||||||
|
|
||||||
|
[Benchmark(OperationsPerInvoke = OperationsPerInvoke)]
|
||||||
|
public void InlineScheduler() => Schedule(_inlineSchedulers);
|
||||||
|
|
||||||
|
private void Schedule(PipeScheduler[] schedulers)
|
||||||
|
{
|
||||||
|
Parallel.For(0, OuterLoopCount, () => schedulers, _parallelAction, (s) => { });
|
||||||
|
|
||||||
|
while (_totalToReport > 0)
|
||||||
|
{
|
||||||
|
_semaphore.Wait();
|
||||||
|
_totalToReport--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ScheduledAction(object o)
|
||||||
|
{
|
||||||
|
var counter = (int)o;
|
||||||
|
var result = Interlocked.Decrement(ref _counters[counter].Remaining);
|
||||||
|
if (result == 0)
|
||||||
|
{
|
||||||
|
_semaphore.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private PipeScheduler[] ParallelBody(int i, ParallelLoopState state, PipeScheduler[] schedulers)
|
||||||
|
{
|
||||||
|
PipeScheduler pipeScheduler = schedulers[i % schedulers.Length];
|
||||||
|
object counter = i;
|
||||||
|
for (var t = 0; t < InnerLoopCount; t++)
|
||||||
|
{
|
||||||
|
pipeScheduler.Schedule(_action, counter);
|
||||||
|
}
|
||||||
|
|
||||||
|
return schedulers;
|
||||||
|
}
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Explicit, Size = 128)]
|
||||||
|
private struct PaddedInteger
|
||||||
|
{
|
||||||
|
// Padded to avoid false sharing
|
||||||
|
[FieldOffset(64)]
|
||||||
|
public int Remaining;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue