让PythonNet优雅地运行在单独的线程

之前不是想让Kokoro运行在C#中嘛
但项目赶得紧,所以就先通过PythonNet嫁接过来实现

Kokoro相应还是要一点时间的,挂主线程就会阻塞
作为CPU消耗型的任务,很自然的会想到Task.Run
Task.Run基于的是线程池,每次运行分配的线程是不一样的
这会导致每次生成语音,Python上下文是不一致的
轻则需要重新加载模型文件,重则会导致难以估计的问题
所以需要一个后台线程来运行Python实例

这里先给出示例代码

public class PythonExecutor
{
	private readonly Thread _pythonThread;
	private readonly BlockingCollection<Func<object>> _tasks = new();
	public PythonExecutor()
	{
		Runtime.PythonDLL = "Your Python Dll Path";
		_pythonThread = new Thread(() =>
        {
            PythonEngine.Initialize();
            PythonEngine.BeginAllowThreads();
            foreach (var task in _taskQueue.GetConsumingEnumerable())
            {
                try
                {
                    using (Py.GIL()) task();
                }
                catch (Exception ex)
                {
                    Console.WriteLine($"Python task error: {ex}");
                }
            }
            PythonEngine.Shutdown();
        })
        {
            IsBackground = true
        };
        _pythonThread.Start();
	}
	
	public void Task<T> EnqueueAsync<T>(Func<T> pythonFunc)
    {
        var tcs = new TaskCompletionSource<T>();
        _taskQueue.Add(() =>
        {
            try
            {
                var result = pythonFunc();
                tcs.SetResult(result);
            }
            catch (Exception ex)
            {
                tcs.SetException(ex);
            }
            return null!;
        });
        return tcs.Task;
    }
    // 对象析构时指定无后继任务,终止线程
    ~PythonExecutor()
    {
	    _taskQueue.CompleteAdding();
    }
}

调用示例

var py = new PythonExecutor();
Console.WriteLine("Do Heavy Calculate In Python");
int ans = await py.EnqueueAsync(()=>{
	dynamic some_module = Py.Import("some_module");
	PyObject result = some_module.heavy_cal();
	return result.As<int>();
});
Console.WriteLine($"Ans: {ans}");

可通过await等待,不阻塞主线程,这样就很方便了

Licensed under CC BY-NC-SA 4.0