Hiç bitmeyen bir görevi yerine getirmenin doğru yolu. (Zamanlayıcılar - Görev)


93

Bu nedenle, uygulamamın, uygulama çalıştığı veya iptal istendiği sürece neredeyse sürekli olarak (her çalıştırma arasında 10 saniyelik bir duraklama ile) bir eylem gerçekleştirmesi gerekiyor. Yapması gereken iş 30 saniyeye kadar sürebilmektedir.

Bir System.Timers.Timer kullanmak ve önceki "onay" tamamlanmadan eylemi gerçekleştirmediğinden emin olmak için AutoReset kullanmak daha mı iyi?

Veya LongRunning modunda bir iptal belirteci ile genel bir Görev kullanmalı mıyım ve içinde çağrılar arasında 10 saniyelik Thread.Sleep ile işi yapan eylemi çağıran düzenli bir sonsuz while döngüsü mü olmalı? Async / await modeline gelince, işten herhangi bir dönüş değerim olmadığı için burada uygun olacağından emin değilim.

CancellationTokenSource wtoken;
Task task;

void StopWork()
{
    wtoken.Cancel();

    try 
    {
        task.Wait();
    } catch(AggregateException) { }
}

void StartWork()
{
    wtoken = new CancellationTokenSource();

    task = Task.Factory.StartNew(() =>
    {
        while (true)
        {
            wtoken.Token.ThrowIfCancellationRequested();
            DoWork();
            Thread.Sleep(10000);
        }
    }, wtoken, TaskCreationOptions.LongRunning);
}

void DoWork()
{
    // Some work that takes up to 30 seconds but isn't returning anything.
}

veya AutoReset özelliğini kullanırken basit bir zamanlayıcı kullanın ve iptal etmek için .Stop () 'u çağırın.


Görev, başarmaya çalıştığınız şeyi göz önünde bulundurarak aşırı bir beceri gibi görünüyor. en.wikipedia.org/wiki/KISS_principle . OnTick () 'in başlangıcında zamanlayıcıyı durdurun, yapmamanız gereken bir şey olup olmadığını görmek için bir bool'u kontrol edin, çalışın, işiniz bittiğinde Timer'ı yeniden başlatın.
Mike Trusov

Yanıtlar:


94

Bunun için TPL Dataflow'u kullanırım (çünkü .NET 4.5'i kullanıyorsunuz ve Taskdahili olarak kullanıyor ). ActionBlock<TInput>İşlemi işlendikten ve uygun bir süre bekledikten sonra kendisine öğeleri gönderen bir öğeyi kolayca oluşturabilirsiniz .

Öncelikle, hiç bitmeyen görevinizi yaratacak bir fabrika oluşturun:

ITargetBlock<DateTimeOffset> CreateNeverEndingTask(
    Action<DateTimeOffset> action, CancellationToken cancellationToken)
{
    // Validate parameters.
    if (action == null) throw new ArgumentNullException("action");

    // Declare the block variable, it needs to be captured.
    ActionBlock<DateTimeOffset> block = null;

    // Create the block, it will call itself, so
    // you need to separate the declaration and
    // the assignment.
    // Async so you can wait easily when the
    // delay comes.
    block = new ActionBlock<DateTimeOffset>(async now => {
        // Perform the action.
        action(now);

        // Wait.
        await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken).
            // Doing this here because synchronization context more than
            // likely *doesn't* need to be captured for the continuation
            // here.  As a matter of fact, that would be downright
            // dangerous.
            ConfigureAwait(false);

        // Post the action back to the block.
        block.Post(DateTimeOffset.Now);
    }, new ExecutionDataflowBlockOptions { 
        CancellationToken = cancellationToken
    });

    // Return the block.
    return block;
}

ActionBlock<TInput>Bir DateTimeOffsetyapı almak için seçtim ; bir tür parametresi iletmeniz gerekir ve bu, bazı yararlı durumları da iletebilir (isterseniz, durumun doğasını değiştirebilirsiniz).

