Пользовательский TaskFactory не использует пользовательский SynchronizationContext - PullRequest
2 голосов
/ 15 апреля 2020

Примечание: это в Unity3D

Мне нужно запустить внутреннюю задачу, используя пользовательский контекст синхронизации, например:

  • внешняя задача - > по умолчанию в планировщике
    • внутренняя задача -> в пользовательском планировщике с пользовательским контекстом синхронизации

Это связано с тем, что некоторые объекты во внутренней задаче могут быть созданы только в с другой стороны, я хочу, чтобы внешняя задача работала нормально, т. е. ThreadPool или все, что Task.Run использует.

После совет по этому вопросу:

Как запустить задачу на пользовательском TaskScheduler с помощью await?

К сожалению, это не работает, контекст синхронизации по-прежнему по умолчанию:

OUTER JOB: 'null'
0
        INNER JOB: 'null' <------------- this context should not be the default one
        0,      1,      2,      3,      4,
1
        INNER JOB: 'null'
        0,      1,      2,      3,      4,
2
        INNER JOB: 'null'
        0,      1,      2,      3,      4,
Done, press any key to exit

Код:

using System;
using System.Threading;
using System.Threading.Tasks;

internal static class Program
{
    private static TaskFactory CustomFactory { get; set; }

    private static async Task Main(string[] args)
    {
        // create a custom task factory with a custom synchronization context,
        // and make sure to restore initial context after it's been created

        var initial = SynchronizationContext.Current;

        SynchronizationContext.SetSynchronizationContext(new CustomSynchronizationContext());

        CustomFactory = new TaskFactory(
            CancellationToken.None,
            TaskCreationOptions.DenyChildAttach,
            TaskContinuationOptions.None,
            TaskScheduler.FromCurrentSynchronizationContext()
        );

        SynchronizationContext.SetSynchronizationContext(initial);


        // now do some work on initial context

        await Task.Run(async () =>
        {
            Console.WriteLine($"OUTER JOB: {GetCurrentContext()}");

            for (var i = 0; i < 3; i++)
            {
                Console.WriteLine(i);
                await Task.Delay(100);

                // now do some work on custom context
                await RunOnCustomScheduler(DoSpecialWork);
            }
        });

        Console.WriteLine("Done, press any key to exit");
        Console.ReadKey();
    }

    private static void DoSpecialWork()
    {
        Console.WriteLine($"\tINNER JOB: {GetCurrentContext()}"); // TODO wrong context

        for (var i = 0; i < 5; i++)
        {
            Thread.Sleep(250);
            Console.Write($"\t{i}, ");
        }

        Console.WriteLine();
    }

    private static Task RunOnCustomScheduler(Action action)
    {
        return CustomFactory.StartNew(action);
    }

    private static string GetCurrentContext()
    {
        return SynchronizationContext.Current?.ToString() ?? "'null'";
    }
}

public class CustomSynchronizationContext : SynchronizationContext
{
}

Когда я отлаживаю, я вижу, что на фабрике изготовителя действительно есть настраиваемый планировщик с настраиваемым контекст, но на практике это не работает, однако. * 103 5 *

Вопрос:

Как мне заставить мой пользовательский планировщик использовать пользовательский контекст, из которого он был создан?

Окончательный ответ:

Я ждал метод внутри потока, в котором я хотел бы избежать работы, завернув этот материал в await Task.Run (плюс некоторые детали, оставленные для краткости), исправил его. Теперь я могу запускать свою длинную задачу вне потока Unity, но по-прежнему делать внутри нее вызовы Unity, используя описанный выше пользовательский подход к фабрике.

т.е. она уже работала, но я не использовал ее правильно

Ответы [ 2 ]

2 голосов
/ 16 апреля 2020

Ваш CustomSynchronizationContext действительно используется для запуска метода DoSpecialWork. Вы можете подтвердить это, переопределив метод Post базового класса:

public class CustomSynchronizationContext : SynchronizationContext
{
    public override void Post(SendOrPostCallback d, object state)
    {
        Console.WriteLine($"@Post");
        base.Post(d, state);
    }
}

Затем снова запустите вашу программу, и вы увидите три @ Post , появляющихся в консоли.

Причина, по которой SynchronizationContext.Current равен null, заключается в том, что это свойство связано с текущим потоком. Вот исходный код :

// Get the current SynchronizationContext on the current thread
public static SynchronizationContext Current 
{
    get      
    {
        return Thread.CurrentThread.GetExecutionContextReader().SynchronizationContext ?? GetThreadLocalContext();
    }
}

Вы можете установить CustomSynchronizationContext в каждом потоке ThreadPool, но это не рекомендуется. Это тоже не обязательно. В отсутствие SynchronizationContext.Current асинхронные c продолжения выполняются в текущем TaskScheduler, а текущий TaskScheduler в методе DoSpecialWorkAsync является тем, который вы создали сами с помощью метода TaskScheduler.FromCurrentSynchronizationContext(), который связан с вашим CustomSynchronizationContext экземпляром.

0 голосов
/ 15 апреля 2020

Этого очень легко добиться, используя ReactiveExtensions (nuget System.Reactive):

public static Task RunOnCustomScheduler(Action action, IScheduler scheduler)
{
    return Observable.Return(Unit.Default)
        .ObserveOn(scheduler) // switch context
        .Do(_ => action.Invoke()) // do the work
        // .Select(_ => func.Invoke()) // change Action to Func if you want to return stuff.
        //.Select(async _ => await func.Invoke()) // can work with async stuff, though ConfigureAwait may become tricky
        .ToTask();
}

Вы также можете передать SynchronizationContext вместо IScheduler.

Не забудьте ConfigureAwait(false/true), хотя, когда вы вызываете этот метод, чтобы настроить, как вы хотите продолжить

...