2016-12-21 7 views
0

Из цикла игры я хочу начать работу в фоновом режиме, которая должна выполняться одна за другой, но не должна блокировать игровой цикл.Как поставить в очередь делегатов, которые будут выполняться последовательно в фоновом режиме с помощью C#?

Так в идеале класс BackgroundQueue, который может быть использован, как это:

BackgroundQueue myQueue = new BackgroundQueue(); 
//game loop called 60 times per second 
void Update() 
{ 
    if (somethingHappens) 
    { 
     myQueue.Enqueue(() => MethodThatTakesLong(someArguments)) 
    } 
} 

Есть ли готовый класс в .NET, который работает для сценария? Или кто-то знает хороший пример того, как реализовать класс BackgroundQueue?

приятно иметь если бы класс мог сообщить будь он в настоящее время делает что-то и сколько делегатов в очереди ...

+0

Посмотрите на BlockingCollection вместе с ConcurrentQueue, есть много примеров. Начните поток, который потребляет от BlockingCollection, и элементы к нему из вашего основного цикла. – Evk

+2

Задача и пользовательский TaskScheduler, вероятно, сделают трюк, если вы хотите, чтобы ваши делегаты выполнялись последовательно. См. Http://www.infoworld.com/article/3063560/application-development/building-your-own-task-scheduler-in-c.html о том, как создать пользовательский TaskScheduler. Если вы не возражаете, чтобы ваши делегаты выполнялись параллельно, стандартный .NET TaskScheduler, вероятно, прекрасен. –

+1

Рассмотрите возможность хранения всего одного потока и использования async-ожидания для создания асинхронного рабочего процесса. Помните, ** асинхронность не является параллелизмом **. Вы можете выполнять асинхронные рабочие процессы на одном потоке, точно так же, как вы можете готовить яйца и тосты одновременно, не нанимая двух поваров. –

ответ

3

Одно из решений состоит из отличной Threading in C# E-book. В своем разделе об основных структурах автор делает практически то, что вы ищете in an example.

Следуйте по этой ссылке и прокрутите вниз до очереди производителя/потребителя.

В более позднем разделе он обращается к тому, что в то время как ConcurrentQueue также будет работать нормально, он выполняет хуже во всех случаях, ЗА ИСКЛЮЧЕНИЕМ в высококонкурентных сценариях. Но для вашего случая с низкой нагрузкой может быть лучше просто получить что-то, что легко работает. У меня нет личного опыта в заявлении из документа, но он там для вас, чтобы оценить.

Надеюсь, это поможет.

Edit2: по предложению Evk (спасибо!) Класс BlockingCollection выглядит так, как вы хотите. По умолчанию он использует ConcurrentQueue под капотом. Мне особенно нравится метод CompleteAdding, а также возможность использования CancellationTokens с ним. Сценарии «Завершение работы» не всегда правильно учитываются при блокировании вещей, но это верно для ИМО.

Редактировать 3: В соответствии с запросом образец того, как это будет работать с BlockingCollection. Я использовал foreach и GetConsumingEnumerable, чтобы сделать это еще более компактным для потребителя стороны проблемы:

using System.Collections.Concurrent; 
private static void testMethod() 
{ 
    BlockingCollection<Action> myActionQueue = new BlockingCollection<Action>(); 
    var consumer = Task.Run(() => 
    { 
    foreach(var item in myActionQueue.GetConsumingEnumerable()) 
    { 
     item(); // Run the task 
    }// Exits when the BlockingCollection is marked for no more actions 
    }); 

    // Add some tasks 
    for(int i = 0; i < 10; ++i) 
    { 
    int captured = i; // Imporant to copy this value or else 
    myActionQueue.Add(() => 
    { 
     Console.WriteLine("Action number " + captured + " executing."); 
     Thread.Sleep(100); // Busy work 
     Console.WriteLine("Completed."); 
    }); 
    Console.WriteLine("Added job number " + i); 
    Thread.Sleep(50); 
    } 
    myActionQueue.CompleteAdding(); 
    Console.WriteLine("Completed adding tasks. Waiting for consumer completion"); 

    consumer.Wait(); // Waits for consumer to finish 
    Console.WriteLine("All actions completed."); 
} 