Ayrıca, ActionBlock<TInput>varsayılan olarak bir seferde yalnızca bir öğeyi işlediğini unutmayın , bu nedenle yalnızca bir eylemin işleneceği garanti edilir (yani, uzantı yöntemini kendi başına geri çağırdığında yeniden giriş yapmanız gerekmez ).Post

Ayrıca CancellationTokenyapıyı hem yapıcıya hem ActionBlock<TInput>de Task.Delayyöntem çağrısına geçirdim ; İşlem iptal edilirse, iptal mümkün olan ilk fırsatta gerçekleşecektir.

Oradan, uygulanan ITargetBlock<DateTimeoffset>arabirimi depolamak için kodunuzun yeniden düzenlenmesi kolaydır ActionBlock<TInput>(bu, tüketiciler olan blokları temsil eden üst düzey soyutlamadır ve Postuzantı yöntemine yapılan bir çağrı yoluyla tüketimi tetikleyebilmek istersiniz ):

CancellationTokenSource wtoken;
ActionBlock<DateTimeOffset> task;

Sizin StartWorkyöntemi:

void StartWork()
{
    // Create the token source.
    wtoken = new CancellationTokenSource();

    // Set the task.
    task = CreateNeverEndingTask(now => DoWork(), wtoken.Token);

    // Start the task.  Post the time.
    task.Post(DateTimeOffset.Now);
}

Ve sonra StopWorkyönteminiz:

void StopWork()
{
    // CancellationTokenSource implements IDisposable.
    using (wtoken)
    {
        // Cancel.  This will cancel the task.
        wtoken.Cancel();
    }

    // Set everything to null, since the references
    // are on the class level and keeping them around
    // is holding onto invalid state.
    wtoken = null;
    task = null;
}

TPL Dataflow'u neden burada kullanmak istersiniz? Birkaç neden:

Endişelerin ayrılması

CreateNeverEndingTaskYöntem şimdi çok konuşmak için "hizmet" yaratan bir fabrikasıdır. Ne zaman başlayıp duracağını siz kontrol edersiniz ve tamamen bağımsızdır. Zamanlayıcının durum kontrolünü kodunuzun diğer yönleriyle iç içe geçirmeniz gerekmez. Basitçe bloğu yaratırsınız, başlatırsınız ve işiniz bittiğinde durdurursunuz.

İş parçacıkları / görevler / kaynakların daha verimli kullanımı

TPL veri akışındaki bloklar için varsayılan zamanlayıcı Task, iş parçacığı havuzu olan a için aynıdır . Kullanarak ActionBlock<TInput>Eyleminizi yanı sıra bir çağrı işlemek için Task.Delay, size aslında hiçbir şey yapmıyorsun kullanmakta olduğu iplik kontrolünü elde ediyoruz. Kabul Taskedilirse , bu aslında devamı işleyecek yeniyi ortaya çıkardığınızda bir miktar ek yüke yol açar , ancak bunu sıkı bir döngüde işlemediğiniz düşünülürse bu küçük olmalıdır (çağrılar arasında on saniye bekliyorsunuz).

Eğer DoWorkgerçekte (bir döndüren içinde, yani awaitable yapılabilir fonksiyonu Task), o zaman (muhtemelen) daha yukarıda fabrika yöntemini değişiklikler yaparak bu duruma bir almaya Func<DateTimeOffset, CancellationToken, Task>bir yerine Action<DateTimeOffset>, şöyle:

ITargetBlock<DateTimeOffset> CreateNeverEndingTask(
    Func<DateTimeOffset, CancellationToken, Task> action, 
    CancellationToken cancellationToken)
{
    // Validate parameters.
    if (action == null) throw new ArgumentNullException("action");

    // Declare the block variable, it needs to be captured.
    ActionBlock<DateTimeOffset> block = null;

    // Create the block, it will call itself, so
    // you need to separate the declaration and
    // the assignment.
    // Async so you can wait easily when the
    // delay comes.
    block = new ActionBlock<DateTimeOffset>(async now => {
        // Perform the action.  Wait on the result.
        await action(now, cancellationToken).
            // Doing this here because synchronization context more than
            // likely *doesn't* need to be captured for the continuation
            // here.  As a matter of fact, that would be downright
            // dangerous.
            ConfigureAwait(false);

        // Wait.
        await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken).
            // Same as above.
            ConfigureAwait(false);

        // Post the action back to the block.
        block.Post(DateTimeOffset.Now);
    }, new ExecutionDataflowBlockOptions { 
        CancellationToken = cancellationToken
    });

    // Return the block.
    return block;
}

