mercredi 19 août 2015

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?

Aucun commentaire:

Enregistrer un commentaire