如何初始化并始终从专用线程调用静态API?

huangapple go评论61阅读模式
英文:

How to initialize and call a static API always from a dedicated thread?

问题

以下是翻译好的部分:

我有一个需要从初始化它的线程调用的API。我想创建一个包装器,用于为运行API的所有调用创建一个线程,然后允许我从用户界面进行await这些调用。在C#中,不使用第三方库,有什么优雅的方法可以做到这一点?

我想象的结果类似于这样:

static class SingleThreadedApi
{
    public static void Init();

    // 必须从与初始化相同的线程调用。
    public static double LongRunningCall();
}

class ApiWrapper
{
    // ???
}

class MyWindow
{
    ApiWrapper api = new();

    async void OnLoad()
    {
        await api.InitAsync();
    }

    async void OnButtonClick()
    {
        var result = await api.LongRunningCallAsync();
        answer.Text = "result: {result}";
    }
}

这个问题类似于 https://stackoverflow.com/questions/25691679/best-way-in-net-to-manage-queue-of-tasks-on-a-separate-single-thread,只是在那里任务只需按顺序运行,不一定要在相同的线程上运行。

https://stackoverflow.com/questions/39271492/how-do-i-create-a-custom-synchronizationcontext-so-that-all-continuations-can-be 可能是一个解决方案,但我希望有比"不是世界上最容易的事情。我有一个开源的[...]实现"更好的东西。

英文:

I have an API that needs to be called from the thread that initialized it. I want to make a wrapper that would create a thread for running all calls to the API and then allow me to await those calls from the UI. What would be an elegant way to do this in C#, without resorting to 3rd party libraries?

I am imagining the result looking something like this:

static class SingleThreadedApi
{
	public static void Init();

	// Has to be called from the same thread as init.
	public static double LongRunningCall();
}

class ApiWrapper
{
	// ???
}

class MyWindow
{
	ApiWrapper api = new();

	async void OnLoad()
	{
		await api.InitAsync();
	}

	async void OnButtonClick()
	{
		var result = await api.LongRunningCallAsync();
		answer.Text = "result: {result}";
	}
}

This question is similar to https://stackoverflow.com/questions/25691679/best-way-in-net-to-manage-queue-of-tasks-on-a-separate-single-thread, except there tasks only had to run serially, not necessarily on the same thread.

https://stackoverflow.com/questions/39271492/how-do-i-create-a-custom-synchronizationcontext-so-that-all-continuations-can-be might be one solution, but I hope there is something better than: "not the easiest thing in the world. I have an open-source [...] implementation."

答案1

得分: 3

以下是您要翻译的部分:

到目前为止,我找到的最佳解决方案是使用 BlockingCollection<Action>TaskCompletionSource。简化后,它看起来像这样:

static class SingleThreadedAPi
{
	public static void Init();

	// 必须从与初始化相同的线程调用。
	public static double LongRunningCall();
}

class ApiWrapper
{
	BlockingCollection<Action> actionQueue = new();

	public ApiWrapper()
	{
		new Thread(Run).Start();
	}

	public Task InitAsync()
	{
		var completion = new TaskCompletionSource();
		actionQueue.Add(() =>
		{
			try
			{
				SingleThreadedAPi.Init();
				completion.SetResult();
			}
			catch (Exception e)
			{
				completion.SetException(e);
			}
		});
		return completion.Task;
	}

	public Task<double> LongRunningCallAsync()
	{
		var completion = new TaskCompletionSource<double>();
		actionQueue.Add(() =>
		{
			try
			{
				
				completion.SetResult(SingleThreadedAPi.LongRunningCall());
			}
			catch (Exception e)
			{
				completion.SetException(e);
			}
		});
		return completion.Task;
	}

	public void Finish()
	{
		actionQueue.CompleteAdding();
	}

	void Run()
	{
		foreach (var action in actionQueue.GetConsumingEnumerable())
			action();
	}
}

class MyWindow
{
	ApiWrapper api;

	async void OnLoad()
	{
		await api.InitAsync();
	}

	async void OnButtonClick()
	{
		var result = await api.LongRunningCallAsync();
		answer.Text = "result: {result}";
	}
}
英文:

So far, the best solution I found was to use a BlockingCollection<Action> with TaskCompletionSource. Simplified, it looks like this:

static class SingleThreadedAPi
{
	public static void Init();

	// Has to be called from the same thread as init.
	public static double LongRunningCall();
}

class ApiWrapper
{
	BlockingCollection<Action> actionQueue = new();