Elbette, CancellationTokenyönteminizi (eğer kabul ederse) burada yapmak iyi bir uygulama olacaktır .

Bu, daha sonra DoWorkAsyncaşağıdaki imzaya sahip bir yönteme sahip olacağınız anlamına gelir :

Task DoWorkAsync(CancellationToken cancellationToken);

StartWorkYönteme aktarılan yeni imzayı hesaba katmak için yöntemi değiştirmeniz gerekir (sadece biraz ve burada endişelerin ayrılmasını aklamazsınız), CreateNeverEndingTaskşöyle:

void StartWork()
{
    // Create the token source.
    wtoken = new CancellationTokenSource();

    // Set the task.
    task = CreateNeverEndingTask((now, ct) => DoWorkAsync(ct), wtoken.Token);

    // Start the task.  Post the time.
    task.Post(DateTimeOffset.Now, wtoken.Token);
}

Merhaba, bu uygulamayı deniyorum ama sorunlarla karşılaşıyorum. DoWork'üm bağımsız değişken almazsa, task = CreateNeverEndingTask (şimdi => DoWork (), wtoken.Token); bana bir yapı hatası veriyor (tür uyuşmazlığı). Öte yandan, DoWork'üm bir DateTimeOffset parametresi alırsa, bu aynı satır bana farklı bir yapı hatası veriyor ve DoWork için hiçbir aşırı yüklemenin 0 bağımsız değişken almadığını söylüyor. Bunu çözmeme yardım eder misin lütfen?
Bovaz

1
Aslında, görev atadığım satıra bir atama ekleyerek ve parametreyi DoWork'e ileterek sorunumu çözdüm: task = (ActionBlock <DateTimeOffset>) CreateNeverEndingTask (şimdi => DoWork (şimdi), wtoken.Token);
Bovaz

Ayrıca "ActionBlock <DateTimeOffset> görevinin" türünü de değiştirebilirsiniz; ITargetBlock <DateTimeOffset> görevine;
XOR

1
Bunun hafızayı sonsuza kadar tahsis edeceğine ve sonunda bir taşmaya yol açacağına inanıyorum.
Nate Gardner

@NateGardner Hangi bölümde?
casperOne

76

Yeni Görev tabanlı arabirimi böyle şeyler yapmak için çok basit buluyorum - Timer sınıfını kullanmaktan bile daha kolay.

Örneğinizde yapabileceğiniz bazı küçük ayarlamalar var. Onun yerine:

task = Task.Factory.StartNew(() =>
{
    while (true)
    {
        wtoken.Token.ThrowIfCancellationRequested();
        DoWork();
        Thread.Sleep(10000);
    }
}, wtoken, TaskCreationOptions.LongRunning);

Bunu yapabilirsiniz:

task = Task.Run(async () =>  // <- marked async
{
    while (true)
    {
        DoWork();
        await Task.Delay(10000, wtoken.Token); // <- await with cancellation
    }
}, wtoken.Token);

Bu şekilde, iptal, bitmesini Task.Delaybeklemek yerine, içinde ise anında gerçekleşir Thread.Sleep.

Ayrıca, Task.Delayover kullanmak Thread.Sleep, uyku süresince hiçbir şey yapmadan bir ipliği bağlamadığınız anlamına gelir.

Mümkünse, DoWork()bir iptal belirtecini de kabul edebilirsiniz ve iptal çok daha hızlı yanıt verir.


1
Task.Factory.StartNew parametresi olarak zaman uyumsuz lambda kullanırsanız hangi görevi alacağınızı öğrenin - blogs.msdn.com/b/pfxteam/archive/2011/10/24/10229468.aspx Ne zaman görev yaptığınızda.Bekle ( ); iptal talep edildikten sonra, yanlış görevi bekliyor olacaksınız.
Lukas Pirkl

