From 542017131dcb70532977bd33535cba19228b61a8 Mon Sep 17 00:00:00 2001 From: "Frank A. Krueger" Date: Sun, 24 May 2020 11:21:13 -0700 Subject: [PATCH] Replace synchronous locks with async The goal is to prevent threadpool starvation on high concurrency apps (like web apps). --- src/SQLiteAsync.cs | 83 ++++++++++++++++++++++++++++++---------------- 1 file changed, 54 insertions(+), 29 deletions(-) diff --git a/src/SQLiteAsync.cs b/src/SQLiteAsync.cs index b981d00e..b49380ea 100644 --- a/src/SQLiteAsync.cs +++ b/src/SQLiteAsync.cs @@ -22,6 +22,7 @@ using System; using System.Collections; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; @@ -203,7 +204,7 @@ public SQLiteConnectionWithLock GetConnection () return SQLiteConnectionPool.Shared.GetConnection (_connectionString); } - SQLiteConnectionWithLock GetConnectionAndTransactionLock (out object transactionLock) + SQLiteConnectionWithLock GetConnectionAndTransactionLock (out SemaphoreSlim transactionLock) { return SQLiteConnectionPool.Shared.GetConnectionAndTransactionLock (_connectionString, out transactionLock); } @@ -220,34 +221,38 @@ public Task CloseAsync () Task ReadAsync (Func read) { - return Task.Factory.StartNew (() => { + return Task.Run (async () => { var conn = GetConnection (); - using (conn.Lock ()) { + using (await conn.LockAsync ().ConfigureAwait (false)) { return read (conn); } - }, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); + }); } Task WriteAsync (Func write) { - return Task.Factory.StartNew (() => { + return Task.Run (async () => { var conn = GetConnection (); - using (conn.Lock ()) { + using (await conn.LockAsync ().ConfigureAwait (false)) { return write (conn); } - }, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); + }); } Task TransactAsync (Func transact) { - return Task.Factory.StartNew (() => { + return Task.Run (async () => { var conn = GetConnectionAndTransactionLock (out var transactionLock); - lock (transactionLock) { - using (conn.Lock ()) { + await transactionLock.WaitAsync ().ConfigureAwait (false); + try { + using (await conn.LockAsync ().ConfigureAwait (false)) { return transact (conn); } } - }, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); + finally { + transactionLock.Release (); + } + }); } /// @@ -1191,22 +1196,22 @@ public AsyncTableQuery (TableQuery innerQuery) Task ReadAsync (Func read) { - return Task.Factory.StartNew (() => { + return Task.Run (async () => { var conn = (SQLiteConnectionWithLock)_innerQuery.Connection; - using (conn.Lock ()) { + using (await conn.LockAsync ().ConfigureAwait (false)) { return read (conn); } - }, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); + }); } Task WriteAsync (Func write) { - return Task.Factory.StartNew (() => { + return Task.Run (async () => { var conn = (SQLiteConnectionWithLock)_innerQuery.Connection; - using (conn.Lock ()) { + using (await conn.LockAsync ().ConfigureAwait (false)) { return write (conn); } - }, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); + }); } /// @@ -1362,7 +1367,7 @@ class Entry public SQLiteConnectionString ConnectionString { get; } - public object TransactionLock { get; } = new object (); + public SemaphoreSlim TransactionLock { get; } = new SemaphoreSlim (1); public Entry (SQLiteConnectionString connectionString) { @@ -1386,7 +1391,7 @@ public void Close () } readonly Dictionary _entries = new Dictionary (); - readonly object _entriesLock = new object (); + readonly object _entriesLock = new object(); static readonly SQLiteConnectionPool _shared = new SQLiteConnectionPool (); @@ -1404,11 +1409,11 @@ public SQLiteConnectionWithLock GetConnection (SQLiteConnectionString connection return GetConnectionAndTransactionLock (connectionString, out var _); } - public SQLiteConnectionWithLock GetConnectionAndTransactionLock (SQLiteConnectionString connectionString, out object transactionLock) + public SQLiteConnectionWithLock GetConnectionAndTransactionLock (SQLiteConnectionString connectionString, out SemaphoreSlim transactionLock) { - var key = connectionString.UniqueKey; - Entry entry; lock (_entriesLock) { + var key = connectionString.UniqueKey; + Entry entry; if (!_entries.TryGetValue (key, out entry)) { // The opens the database while we're locked // This is to ensure another thread doesn't get an unopened database @@ -1456,7 +1461,8 @@ public void Reset () /// public class SQLiteConnectionWithLock : SQLiteConnection { - readonly object _lockPoint = new object (); + readonly SemaphoreSlim _asyncLock = + new SemaphoreSlim (1); /// /// Initializes a new instance of the class. @@ -1478,24 +1484,43 @@ public SQLiteConnectionWithLock (SQLiteConnectionString connectionString) /// on the returned object. /// /// The lock. + [Obsolete("Please use LockAsync to prevent threadpool starvation")] public IDisposable Lock () { - return SkipLock ? (IDisposable)new FakeLockWrapper() : new LockWrapper (_lockPoint); + return LockAsync ().Result; + } + + /// + /// Asynchronously lock the database to serialize access to it. + /// To unlock it, call Dispose on the returned object. + /// + /// The lock. + public Task LockAsync () + { + return SkipLock ? + Task.FromResult ((IDisposable)new FakeLockWrapper ()) : + LockWrapper.CreateAsync (_asyncLock); } class LockWrapper : IDisposable { - object _lockPoint; + SemaphoreSlim asyncLock; + + LockWrapper (SemaphoreSlim asyncLock) + { + this.asyncLock = asyncLock; + } - public LockWrapper (object lockPoint) + public static async Task CreateAsync (SemaphoreSlim asyncLock) { - _lockPoint = lockPoint; - Monitor.Enter (_lockPoint); + var wrapper = new LockWrapper (asyncLock); + await asyncLock.WaitAsync ().ConfigureAwait (false); + return wrapper; } public void Dispose () { - Monitor.Exit (_lockPoint); + asyncLock.Release (); } } class FakeLockWrapper : IDisposable