	public ApiWrapper()
	{
		new Thread(Run).Start();
	}

	public Task InitAsync()
	{
		var completion = new TaskCompletionSource();
		actionQueue.Add(() =>
		{
			try
			{
				SingleThreadedAPi.Init();
				completion.SetResult();
			}
			catch (Exception e)
			{
				completion.SetException(e);
			}
		});
		return completion.Task;
	}

	public Task<double> LongRunningCallAsync()
	{
		var completion = new TaskCompletionSource<double>();
		actionQueue.Add(() =>
		{
			try
			{
				
				completion.SetResult(SingleThreadedAPi.LongRunningCall());
			}
			catch (Exception e)
			{
				completion.SetException(e);
			}
		});
		return completion.Task;
	}

	public void Finish()
	{
		actionQueue.CompleteAdding();
	}

	void Run()
	{
		foreach (var action in actionQueue.GetConsumingEnumerable())
			action();
	}
}

class MyWindow
{
	ApiWrapper api;

	async void OnLoad()
	{
		await api.InitAsync();
	}

	async void OnButtonClick()
	{
		var result = await api.LongRunningCallAsync();
		answer.Text = "result: {result}";
	}
}

答案2

得分: 1

任何解决此问题的方法都将基于BlockingCollection<T>,问题变成了如何更方便地与BlockingCollection<T>互动。我的建议是将其包装在自定义的TaskScheduler中,使用单个专用线程,并在此线程上执行所有任务。您可以在旧文章中找到有关如何编写自定义TaskScheduler的材料(文章的源代码可在此处找到),或者您可以使用我在发布的SingleThreadTaskScheduler实现。它可以这样使用:

class MyWindow
{
    private SingleThreadTaskScheduler _apiTaskScheduler;
    private TaskFactory _apiTaskFactory;

    async void OnLoad()
    {
        _apiTaskScheduler = new();
        _apiTaskFactory = new(_apiTaskScheduler);
        await _apiTaskFactory.StartNew(() => SingleThreadedApi.Init());
    }

    async void OnButtonClick()
    {
        double result = await _apiTaskFactory
            .StartNew(() => SingleThreadedApi.LongRunningCall());
        answer.Text = "result: {result}";
    }

    void OnClosed()
    {
        _apiTaskScheduler.Dispose();
    }
}

Dispose是一个阻塞调用。它会阻止UI线程,直到已安排执行的所有任务都完成,且专用线程终止。这并不理想,但允许窗口在所有操作完成之前关闭可能更糟糕。

英文:

Pretty much any solution to this problem will be based on a BlockingCollection<T> one way or another, and the question becomes how to make the interaction with the BlockingCollection<T> more convenient. My suggestion is to wrap it in a custom TaskScheduler, that uses a single dedicated thread, and executes all the tasks on this thread. You can find material about how to write custom TaskSchedulers on this old article (the source code of the article can be found here), or you can just use the SingleThreadTaskScheduler implementation that I've posted here. It can be used like this:

class MyWindow
{
    private SingleThreadTaskScheduler _apiTaskScheduler;
    private TaskFactory _apiTaskFactory;

    async void OnLoad()
    {
        _apiTaskScheduler = new();
        _apiTaskFactory = new(_apiTaskScheduler);
        await _apiTaskFactory.StartNew(() => SingleThreadedApi.Init());
    }

    async void OnButtonClick()
    {
        double result = await _apiTaskFactory
            .StartNew(() => SingleThreadedApi.LongRunningCall());
        answer.Text = "result: {result}";
    }

    void OnClosed()
    {
        _apiTaskScheduler.Dispose();
    }
}

The Dispose is a blocking call. It will block the UI thread until all tasks that have been scheduled for execution are completed, and the dedicated thread is terminated. This is not ideal, but allowing the window to close before all operations are completed is probably even worse.

3: https://devblogs.microsoft.com/pfxteam/parallelextensionsextras-tour-7-additional-taskschedulers/ "ParallelExtensionsExtras Tour – #7 – Additional TaskSchedulers"
4: https://github.com/donatasm/ParallelExtensionsExtras/tree/master/src/TaskSchedulers "ParallelExtensionsExtras"
5: https://stackoverflow.com/questions/58379898/c-sharp-moving-database-to-a-separate-thread-without-busy-wait/58397942#58397942 "Moving Database to a separate thread, without busy wait"

huangapple
  • 本文由 发表于 2023年4月11日 04:00:58
  • 转载请务必保留本文链接:https://go.coder-hub.com/75980302.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定