Evet, bu aslında doğru aşırı yüklemeye sahip Task.Run şimdi olmalıdır.
porges

Http://blogs.msdn.com/b/pfxteam/archive/2011/10/24/10229468.aspx'e göre Task.Runiş parçacığı havuzunu kullanıyor gibi görünüyor , bu nedenle with Task.Runyerine kullanan örneğiniz tam olarak aynı şeyi yapmıyor - Seçeneği kullanmak için göreve ihtiyacım olsaydı , gösterdiğiniz gibi kullanamaz mıydım yoksa bir şey mi kaçırıyorum? Task.Factory.StartNewTaskCreationOptions.LongRunningLongRunningTask.Run
Jeff

@Lumirris: Async / await'in amacı, yürütüldüğü tüm süre boyunca bir iş parçacığını bağlamamaktır (burada, Gecikme çağrısı sırasında görev bir iş parçacığı kullanmıyor). Dolayısıyla kullanmak LongRunning, iş parçacığı bağlamamak amacıyla bir nevi uyumsuzdur. Kendi iş parçacığı üzerinde çalışmayı garanti etmek istiyorsanız , onu kullanabilirsiniz, ancak burada çoğu zaman uyuyan bir iş parçacığı başlatacaksınız. Kullanım durumu nedir?
porges

1
@Lumirris: haklısın, bunu belirtmenin bir yolu yok; Task.Run ( Referencesource.microsoft.com/#mscorlib/system/threading/Tasks/… ) temelde Task.Factory.StartNew ( referenceource.microsoft.com/#mscorlib/system/threading/Tasks/… ) ile aynıdır. varsayılan seçenekler. (Ancak DenyChildAttach'ı belirtir.)
porges

5

İşte bulduğum şey:

  • Yapmak istediğiniz işle yöntemi devralın NeverEndingTaskve geçersiz kılın ExecutionCore.
  • Değiştirme ExecutionLoopDelayMs, örneğin bir geri çekilme algoritması kullanmak istiyorsanız, döngüler arasındaki zamanı ayarlamanıza olanak tanır.
  • Start/Stop görevi başlatmak / durdurmak için senkronize bir arayüz sağlar.
  • LongRunningher bir özel iş parçacığı alacağınız anlamına gelir NeverEndingTask.
  • Bu sınıf, ActionBlockyukarıdaki temel çözümden farklı olarak bir döngüde bellek ayırmaz .
  • Aşağıdaki kod taslaktır, üretim kodu olması gerekmez :)

:

public abstract class NeverEndingTask
{
    // Using a CTS allows NeverEndingTask to "cancel itself"
    private readonly CancellationTokenSource _cts = new CancellationTokenSource();

    protected NeverEndingTask()
    {
         TheNeverEndingTask = new Task(
            () =>
            {
                // Wait to see if we get cancelled...
                while (!_cts.Token.WaitHandle.WaitOne(ExecutionLoopDelayMs))
                {
                    // Otherwise execute our code...
                    ExecutionCore(_cts.Token);
                }
                // If we were cancelled, use the idiomatic way to terminate task
                _cts.Token.ThrowIfCancellationRequested();
            },
            _cts.Token,
            TaskCreationOptions.DenyChildAttach | TaskCreationOptions.LongRunning);

        // Do not forget to observe faulted tasks - for NeverEndingTask faults are probably never desirable
        TheNeverEndingTask.ContinueWith(x =>
        {
            Trace.TraceError(x.Exception.InnerException.Message);
            // Log/Fire Events etc.
        }, TaskContinuationOptions.OnlyOnFaulted);

    }

    protected readonly int ExecutionLoopDelayMs = 0;
    protected Task TheNeverEndingTask;

    public void Start()
    {
       // Should throw if you try to start twice...
       TheNeverEndingTask.Start();
    }

    protected abstract void ExecutionCore(CancellationToken cancellationToken);

    public void Stop()
    {
        // This code should be reentrant...
        _cts.Cancel();
        TheNeverEndingTask.Wait();
    }
}
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.