วิธีที่เหมาะสมในการดำเนินงานที่ไม่มีวันสิ้นสุด (ตัวจับเวลาเทียบกับงาน)


92

ดังนั้นแอปของฉันจำเป็นต้องดำเนินการเกือบจะต่อเนื่อง (โดยหยุดชั่วคราวประมาณ 10 วินาทีระหว่างการรันแต่ละครั้ง) ตราบเท่าที่แอปกำลังทำงานอยู่หรือมีการร้องขอการยกเลิก งานที่ต้องทำมีความเป็นไปได้ที่จะใช้เวลาถึง 30 วินาที

จะดีกว่าไหมหากใช้ System.Timers.Timer และใช้ AutoReset เพื่อให้แน่ใจว่าจะไม่ดำเนินการก่อนที่ "ติ๊ก" ก่อนหน้าจะเสร็จสิ้น

หรือฉันควรใช้งานทั่วไปในโหมด LongRunning ที่มีโทเค็นการยกเลิกและมีปกติไม่มีที่สิ้นสุดในขณะที่วนซ้ำอยู่ภายในเรียกการดำเนินการที่ทำงานกับเธรด 10 วินาทีนอนหลับระหว่างการโทร? สำหรับโมเดล async / await ฉันไม่แน่ใจว่ามันจะเหมาะสมที่นี่เพราะฉันไม่มีค่าตอบแทนจากการทำงาน

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 () ตรวจสอบบูลเพื่อดูว่าคุณควรจะทำอะไรหรือไม่ทำงานเริ่มตัวจับเวลาใหม่เมื่อคุณทำเสร็จแล้ว
Mike Trusov

คำตอบ:


94

