【问题标题】:C# execute Threading.ThreadPool.QueueUserWorkItem callback in main thread affinityC# 在主线程关联中执行 Threading.ThreadPool.QueueUserWorkItem 回调
【发布时间】:2019-10-13 12:23:25
【问题描述】:

我想在 Unity 中从磁盘加载一堆图像。所以我想使用线程池在辅助线程中实际加载图像字节,然后在主线程中应用纹理。

我创建了一个函数,它获取路径作为输入并在System.Threading.ThreadPool.QueueUserWorkItem 的帮助下加载图像字节,当它完成时,它调用一个回调,从这些字节中创建纹理并将其应用于游戏对象。

我遇到的问题是回调是使用工作线程的亲和性执行的。并且统一不允许非主线程对其数据进行修改。

有没有办法在主线程的关联中执行回调?

这是相关代码:

 public delegate void OnBytesLoaded(byte[] bytes);

    private void LoadBytesToTexture(byte[] bytes)
    {
        Texture2D texture = new Texture2D(100, 100);
        texture.LoadImage(bytes);
        thumbnail.texture = texture;
    }

    private void LoadTextureImage(string imagePath)
    {
        OnBytesLoaded callback = new OnBytesLoaded(LoadBytesToTexture);

        System.Threading.ThreadPool.QueueUserWorkItem(o =>
        {
            byte[] bytes = System.IO.File.ReadAllBytes(imagePath);
            Debug.Log($"Loaded image bytes");
            callback?.Invoke(bytes);
        });
    }

正如我所说,我的问题是LoadBytesToTexture 是在线程池线程的亲和中执行的,而不是主线程。

【问题讨论】:

  • @mjwills 有人说:“从 MyMethod 中调用 Control.Invoke 以将委托的执行编组到 UI 线程上”。这正是我正在做的。虽然委托仍然没有在主线程上执行。
  • @mjwills callback?.Invoke(bytes);
  • callback?.Invoke(bytes); 中的Invokea compiler-generated method。和Control.Invoke不一样。
  • @Theodor Zoulias 我明白了。虽然Control.Invoke 似乎与我无关,因为它是寡妇表格的一部分。

标签: c# multithreading unity3d


【解决方案1】:

.NET 中有一个名为SynchronizationContext 的概念。

简而言之,同步上下文可以将工作委派给它所关联的线程(取决于框架,这可以是线程池GUI 线程创建同步上下文的线程 ...等)。

使用这样的东西应该可以完成这项工作:

private void LoadTextureImage(string imagePath)
{
    var syncContext = SynchronizationContext.Current;
    OnBytesLoaded callback = new OnBytesLoaded(bytes => syncContext .Post(_ => LoadBytesToTexture(bytes), null));

    System.Threading.ThreadPool.QueueUserWorkItem(o =>
    {
        byte[] bytes = System.IO.File.ReadAllBytes(imagePath);
        Debug.Log($"Loaded image bytes");
        callback?.Invoke(bytes);
    });
}

注意:
SynchronizationContext 取决于调用上下文。

在当前场景中syncContextbytes => syncContext.Post的闭包中被捕获,这样
.Current 属性会将 syncContext 委托工作返回给 GUI 线程(因为 LoadTextureImage 是从 GUI 线程调用的),
否则,如果我们使用了bytes => SynchronizationContext.Current.Post,这可能会产生 nullsyncContext,它们会将工作委托给 ThreadPool(因为 .Current 是从水池)。


要求 cmets 提供更多信息:

OnBytesLoaded(bytes => syncContext .Post(_ => LoadBytesToTexture(bytes), null))

这个
OnBytesLoaded callback = new OnBytesLoaded(LoadBytesToTexture);

OnBytesLoaded callback = new OnBytesLoaded(bytes => LoadBytesToTexture(bytes));

在代码的原始版本中,您使用LoadBytesToTexture 作为OnBytesLoaded 的委托(期望方法/委托具有以下定义byte[] -> void),现在我们是将使用syncContext 的新委托将LoadBytesToTexture 排队返回GUI 线程,该线程再次具有相同的定义bytes[] -> void。
我们不是直接调用LoadBytesToTexture,而是告诉syncContext 使用在此调用callback?.Invoke(bytes) 中传递的字节将LoadBytesToTexture 排队返回到GUI 线程

【讨论】:

  • 工作就像一个魅力!谢谢!虽然我对这一行的语法有点困惑:new OnBytesLoaded(bytes => syncContext .Post(_ => LoadBytesToTexture(bytes), null))
  • 确实如此。谢谢!
  • 非常感谢!到目前为止,您的解决方案是我发现的唯一不需要MonoBehaviour 的方法。
【解决方案2】:

1) 创建简单的调度程序脚本并将其附加到场景中的空活动游戏对象。

using System;
using System.Collections.Generic;

/// <summary>
/// Helps dispatch task results to the main thread to be able to operate on unity's API like SetActive, enabled etc...
/// </summary>
public class MainThreadDispatcher : MonoBehaviour
{
    Queue<Action> jobs = new Queue<Action>();
    static MainThreadDispatcher Instance = null;

    private void Awake()
    {
        Instance = this;
    }
    private void Update()
    {
        while (jobs.Count > 0)
        {
            var next = jobs.Dequeue();
            if(next != null)
            {
                next.Invoke();
            }
        }
    }
    /// <summary>
    /// Dispatches a function to be executed on unity's main thread to be able to use unity's API.
    /// </summary>
    /// <param name="newJob"></param>
    public static void Dispatch(Action newJob)
    {
        if (newJob == null)
            return;
        Instance.jobs.Enqueue(newJob);
    }
}

2) 将您的 LoadBytesToTexture 方法主体编辑为:

private void LoadBytesToTexture(byte[] bytes)
{
    MainThreadDispatcher.Dispatch(()=>
    {
        Texture2D texture = new Texture2D(100, 100);
        texture.LoadImage(bytes);
        thumbnail.texture = texture;
    });
}

所以基本上要在主线程上执行任何事情都做MainThreadDispatcher.Dispatch(()=&gt; code);

【讨论】:

  • 与@vasil oreshenski 建议的使用 SynchronizationContext 相比,使用它有什么优势吗?
  • @Curtwagner1984 我对 Unity3d 不够熟悉,但 MonoBehaviour 是 Unity 特有的——这可能是更好的选择,而且 SyncContext 解决方案更通用。 MonoBehaviour 可能在某些时候使用了 SyncContext,所以 MonoBehaviour 更像是一个包装器(我猜更适合 Unity)。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-10-02
  • 1970-01-01
  • 2015-10-27
  • 2018-01-02
  • 1970-01-01
  • 2023-03-04
相关资源
最近更新 更多