я добавил в Sleep() вызовов, так что вы можете увидеть, что вещи будут добавлены в то время как другие вещи потребляются , Вы также можете выбрать любое количество этого consumer lambda (просто назовите его Action, затем запустите Action несколько раз) или цикл добавления. И в любое время вы можете позвонить Count в сборнике, чтобы получить количество задач, которые НЕ запускаются. Предположительно, если это отличное от нуля, тогда выполняются ваши задачи-производители.

+2

Если вы используете BlockingCollection с ConcurrentQueue (и его метод GetConsumingEnumerable) - он будет блокироваться при удалении. – Evk

1

Как об этом

void Main() 
{ 
    var executor = new MyExecutor(); 
    executor.Execute(()=>Console.WriteLine("Hello")); 
    executor.Execute(()=>Console.WriteLine(",")); 
    executor.Execute(()=>Console.WriteLine("World")); 
} 

public class MyExecutor 
{ 
    private Task _current = Task.FromResult(0); 

    public void Execute(Action action) 
    { 
     _current=_current.ContinueWith(prev=>action()); 
    } 
} 

UPD

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

void Main() 
{ 
    var executor = new MyExecutor(); 
    executor.Execute(() => Console.WriteLine("Hello")); 
    executor.Execute(() => Thread.Sleep(100)); 
    executor.Execute(() => Console.WriteLine(",")); 
    executor.Execute(() => { throw new Exception(); }); 
    executor.Execute(() => Console.WriteLine("World")); 
    executor.Execute(() => Thread.Sleep(100)); 

    executor.WaitCurrent(); 

    Console.WriteLine($"{nameof(MyExecutor.Total)}:{executor.Total}"); 
    Console.WriteLine($"{nameof(MyExecutor.Finished)}:{executor.Finished}"); 
    Console.WriteLine($"{nameof(MyExecutor.Failed)}:{executor.Failed}"); 
} 

public class MyExecutor 
{ 
    private Task _current = Task.FromResult(0); 
    private int _failed = 0; 
    private int _finished = 0; 
    private int _total = 0; 
    private object _locker = new object(); 

    public void WaitCurrent() 
    { 
     _current.Wait();   
    } 

    public int Total 
    { 
     get { return _total; } 
    } 

    public int Finished 
    { 
     get { return _finished; } 
    } 

    public int Failed 
    { 
     get { return _failed; } 
    } 

    public void Execute(Action action) 
    { 
     lock (_locker) // not sure that lock it is the best way here 
     { 
      _total++; 
      _current = _current.ContinueWith(prev => SafeExecute(action)); 
     } 
    } 

    private void SafeExecute(Action action) 
    { 
     try 
     {    
      action(); 
     } 
     catch 
     { 
      Interlocked.Increment(ref _failed); 
     } 
     finally 
     { 
      Interlocked.Increment(ref _finished); 
     } 
    } 
} 
+0

Кажется, что это сработает, если вы только когда-либо добавляете «Задачи» из одного потока. В случае с OP это нормально, но я не думаю, что он работал бы из нескольких потоков «производителя», и вы не получите желания OP видеть, сколько заданий поставлено в очередь. Изменить: также, что произойдет, если задачи будут выполнены, будет ли выполняться немедленное добавление ContinueWith' Task'? –

+0

@KevinAnderson обновленный ответ. Задание, если оно закончено, продолжение начнется немедленно. – tym32167

+0

Интересные дополнения, но ваша переменная '_total' не будет обновляться до тех пор, пока задача не начнет работать, поэтому я не думаю, что это даст ему номер' Task', который он поставил в очередь. В принципе, после всех этих усилий я думаю, что предложение от @Evk 'BlockingCollection'' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' ' Но другие могут не согласиться. –