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).