कभी समाप्त न होने वाले कार्य को कार्यान्वित करने का उचित तरीका। (टाइमर बनाम टास्क)


92

इसलिए, जब तक ऐप चल रहा है या रद्द करने का अनुरोध किया जाता है, तब तक मेरे ऐप को लगभग निरंतर (10 सेकंड या प्रत्येक रन के बीच में) एक क्रिया करने की आवश्यकता है। जिस काम को करने की आवश्यकता है, उसमें 30 सेकंड तक लेने की संभावना है।

क्या System.Timers.Timer का उपयोग करना बेहतर है और AutoReset का उपयोग यह सुनिश्चित करने के लिए करें कि यह पिछले "टिक" के पूरा होने से पहले कार्रवाई नहीं करता है।

या क्या मुझे एक कैंसिलेशन टोकन के साथ लॉन्ग-डायनामिक मोड में एक सामान्य टास्क का उपयोग करना चाहिए, और एक नियमित रूप से अनंत है जबकि लूप इसके अंदर है जो 10 सेकंड थ्रेड के साथ कार्य कर रहा है। कॉल के बीच सोएं? Async / प्रतीक्षा मॉडल के लिए, मुझे यकीन नहीं है कि यह यहां उचित होगा क्योंकि मेरे पास काम से कोई वापसी मूल्य नहीं है।

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.
}

या अपने AutoReset संपत्ति का उपयोग करते समय एक साधारण टाइमर का उपयोग करें, और इसे रद्द करने के लिए .Stop () कॉल करें?


टास्क एक ओवरकिल की तरह लगता है कि आप क्या हासिल करने की कोशिश कर रहे हैं। en.wikipedia.org/wiki/KISS_principle । OnTick () की शुरुआत में टाइमर बंद करो, यह देखने के लिए एक बूल की जांच करें कि क्या आपको कुछ नहीं करना चाहिए, काम नहीं करना चाहिए, जब आप कर रहे हों तब टाइमर को पुनरारंभ करें।
माइक ट्रूसोव

जवाबों:


94

मैं इसके लिए TPL डेटाफ़्लो का उपयोग करूँगा (क्योंकि आप .NET 4.5 का उपयोग कर रहे हैं और यह Taskआंतरिक रूप से उपयोग करता है )। ActionBlock<TInput>यह संसाधित होने के बाद आप आसानी से एक पोस्ट पोस्ट आइटम बना सकते हैं और यह उचित मात्रा में इंतजार कर रहा है।

सबसे पहले, एक कारखाना बनाएँ जो आपके कभी न खत्म होने वाले कार्य का निर्माण करेगा:

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>एक DateTimeOffsetसंरचना लेने के लिए चुना है ; आपको एक प्रकार का पैरामीटर पास करना होगा, और यह कुछ उपयोगी स्थिति को पारित कर सकता है (यदि आप चाहें तो राज्य की प्रकृति को बदल सकते हैं)।

यह भी ध्यान दें कि ActionBlock<TInput>डिफ़ॉल्ट रूप से एक समय में केवल एक आइटम संसाधित करता है, इसलिए आपको गारंटी दी जाती है कि केवल एक ही कार्रवाई संसाधित की जाएगी (इसका अर्थ है, आपको विस्तार विधि को कॉल करने पर पुनर्व्यवस्थितता से नहीं निपटना होगाPost को स्वयं वापस )।

मैंने CancellationTokenसंरचना के दोनों ActionBlock<TInput>और Task.Delayविधि कॉल के लिए संरचना को पारित कर दिया है ; यदि प्रक्रिया रद्द हो जाती है, तो पहले संभावित अवसर पर रद्द कर दिया जाएगा।

वहां से, आपके द्वारा कार्यान्वित ITargetBlock<DateTimeoffset>इंटरफ़ेस को स्टोर करने के लिए यह आपके कोड का एक आसान रीफ़ैक्टरिंग है ActionBlock<TInput>(यह उच्च-स्तरीय एब्सट्रैक्ट है जो उपभोक्ता हैं ब्लॉकों का प्रतिनिधित्व करते हैं, और आप कॉल को Postएक्सटेंशन विधि के माध्यम से उपभोग को ट्रिगर करना चाहते हैं ):

CancellationTokenSource wtoken;
ActionBlock<DateTimeOffset> task;

आपका StartWorkतरीका:

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);
}

और फिर आपकी StopWorkविधि:

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 का उपयोग क्यों करना चाहेंगे? कुछ कारण:

चिंताओ का विभाजन

