C # выполняет Threading.ThreadPool.QueueUserWorkItem обратный вызов в соответствии с основным потоком - PullRequest
1 голос
/ 13 октября 2019

Я хочу загрузить кучу изображений с диска в 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 выполняется в соответствии с потоком пула потоков, а не с основным потоком.

Ответы [ 2 ]

3 голосов
/ 13 октября 2019

В .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 зависит от контекста вызова.

В текущем сценарии syncContext в захваченном при закрытии bytes => syncContext.Post, таким образом
свойство. Current вернет syncContext, делегируя работу обратно в поток GUI (потому что LoadTextureImage вызывается из потока GUI ),
в противном случае, если мы использовали bytes => SynchronizationContext.Current.Post, это может привести к null или syncContext, что делегирует работу обратнов ThreadPool (потому что. Current вызывается из потока в пуле).


Запрошена дополнительная информация из комментариев о:

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 поставить в очередь LoadBytesToTexture обратно в поток GUI с использованием байтов, переданных в этом вызове callback?.Invoke(bytes).

2 голосов
/ 13 октября 2019

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(()=> code);

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...