I'm faced with the same realization as you and will have to do something similar. I see no issues with your code.
I will try to optimize a bit by using ConcurrentDictionary. It should allow me to avoid locking readers while updating.
If you stay with Dictionary, you may want to change the ContainsKey() call to a TryGetValue() as the documentation suggests it may be more efficient if using a key that does not exist (we would often see new threads?)
In case someone else drives by, here is my background research on the issue:
Microsoft.Data.Sqlite derives from ADO.NET objects, e.g. DbConnection that, per design, are not thread safe. The designers of ADO.NET sacrificed thread safy on the altar of high performance. For this reason any code using anything derived from ADO.NET eventually needs to go down this rabbit hole just to make sure really weird stuff do not happen.
As for Microsoft.Data.Sqlite my issue has been SqliteCommand.Dispose that internally encounters a null and crashes. This is when the framework has a lot of parallel calls all on different threads.
You may also note that sqlite itself has multi thread settings and believe that this rabbit hole is the one for you. Unfortunately fiddling with those settings, as beneficial as they may be, does nothing to change the ADO.NET object design and hence the problem remains as long as you use Microsoft.Data.Sqlite to access sqlite.
Regards for posting your code! Hope it made it onto production with great success :-)
/Nicke
Adding after posting:
The Dictionary holding references to Thread and SqliteConnection will have an impact on resources since it will stop garbage collection from reclaiming their resources for as long as the reference is there. The same goes for SqliteConnections.
Might not be a real problem, but one might also want to think about some kind of clean up algorithm that would remove stale Thread references after some time. SqliteConnections could still be reused if moved over and stored separately.
/Nicke