CreateNeverEndingTaskविधि अब एक कारखाने हैं जो आपके "सेवा" इतनी बात करने के लिए बनाता है। जब आप शुरू करते हैं और रुक जाते हैं, तो आप इसे नियंत्रित करते हैं और यह पूरी तरह से आत्म-निहित है। आपको अपने कोड के अन्य पहलुओं के साथ टाइमर के राज्य नियंत्रण को इंटरवाइव करने की आवश्यकता नहीं है। आप बस ब्लॉक बनाते हैं, इसे शुरू करते हैं, और जब आप पूरा कर लेते हैं तो इसे रोक देते हैं।

धागे / कार्यों / संसाधनों का अधिक कुशल उपयोग

TPL डेटा प्रवाह में ब्लॉक के लिए डिफ़ॉल्ट अनुसूचक एक के लिए समान है Task, जो थ्रेड पूल है। ActionBlock<TInput>अपनी कार्रवाई को संसाधित करने के लिए और साथ ही कॉल टू Task.Delayका उपयोग करके, आप उस थ्रेड का नियंत्रण नियंत्रित कर रहे हैं जिसका उपयोग आप तब कर रहे थे जब आप वास्तव में कुछ भी नहीं कर रहे थे। दी, यह वास्तव में कुछ ओवरहेड की ओर जाता है जब आप नए को स्पॉन करते हैं Taskजो निरंतरता को संसाधित करेगा, लेकिन यह छोटा होना चाहिए, यह देखते हुए कि आप इसे एक तंग लूप में संसाधित नहीं कर रहे हैं (आप इनवोक के बीच दस सेकंड इंतजार कर रहे हैं)।

यदि DoWorkफ़ंक्शन वास्तव में प्रतीक्षा योग्य बनाया जा सकता है (अर्थात्, इसमें रिटर्न होता है Task), तो आप (संभवतः) एक के Func<DateTimeOffset, CancellationToken, Task>बजाय लेने के लिए ऊपर फैक्ट्री विधि को ट्वीक करके इसे और भी अधिक अनुकूलित कर सकते हैं Action<DateTimeOffset>, जैसे:

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;
}

बेशक, CancellationTokenआपकी विधि के माध्यम से बुनाई करना अच्छा होगा (यदि यह एक को स्वीकार करता है), जो यहां किया जाता है।

इसका मतलब है कि आपके पास DoWorkAsyncनिम्नलिखित हस्ताक्षर के साथ एक विधि होगी :

Task DoWorkAsync(CancellationToken cancellationToken);

आपको बदलना होगा (केवल थोड़ा सा, और आप यहां से चिंताओं को अलग नहीं कर रहे हैं) StartWorkविधि को पारित करने के लिए नए हस्ताक्षर करने की CreateNeverEndingTaskविधि, जैसे कि:

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);
}

नमस्कार, मैं इस कार्यान्वयन की कोशिश कर रहा हूं लेकिन मैं मुद्दों का सामना कर रहा हूं। यदि मेरा DoWork कोई तर्क नहीं लेता है, कार्य = CreateNeverEndingTask (अब => DoWork (), wtoken.Token); मुझे एक बिल्ड एरर देता है (टाइप मिसमैच)। दूसरी ओर, अगर मेरा DoWork एक DateTimeOffset पैरामीटर लेता है, तो वही लाइन मुझे एक अलग बिल्ड एरर देती है, जो मुझे बताती है कि DoWork के लिए कोई अधिभार 0 तर्क नहीं लेता है। क्या आप मुझे यह जानने में मदद करेंगे?
बोवाज

1
वास्तव में, मैंने एक मुद्दे को उस पंक्ति में जोड़कर हल किया जहां मैं कार्य असाइन करता हूं और DoWork को पैरामीटर पास करता हूं: कार्य = (ActionBlock <DateTimeOffset>) CreateNeverEndingTask (अब => DoWork (अब, wtoken.Token));
बोवाज

आप "ActionBlock <DateTimeOffset> कार्य" का प्रकार भी बदल सकते हैं; को ITargetBlock <DateTimeOffset> कार्य;
XOR

1
मेरा मानना ​​है कि यह हमेशा के लिए स्मृति आवंटित करने की संभावना है, इस प्रकार अंततः एक अतिप्रवाह की ओर जाता है।
नैट गार्डनर

@ नैटगार्डनर किस हिस्से में?
कैस्पर ओने

75

मुझे लगता है कि नए टास्क-आधारित इंटरफ़ेस को इस तरह के काम करने के लिए बहुत सरल होना चाहिए - टाइमर वर्ग का उपयोग करने की तुलना में भी आसान।

कुछ छोटे समायोजन हैं जो आप अपने उदाहरण के लिए कर सकते हैं। के बजाय:

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

तुम यह केर सकते हो:

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

