首页 > 解决方案 > 始终从 C# 中的同一线程调用非托管 dll

问题描述

从 c# 调用非托管 dll 中的函数的最简单方法是什么,总是从同一个线程?

我的问题:我必须使用非托管 dll。此 dll 不是线程安全的,而且它还包含预期(如 WPF)始终由同一线程使用的 UI。因此,它似乎将影响 dll 中 UI 的第一个线程视为其“主”线程。

我认为“非线程安全”问题可以通过使用信号量来解决。通过仅从 C# 中的主线程调用 dll 可以轻松避免“仅从主线程”问题。但是后来我的主线程阻塞了。有没有一种简单的方法可以在调用 dll 时始终切换到同一个线程?

还是我尝试以错误的方式解决它?

标签: c#multithreading

解决方案


从一个线程调用库的最简单方法是确保从一个线程调用库。

这样做的缺点是它依赖于程序员不要从错误的线程调用它,因此您可以为对库的每次调用创建一个包装器,以添加一个正在使用同一线程的断言,并且您的单元测试将失败,如果您从不同的线程调用它,告诉您需要更改调用代码以适应约定的位置。

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
}

这确实意味着任何调用仍将阻塞调用线程。

如果您不想要该块,则将所有请求作为由单线程调度程序提供服务的任务,请参阅Run work on specific thread 的答案。

完整示例:

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());
        }
    }
}

如果您认为程序员可能忘记使用调度程序进行调用,您可以将两者结合起来。一般来说,当你的库从多个线程调用时,最好尽早失败断言而不是做任何奇怪的事情(并且出于这个原因,我有一些库有非常奇怪的行为)。


推荐阅读