c# - 始终从 C# 中的同一线程调用非托管 dll
问题描述
从 c# 调用非托管 dll 中的函数的最简单方法是什么,总是从同一个线程?
我的问题:我必须使用非托管 dll。此 dll 不是线程安全的,而且它还包含预期(如 WPF)始终由同一线程使用的 UI。因此,它似乎将影响 dll 中 UI 的第一个线程视为其“主”线程。
我认为“非线程安全”问题可以通过使用信号量来解决。通过仅从 C# 中的主线程调用 dll 可以轻松避免“仅从主线程”问题。但是后来我的主线程阻塞了。有没有一种简单的方法可以在调用 dll 时始终切换到同一个线程?
还是我尝试以错误的方式解决它?
解决方案
从一个线程调用库的最简单方法是确保从一个线程调用库。
这样做的缺点是它依赖于程序员不要从错误的线程调用它,因此您可以为对库的每次调用创建一个包装器,以添加一个正在使用同一线程的断言,并且您的单元测试将失败,如果您从不同的线程调用它,告诉您需要更改调用代码以适应约定的位置。
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());
}
}
}
如果您认为程序员可能忘记使用调度程序进行调用,您可以将两者结合起来。一般来说,当你的库从多个线程调用时,最好尽早失败断言而不是做任何奇怪的事情(并且出于这个原因,我有一些库有非常奇怪的行为)。
推荐阅读
- python - 让 if/else 在循环两个范围的列表理解中跳出一个循环
- generics - 通用参数在“供应商” lambda 中丢失,但在“可调用”中没有
- java - 获取图像并放入 jframe 的问题
- arrays - 字典保存错误“SwiftValue encodeWithCoder:]: unrecognized selector sent to instance”
- vee-validate - How would I get a different message on each screen?
- python - 如何使用 Python 3 将包含 15,000 条推文的 JSON 文件转换为 CSV
- angular - 在 Angular 中从 Firebase 获取和显示数据
- java - 非常基本的 JFrame 没有出现
- javascript - 使用 d3.mouse 时出现 D3 Typescript 错误:“this”隐含类型为“any”,因为它没有类型注释
- java - Spring Boot + JPA + Hibernate CommandAcceptanceException:执行 DDL 时出错