Klamsi
Klamsi

Reputation: 906

Call an unmanaged dll always from same thread in C#

What is the easiest way to call functions in an unmanaged dll from c#, always from the same thread?

My problem: I have to use an unmanaged dll. This dll is not thread-safe and additionaly it has UI included that expects (like WPF) to be used always by the same thread. So it seems it regards the first thread that affects UI in the dll as its "main" thread.

The "not thread-safe" problem I think would be resolved by using a semaphore. The "only from main thread" problem can be easily avoided by calling the dll only from the main thread in c#. But then my main thread blocks. Is there a simple way to always switch to the same thread when calling the dll?

Or do I try to solve it the false way?

Upvotes: 0

Views: 1241

Answers (2)

Pete Kirkham
Pete Kirkham

Reputation: 49321

The easiest way to call the library from one thread is to make sure you call the library from one thread.

This has the disadvantage that it relies on the programmer not to call it from the wrong thread, so you can create a wrapper for each call to the library to add an assertion that the same thread is being used, and your unit tests will fail if you call it from a different thread, telling you where you need to change the calling code to fit the convention.

public class Library
{
    private readonly int[] _threadId;

    public Library()
    {
        _threadId = new[] { Thread.CurrentThread.ManagedThreadId };
    }

    private void CheckIsSameThread()
    {
        var id = Thread.CurrentThread.ManagedThreadId;
        lock (_threadId)
            if (id != _threadId[0])
                throw new InvalidOperationException("calls to the library were made on a different thread to the one which constructed it.");
    }

    // expose the API with a check for each call
    public void DoTheThing()
    {
        CheckIsSameThread();
        ActuallyDoTheThing();
    }

    private void ActuallyDoTheThing() // etc
}

This does means any calls will still block the calling thread.

If you don't want the block, then make all requests as tasks which are serviced by a single threaded scheduler, see this answer to Run work on specific thread.

Full example:

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace RunInSameThreadOnly
{
    public class Library
    {
        private readonly int[] _threadId;

        public Library()
        {
            _threadId = new[] { Thread.CurrentThread.ManagedThreadId };
        }

        private void CheckIsSameThread()
        {
            var id = Thread.CurrentThread.ManagedThreadId;
            lock (_threadId)
                if(id != _threadId[0])
                    throw new InvalidOperationException("calls to the library were made on a different thread to the one which constructed it.");
        }

        public void DoTheThing()
        {
            CheckIsSameThread();
            ActuallyDoTheThing();
        }

        private void ActuallyDoTheThing()
        {
        }
    }

    public sealed class SingleThreadTaskScheduler : TaskScheduler
    {
        [ThreadStatic]
        private static bool _isExecuting;
        private readonly CancellationToken _cancellationToken;
        private readonly BlockingCollection<Task> _taskQueue;

        public SingleThreadTaskScheduler(CancellationToken cancellationToken)
        {
            this._cancellationToken = cancellationToken;
            this._taskQueue = new BlockingCollection<Task>();
        }

        public void Start()
        {
            new Thread(RunOnCurrentThread) { Name = "STTS Thread" }.Start();
        }

        // Just a helper for the sample code
        public Task Schedule(Action action)
        {
            return
                Task.Factory.StartNew
                    (
                        action,
                        CancellationToken.None,
                        TaskCreationOptions.None,
                        this
                    );
        }

        // You can have this public if you want - just make sure to hide it
        private void RunOnCurrentThread()
        {
            _isExecuting = true;

            try
            {
                foreach (var task in _taskQueue.GetConsumingEnumerable(_cancellationToken))
                {
                    TryExecuteTask(task);
                }
            }
            catch (OperationCanceledException)
            { }
            finally
            {
                _isExecuting = false;
            }
        }

        // Signalling this allows the task scheduler to finish after all tasks complete
        public void Complete() { _taskQueue.CompleteAdding(); }
        protected override IEnumerable<Task> GetScheduledTasks() { return null; }

        protected override void QueueTask(Task task)
        {
            try
            {
                _taskQueue.Add(task, _cancellationToken);
            }
            catch (OperationCanceledException)
            { }
        }

        protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)
        {
            // We'd need to remove the task from queue if it was already queued. 
            // That would be too hard.
            if (taskWasPreviouslyQueued) return false;

            return _isExecuting && TryExecuteTask(task);
        }
    }

    [TestClass]
    public class UnitTest1
    {
        // running tasks with default scheduler fails as they are run on multiple threads 
        [TestMethod]
        public void TestMethod1()
        {
            Library library = null;

            Task.Run(() => { library = new Library(); }).Wait();

            var tasks = new List<Task>();

            for (var i = 0; i < 100; ++i)
                tasks.Add(Task.Run(() => library.DoTheThing()));

            Task.WaitAll(tasks.ToArray());
        }

        // tasks all run on same thread using SingleThreadTaskScheduler
        [TestMethod]
        public void TestMethod2()
        {
            var cts = new CancellationTokenSource();
            var myTs = new SingleThreadTaskScheduler(cts.Token);
            
            myTs.Start();

            Library library = null;

            myTs.Schedule(() => { library = new Library(); }).Wait();

            var tasks = new List<Task>();

            for (var i = 0; i < 100; ++i)
                tasks.Add(myTs.Schedule(() => library.DoTheThing()));

            Task.WaitAll(tasks.ToArray());
        }
    }
}

You can combine both if you think the programmer might forget to make the calls using the scheduler. Generally it's better to fail early with an assertion than do whatever weird thing your library does when called from more than one thread (and I've had some libraries have very odd behaviour for this reason).

Upvotes: 2

Dan
Dan

Reputation: 1959

You should create another application, with only an UI thread, that only calls your .DLL code. This "shim" application has an IPC channel with your main application (I always try registered Windows messages first, if the context allows, otherwise I go for named pipes), to route commands to your .DLL and then route back the results from the .DLL. The "shim" application only shows the UI to the user when needed and it is launched by the main application. The most important advantage of this solution is you are avoiding writing multi-threaded code, especially since you must use third-party code.

Upvotes: 0

Related Questions