0

I have a system where all database operations (SQLite) must be run through the use of a work processor so i can schedule them however i like.

Right now the processor has a Task<TResult> PostAsync<TResult>(Func<SQLiteConnection, TResult> dbFunc) but the only sane implementation i've been able to come up with is a stack-based UI-thread worker loop that leverages Task to be able to defer the result up to the moment the actual action is processed.

It works fine but it is clogging up the UI thread with the SQLite calls. I'd rather have that not happen but the only idea i have been able to come up with is to spawn a worker thread to flush the queue or to give each action it's own worker thread. I don't think either one of these is the most sane approach as they'd end up creating threads everytime DB access is needed.

How can i achieve an implementation where the dbFunc arguments in the stack are executed in a background thread but said thread is only created once or is as lightweight as possible?

This has to work in a Xamarin environment. Either in PCL or Android+iOS implementations.

Machinarius
  • 3,637
  • 3
  • 30
  • 53
  • As far as multithreading goes, I don't think there is anything in .NET PCL that supports spinning up another thread. `System.ComponentModel` does not contain a `BackgroundWorker` class. The `System.Threading` namespace does not contain a `Thread` class. With either one of these, you could architect a queue (which I have done a number of times. But it is reliant on the `Thread` class). It may be that async tasks are the best you can get in a PCL. This SO thread seems to confirm: http://stackoverflow.com/questions/9251917/thread-sleep-in-a-portable-class-library – jwatts1980 May 15 '16 at 06:43
  • I failed to mention i can get away with Android+iOS platform-specific implementations of the worker interface. – Machinarius May 15 '16 at 14:19
  • 1
    It seems that your `PostAsync` argument should be a `Func`, not the other way around. – Kirill Shlenskiy May 15 '16 at 14:22
  • @KirillShlenskiy Good catch. Fixed. – Machinarius May 15 '16 at 14:25

1 Answers1

1

You don't need a pump or queue-type solution to accomplish this. Simply serializing access to your SQLiteConnection via a SemaphoreSlim will do the trick (of course you'll need singleton SQLiteConnection and SemaphoreSlim instances):

private readonly SQLiteConnection _sharedConnection;
private readonly SemaphoreSlim Semaphore = new SemaphoreSlim(1, 1);

public async Task<TResult> PostAsync<TResult>(Func<SQLiteConnection, TResult> dbFunc, CancellationToken ct)
{
    TResult result;
    bool needToRelease = false;

    try
    {
        await Semaphore.WaitAsync(ct).ConfigureAwait(false);

        // If we got this far, Release *must* be called.
        needToRelease = true;

        ct.ThrowIfCancellationRequested();

        // Push the work off to the thread pool in the event that
        // WaitAsync completed synchronously and we're still on the UI thread.
        result = await Task
            .Run(() => dbFunc(_sharedConnection), ct)
            .ConfigureAwait(false);
    }
    finally
    {
        if (needToRelease) {
            Semaphore.Release();
        }
    }

    return result;
}

The above will work fine with Xamarin.iOS and Xamarin.Android - however I understand that SemaphoreSlim is not available in some PCL profiles.

You might think that spinning up multiple Task instances every time you need to access the database is a bit rich, but in reality it's not much work, even for a mobile device.

A bigger problem with the above is that it's still "fake" asynchrony. We're just abusing Task.Run to offload blocking operations to the thread pool.

Therefore a more basic alternative (which I myself use) is worth mentioning. It is to expose your SQLiteConnection via a wrapper class where every public method locks on a shared object before executing the relevant method on the SQLiteConnection instance. In this case it's up to the callers whether they want to execute potentially blocking calls directly, or push them off to the thread pool via Task.Run. Until SQLite.Net starts offering true async I believe this to be the best method of managing concurrency with SQLiteConnection.

Community
  • 1
  • 1
Kirill Shlenskiy
  • 9,367
  • 27
  • 39
  • I really wanted to avoid `Task.Run` with a thread that is created once the stack gets a job pushed unto it and dies when there is nothing more to pump. Can this kind of thing be achieved? What's the benefit of a semaphore versus a `Stack` implementation? I am running that right now and it works but i want to hear if i am missing out on something. – Machinarius May 15 '16 at 16:45
  • `Task.Run` does not create and kill threads - it enlists threads from the thread pool, and if you ensure that only one task created via `Task.Run` is allowed to run at any given time (which is what `SemaphoreSlim` does - it's a truly async mutex protecting the task), then (theoretically) you never need more than one thread pool thread to service your database calls. In other words, this produces the exact semantics you describe (minus the stack part - which is very unconventional). You're just letting the thread pool manage the starting and restarting of your "pump", which it's pretty good at. – Kirill Shlenskiy May 15 '16 at 23:57
  • Of course you can roll your managed "keep executing work while available, then terminate until more work is pushed" state machine. Thing is, there's no real benefit in doing so as you still need it to run *sonewhere* while work is available. So it's going to borrow one thread from the thread pool (while work is available) to do so. Same result as in my example above, *but* you also have to worry about the (potentially non-trivial) thread synchronisation when scheduling new work, and when the work completes. It's a fun academic exercise, but more pain than it's worth. – Kirill Shlenskiy May 16 '16 at 00:03
  • Awesome description, I'll switch my implementation to this. Thank you very much. – Machinarius May 16 '16 at 23:13
  • Just came back to tell you this has worked wonders for my app for the last 5 months and the UI is buttery smooth thanks to it. Thank you :) – Machinarius Dec 13 '16 at 01:50
  • 1
    @Machinarius, I appreciate the feedback. Happy to hear that the solution worked well for your purposes. – Kirill Shlenskiy Dec 13 '16 at 02:19