Обеспечение ровно одного SQLite-соединения на поток
Я использую
Microsoft.Data.Sqlite.Core
с участием
SQLitePCLRaw.lib.e_sqlcipher
для доступа к зашифрованной базе данных SQLite. Мне нужно решить несколько проблем:
- Я должен убедиться, что тот же экземпляр
Microsoft.Data.Sqlite.SqliteConnection
не используется несколькими потоками (соединение SQLite не является потокобезопасным); - Открытие зашифрованной базы данных SQLCipher обходится дорого, поэтому я должен ограничить количество таких операций до минимума.
Я придумал решение, но я не уверен, насколько оно надежно, поэтому я надеялся, что кто-то сможет проделать в нем какие-то дыры.
2 ответа
Я столкнулся с тем же осознанием, что и вы, и мне придется сделать что-то подобное. Я не вижу проблем с вашим кодом.
Я попробую немного оптимизировать, используя ConcurrentDictionary. Это должно позволить мне избежать блокировки читателей при обновлении.
Если вы останетесь с Dictionary, вы можете изменить вызов ContainsKey() на TryGetValue(), поскольку документация предполагает, что это может быть более эффективным, если использовать ключ, который не существует (мы часто видим новые потоки?)
На случай, если мимо проезжает кто-то другой, вот мои предварительные исследования по этой проблеме:Microsoft.Data.Sqlite является производным от объектов ADO.NET, например, DbConnection, которые, согласно проекту, не являются потокобезопасными. Разработчики ADO.NET пожертвовали потокобезопасностью на алтарь высокой производительности. По этой причине любой код, использующий что-либо, полученное из ADO.NET, в конечном итоге должен пройти в эту кроличью нору, чтобы убедиться, что не произойдет действительно странных вещей.
Что касается Microsoft.Data.Sqlite, моей проблемой был SqliteCommand.Dispose, который внутренне обнаруживает значение null и вылетает. Это когда фреймворк имеет много параллельных вызовов в разных потоках.
Вы также можете заметить, что сам sqlite имеет многопоточные настройки и полагает, что эта кроличья нора - то, что вам нужно. К сожалению, возиться с этими настройками, какими бы полезными они ни были, не происходит ничего, чтобы изменить дизайн объекта ADO.NET, и, следовательно, проблема остается, пока вы используете Microsoft.Data.Sqlite для доступа к sqlite.
С уважением, что разместили свой код! Надеюсь, он с большим успехом попал в производство :-)
/ Ник
Вот мое решение, пожалуйста, дайте мне знать, если / где я напортачил:
public class SqliteConnectionPool
{
private readonly object lockObject_ = new object();
private readonly string connectionString_;
private readonly Dictionary<Thread, SqliteConnection> pool_;
public SqliteConnectionPool(string connectionString)
{
new SqliteConnectionStringBuilder
{
ConnectionString = connectionString // throws if connection string is invalid
};
connectionString_ = connectionString;
pool_ = new Dictionary<Thread, SqliteConnection>();
}
public SqliteConnection GetConnection()
{
lock (lockObject_)
{
Thread currentThread = Thread.CurrentThread;
// If this thread owns a connection, just retrieve it.
if (pool_.ContainsKey(currentThread))
{
return pool_[currentThread];
}
// Looking for a thread that doesn't need its connection anymore.
(Thread inactiveThread, SqliteConnection availableConnection) = pool_.Where(p => p.Key.ThreadState != ThreadState.Running)
.Select(p => (p.Key, p.Value))
.FirstOrDefault();
// If all existing connections are being used, create a new one.
if (availableConnection is null)
{
var connection = new SqliteConnection(connectionString_);
pool_[currentThread] = connection;
return connection;
}
// Otherwise, just use the existing free connection.
pool_.Remove(inactiveThread);
pool_[currentThread] = availableConnection;
return availableConnection;
}
}
}