इस तरह से रद्द करने के अंदर होने पर तुरंत होगा Task.DelayThread.Sleep समाप्त होने की प्रतीक्षा करने के बजाय, ।

इसके अलावा, का उपयोग कर Task.Delay से अधिक Thread.Sleepसाधन आप एक धागा नींद की अवधि के लिए कुछ नहीं कर रहे बांधने ऊपर नहीं हैं।

यदि आप सक्षम हैं, तो आप DoWork()एक रद्दकरण टोकन भी स्वीकार कर सकते हैं , और रद्द करना अधिक उत्तरदायी होगा।


1
बाहर whatch क्या काम आप यदि आप Task.Factory.StartNew के पैरामीटर के रूप में async लैम्ब्डा का उपयोग मिलेगा - blogs.msdn.com/b/pfxteam/archive/2011/10/24/10229468.aspx आप task.Wait करते हैं ( ); रद्द करने का अनुरोध करने के बाद, आप गलत कार्य की प्रतीक्षा करेंगे।
लुकास पिरकल

हां, यह वास्तव में टास्क होना चाहिए। अब सही ओवरलोड है।
पोर्स

के अनुसार http://blogs.msdn.com/b/pfxteam/archive/2011/10/24/10229468.aspx यह कैसा दिखता Task.Runका उपयोग कर का उपयोग करता है थ्रेड पूल, अपने उदाहरण तो Task.Runबजाय Task.Factory.StartNewसाथ TaskCreationOptions.LongRunningबिल्कुल वही बात भी नहीं करता है - अगर मुझे LongRunningविकल्प का उपयोग करने के लिए कार्य की आवश्यकता है , तो क्या मैं Task.Runआपके द्वारा दिखाए गए जैसे उपयोग नहीं कर पाऊंगा , या क्या मुझे कुछ याद आ रहा है?
जेफ

@Lumirris: async / प्रतीक्षा का बिंदु पूरे समय के लिए एक धागा बांधने से बचना है जो इसे निष्पादित कर रहा है (यहां, डेल कॉल के दौरान कार्य एक थ्रेड का उपयोग नहीं कर रहा है)। तो LongRunningथ्रेडिंग बांधने के लक्ष्य के साथ प्रयोग करना असंगत है। यदि आप अपने स्वयं के धागे पर चलने की गारंटी देना चाहते हैं , तो आप इसका उपयोग कर सकते हैं, लेकिन यहां आप एक ऐसा धागा शुरू करने जा रहे हैं जो अधिकांश समय सो रहा है। उपयोग मामला क्या है?
porges

@ पॉर्ज़ पॉइंट लिया गया। मेरा उपयोग मामला एक अनंत लूप चलाने वाला कार्य होगा, जिसमें प्रत्येक पुनरावृत्ति कार्य का एक हिस्सा करेगा, और अगले पुनरावृत्ति पर दूसरा चक कार्य करने से पहले 2 सेकंड के लिए 'आराम' करेगा। यह हमेशा के लिए चल रहा है, लेकिन नियमित 2 सेकंड का ब्रेक लेना। मेरी टिप्पणी, हालांकि, इस बारे में अधिक थी कि क्या आप इसे सिंटैक्स LongRunningका उपयोग करते हुए निर्दिष्ट कर सकते हैं Task.Run। दस्तावेज़ीकरण से, ऐसा लगता है Task.Runकि क्लीनर सिंटैक्स है, जब तक आप डिफ़ॉल्ट सेटिंग्स के उपयोग से खुश हैं। वहाँ इसके साथ एक अधिभार प्रतीत नहीं होता है जो एक TaskCreationOptionsतर्क लेता है ।
जेफ

4

यहां वह है जो मैंने जुटाया:

  • जिस कार्य को आप करना चाहते NeverEndingTaskहैं, उस ExecutionCoreविधि से इनहेरिट और ओवरराइड करें।
  • ExecutionLoopDelayMsयदि आप बैकऑफ़ एल्गोरिथ्म का उपयोग करना चाहते हैं तो परिवर्तन आपको लूप के बीच के समय को समायोजित करने की अनुमति देता है।
  • Start/Stop कार्य शुरू / बंद करने के लिए एक तुल्यकालिक इंटरफ़ेस प्रदान करें।
  • LongRunningइसका मतलब है कि आपको प्रति एक समर्पित धागा मिलेगा NeverEndingTask
  • यह वर्ग ActionBlockउपरोक्त समाधान के विपरीत लूप में मेमोरी आवंटित नहीं करता है।
  • नीचे दिया गया कोड स्केच है, जरूरी नहीं कि उत्पादन कोड :)

:

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();
    }
}
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.