ฉันจะใช้TPL Dataflowสำหรับสิ่งนี้ (เนื่องจากคุณใช้. 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โครงสร้าง ; คุณต้องส่งพารามิเตอร์ type และมันอาจผ่านสถานะที่มีประโยชน์บางอย่าง (คุณสามารถเปลี่ยนลักษณะของสถานะได้หากต้องการ)

นอกจากนี้ยังทราบว่าActionBlock<TInput>กระบวนการเริ่มต้นเพียงหนึ่งรายการในเวลาดังนั้นคุณจะรับประกันว่ามีเพียงหนึ่งการกระทำจะถูกประมวลผล (หมายถึงคุณจะไม่ต้องจัดการกับreentrancyเมื่อมันเรียก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 จะเหมือนกันสำหรับ a Taskซึ่งเป็นเธรดพูล ด้วยการใช้ActionBlock<TInput>เพื่อประมวลผลการกระทำของคุณเช่นเดียวกับการโทรTask.Delayคุณจะสามารถควบคุมเธรดที่คุณใช้เมื่อคุณไม่ได้ทำอะไรเลย จริงอยู่สิ่งนี้นำไปสู่ค่าใช้จ่ายบางส่วนเมื่อคุณสร้างสิ่งใหม่Taskที่จะประมวลผลความต่อเนื่อง แต่ควรมีขนาดเล็กเนื่องจากคุณไม่ได้ประมวลผลสิ่งนี้ในวงที่แน่น (คุณกำลังรอสิบวินาทีระหว่างการเรียก)

หากDoWorkฟังก์ชันนั้นสามารถรอได้จริง (กล่าวคือในกรณีที่ส่งกลับ a 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 ของฉันไม่มีอาร์กิวเมนต์ task = CreateNeverEndingTask (now => DoWork (), wtoken.Token); ทำให้ฉันมีข้อผิดพลาดในการสร้าง (ประเภทไม่ตรงกัน) ในทางกลับกันถ้า DoWork ของฉันใช้พารามิเตอร์ DateTimeOffset บรรทัดเดียวกันนั้นทำให้ฉันมีข้อผิดพลาดในการสร้างที่แตกต่างกันโดยบอกฉันว่า DoWork ไม่มีการโอเวอร์โหลดจะรับ 0 อาร์กิวเมนต์ คุณช่วยฉันคิดออกได้ไหม
Bovaz

1
อันที่จริงฉันแก้ไขปัญหาของฉันโดยการเพิ่มนักแสดงในบรรทัดที่ฉันมอบหมายงานและส่งผ่านพารามิเตอร์ไปยัง DoWork: task = (ActionBlock <DateTimeOffset>) CreateNeverEndingTask (now => DoWork (now), wtoken.Token);
Bovaz

คุณยังสามารถเปลี่ยนประเภทของงาน "ActionBlock <DateTimeOffset>;" ไปยังงาน ITargetBlock <DateTimeOffset>;
XOR

1
ฉันเชื่อว่าสิ่งนี้มีแนวโน้มที่จะจัดสรรหน่วยความจำตลอดไปซึ่งนำไปสู่การล้นในที่สุด
Nate Gardner

@NateGardner ในส่วนไหน?
casperOne

75

ฉันพบว่าอินเทอร์เฟซตามงานใหม่นั้นง่ายมากสำหรับการทำสิ่งนี้ - ง่ายกว่าการใช้คลาส Timer ด้วยซ้ำ

มีการปรับเปลี่ยนเล็กน้อยกับตัวอย่างของคุณ แทน:

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.Delayแทนที่จะต้องรอThread.Sleepให้เสร็จสิ้น

นอกจากนี้การใช้Task.Delayover Thread.Sleepหมายความว่าคุณจะไม่ผูกด้ายโดยไม่ได้ทำอะไรเลยตลอดช่วงเวลาของการนอนหลับ

หากทำได้คุณสามารถDoWork()ยอมรับโทเค็นการยกเลิกได้เช่นกันและการยกเลิกจะตอบสนองได้ดีกว่ามาก


1
สิ่งที่คุณจะได้รับคืออะไรถ้าคุณใช้ async lambda เป็นพารามิเตอร์ของ Task.Factory.StartNew - blogs.msdn.com/b/pfxteam/archive/2011/10/24/10229468.aspxเมื่อคุณทำงานรอ ( ); หลังจากขอยกเลิกคุณจะต้องรอให้ทำงานที่ไม่ถูกต้อง
Lukas Pirkl

ใช่นี่ควรเป็นงานจริงเรียกใช้ตอนนี้ซึ่งมีโอเวอร์โหลดที่ถูกต้อง
porges

อ้างอิงจากhttp://blogs.msdn.com/b/pfxteam/archive/2011/10/24/10229468.aspxดูเหมือนว่าTask.Runจะใช้เธรดพูลดังนั้นตัวอย่างของคุณที่ใช้Task.RunแทนTask.Factory.StartNewด้วยTaskCreationOptions.LongRunningจะไม่ทำสิ่งเดียวกัน - ถ้าฉันต้องการงานเพื่อใช้LongRunningตัวเลือกนี้ฉันจะใช้ไม่ได้Task.Runเหมือนที่คุณแสดงไว้หรือฉันทำอะไรพลาดไป?
Jeff

@ Lumirris: จุด async / await คือหลีกเลี่ยงการผูกเธรดตลอดเวลาที่กำลังดำเนินการ (ที่นี่ในระหว่าง Delay call งานไม่ได้ใช้เธรด) ดังนั้นการใช้LongRunningจึงไม่เข้ากันกับเป้าหมายของการไม่ผูกเธรด หากคุณต้องการรับประกันว่าจะทำงานบนเธรดของตัวเองคุณสามารถใช้ได้ แต่ที่นี่คุณจะเริ่มต้นเธรดที่หลับอยู่เกือบตลอดเวลา กรณีการใช้งานคืออะไร?
porges

@ Porges Point ถ่าย. กรณีการใช้งานของฉันจะเป็นงานที่ใช้การวนซ้ำแบบไม่สิ้นสุดซึ่งการวนซ้ำแต่ละครั้งจะทำงานเป็นชิ้น ๆ และ 'ผ่อนคลาย' เป็นเวลา 2 วินาทีก่อนที่จะทำงานอีกครั้งในการทำซ้ำครั้งต่อไป มันทำงานตลอดไป แต่หยุดพัก 2 วินาทีตามปกติ ความคิดเห็นของฉันเป็นข้อมูลเพิ่มเติมเกี่ยวกับว่าคุณสามารถระบุได้ว่ากำลังLongRunningใช้Task.Runไวยากรณ์หรือไม่ จากเอกสารดูเหมือนว่าTask.Runไวยากรณ์จะสะอาดกว่าตราบใดที่คุณพอใจกับการตั้งค่าเริ่มต้นที่ใช้ ดูเหมือนว่าจะไม่มีการโอเวอร์โหลดที่ทำให้เกิดการTaskCreationOptionsโต้แย้ง
Jeff

4

นี่คือสิ่งที่ฉันคิดขึ้น:

  • รับช่วงNeverEndingTaskและแทนที่ExecutionCoreเมธอดด้วยงานที่คุณต้องการทำ
  • การเปลี่ยนExecutionLoopDelayMsช่วยให้คุณสามารถปรับเวลาระหว่างลูปได้เช่นหากคุณต้องการใช้อัลกอริทึมแบ็คออฟ
  • Start/Stop จัดเตรียมอินเทอร์เฟซแบบซิงโครนัสเพื่อเริ่ม / หยุดงาน
  • LongRunningNeverEndingTaskหมายความว่าคุณจะได้รับหนึ่งด้ายทุ่มเทต่อ
  • คลาสนี้ไม่ได้จัดสรรหน่วยความจำแบบวนซ้ำซึ่งแตกต่าง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();
    }
}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.