Mike Asdf
Mike Asdf

Reputation: 2329

Are the constructor and Dispose method for SqliteCommand objects threadsafe in Mono?

The following is a simplified representation of some data-access code I'm working with in an iOS xamarin app:

public async Task<List<Foo>> GetAllFoos()
{
    var foos = new List<Foo>();
    using(CommandWrapper command = GetCommandWrapper("SELECT Id, Name, Rank FROM Foos"))//this creates SqliteCommand object and puts it in wrapper object
    {
        using(ReaderWrapper reader = ExecuteReaderAsync(_Connection, command))
        {
            while(reader.Read())
            {
                var foo = ConstructFoo(reader);//construct foo with column values read from reader
                foos.Add(foo);
            }
        }
    }
    return foos;
}

private SemaphoreSlim _commandLockSemaphore = new SemaphoreSlim(1, 1);//only 1 thread at once

private async ExecuteReaderAsync(Mono.Data.Sqlite.SqliteConnection connection, CommandWrapper command)
{
    if(!(await _SemaphoreLock.WaitAsync(_TimeoutTimespan))) throw new Exception("timeout");

    //lock now held

    Mono.Data.Sqlite.SqliteCommand sqliteCommand = command.UnderlyingCommand;

    try
    {
        sqliteCommand.Connection = connection;
        IDataReader rawReader = await sqliteCommand.ExecuteReaderAsync();
    }
    catch(Exception)
    {
        //emergency lock release
        _SemaphoreLock.Release();
        throw;
    }
    return new ReaderWrapper(){DataReader = rawReader, SemaphoreLock = _SemaphoreLock};
}

internal class ReaderWrapper : IDisposable
{
    internal IDataReader DataReader{get; set;}
    internal SemaphoreSlim SemaphoreLock{get; set;}

    //... [read methods]

    public void Dispose()
    {
        DataReader.Dispose();
        SemaphoreLock.Release();
    }
}

Since we knew Sqlite is known to not support multiple threads on a connection, we put the SemaphoreSlim in place to ensure that the usage of a connection and the execution of any command only occurs within a single thread. The SemaphoreSlim is subsequently released after the reader is exhausted and disposed.

However, when I have multiple threads calling GetAllFoos, then the app crashes with the following:

2015-08-19 14:33:22.296 MyCoolIosApp[8421:5311923] critical: Stacktrace:
2015-08-19 14:33:22.296 MyCoolIosApp[8421:5311923] critical:   at  
2015-08-19 14:33:22.297 MyCoolIosApp[8421:5311923] critical:   at (wrapper managed-to-native) Mono.Data.Sqlite.UnsafeNativeMethods.sqlite3_prepare (intptr,intptr,int,intptr&,intptr&) 
2015-08-19 14:33:22.297 MyCoolIosApp[8421:5311923] critical:   at Mono.Data.Sqlite.SQLite3.Prepare (Mono.Data.Sqlite.SqliteConnection,string,Mono.Data.Sqlite.SqliteStatement,uint,string&) [0x00044] in //Library/Frameworks/Xamarin.iOS.framework/Versions/8.10.4.46/src/mono/mcs/class/Mono.Data.Sqlite/Mono.Data.Sqlite_2.0/SQLite3.cs:268
2015-08-19 14:33:22.298 MyCoolIosApp[8421:5311923] critical:   at Mono.Data.Sqlite.SqliteCommand.BuildNextCommand () [0x00019] in //Library/Frameworks/Xamarin.iOS.framework/Versions/8.10.4.46/src/mono/mcs/class/Mono.Data.Sqlite/Mono.Data.Sqlite_2.0/SQLiteCommand.cs:230
2015-08-19 14:33:22.298 MyCoolIosApp[8421:5311923] critical:   at Mono.Data.Sqlite.SqliteCommand.GetStatement (int) [0x0000b
] in //Library/Frameworks/Xamarin.iOS.framework/Versions/8.10.4.46/src/mono/mcs/class/Mono.Data.Sqlite/Mono.Data.Sqlite_2.0/SQLiteCommand.cs:264
2015-08-19 14:33:22.299 MyCoolIosApp[8421:5311923] critical:   at Mono.Data.Sqlite.SqliteDataReader.NextResult () [0x000cc] in //Library/Frameworks/Xamarin.iOS.framework/Versions/8.10.4.46/src/mono/mcs/class/Mono.Data.Sqlite/Mono.Data.Sqlite_2.0/SQLiteDataReader.cs:914
2015-08-19 14:33:22.299 MyCoolIosApp[8421:5311923] critical:   at Mono.Data.Sqlite.SqliteDataReader..ctor (Mono.Data.Sqlite.SqliteCommand,System.Data.CommandBehavior) [0x00051] in //Library/Frameworks/Xamarin.iOS.framework/Versions/8.10.4.46/src/mono/mcs/class/Mono.Data.Sqlite/Mono.Data.Sqlite_2.0/SQLiteDataReader.cs:89
2015-08-19 14:33:22.300 MyCoolIosApp[8421:5311923] critical:   at Mono.Data.Sqlite.SqliteCommand.ExecuteReader (System.Data.CommandBehavior) [0x00006] in //Library/Frameworks/Xamarin.iOS.framework/Versions/8.10.4.46/src/mono/mcs/class/Mono.Data.Sqlite/Mono.Data.Sqlite_2.0/SQLiteCommand.cs:539
2015-08-19 14:33:22.300 MyCoolIosApp[8421:5311923] critical:   at Mono.Data.Sqlite.SqliteCommand.ExecuteReader () [0x00000] in //Library/Frameworks/Xamarin.iOS.framework/Versions/8.10.4.46/src/mono/mcs/class/Mono.Data.Sqlite/Mono.Data.Sqlite_2.0/SQLiteCommand.cs:551
...

According to various discussions, that error is caused by too many threads on Sqlite on iOS devices. I can corroborate this since it crashes 100% of the time if I have a pair of threads attempt to endlessly call GetAllFoos in a while loop; and also when I rearrange the locks (e.g. add a simple lock statement around construction and disposal of the command), it fixes the issue:

public async Task<List<Foo>> GetAllFoos()
{
    var foos = new List<Foo>();
    lock(_someStaticObject)//why is this needed
    {
        using(CommandWrapper command = GetCommandWrapper("SELECT Id, Name, Rank FROM Foos"))//this creates SqliteCommand object and puts it in wrapper object
        {
            using(ReaderWrapper reader = ExecuteReaderAsync(_Connection, command))
            {
                while(reader.Read())
                {
                    var foo = ConstructFoo(reader);//construct foo with column values read from reader
                    foos.Add(foo);
                }
            }
        }
    }
    return foos;
}

As near as I can tell, the Dispose method (and possibly the constructor) on SqliteCommand is causing the concurrency issue.

Are the constructor and Dispose method for SqliteCommand objects threadsafe in Mono? Or do I need to consider one or both to be a critical section for locking purposes?

Upvotes: 2

Views: 232

Answers (1)

Clay Fowler
Clay Fowler

Reputation: 2078

The great thing about using Mono is that it's open source, so let's answer your question "Are the constructor and Dispose method for SqliteCommand objects threadsafe in Mono?"

If we take a quick look here:

https://github.com/mono/mono/blob/master/mcs/class/Mono.Data.Sqlite/Mono.Data.Sqlite_2.0/SQLiteCommand.cs#L108

and here:

https://github.com/mono/mono/blob/master/mcs/class/Mono.Data.Sqlite/Mono.Data.Sqlite_2.0/SQLiteCommand.cs#L153

...then we can emphatically answer "No, neither the constructor nor the Dispose of Mono's SQLiteCommand are thread safe".

For that matter, it is correct to assume that nothing anywhere is ever threadsafe unless it explicitly says so via documentation. And even then it is sometimes a lie.

(And now Microsoft's stuff is open source, too, so we can answer these questions outside of Mono the same way. Yay!)

Upvotes: 1

Related Questions