Cách thích hợp để thực hiện một nhiệm vụ không bao giờ kết thúc. (Bộ hẹn giờ so với công việc)


92

Vì vậy, ứng dụng của tôi cần thực hiện một hành động gần như liên tục (với khoảng thời gian tạm dừng 10 giây giữa mỗi lần chạy) miễn là ứng dụng đang chạy hoặc yêu cầu hủy. Công việc cần làm có khả năng mất đến 30 giây.

Tốt hơn là sử dụng System.Timers.Timer và sử dụng AutoReset để đảm bảo rằng nó không thực hiện hành động trước khi "đánh dấu" trước đó hoàn tất.

Hay tôi nên sử dụng một Tác vụ chung trong chế độ LongRunning với mã thông báo hủy và có một vòng lặp while vô hạn thông thường bên trong nó gọi hành động thực hiện công việc với 10 giây Thread.Sleep giữa các lần gọi? Đối với mô hình async / await, tôi không chắc nó sẽ phù hợp ở đây vì tôi không có bất kỳ giá trị trả về nào từ công việc.

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

hay chỉ sử dụng một bộ đếm thời gian đơn giản trong khi sử dụng thuộc tính AutoReset của nó và gọi .Stop () để hủy nó?


Nhiệm vụ có vẻ như là một việc quá mức cần thiết khi xem xét những gì bạn đang cố gắng đạt được. vi.wikipedia.org/wiki/KISS_principle . Dừng hẹn giờ khi bắt đầu OnTick (), kiểm tra bool để xem bạn có nên làm gì khi không, làm việc gì, khởi động lại Bộ hẹn giờ khi bạn hoàn thành.
Mike Trusov

Câu trả lời:


94

Tôi muốn sử dụng TPL Dataflow cho việc này (vì bạn đang sử dụng .NET 4.5 và nó sử dụng Tasknội bộ). Bạn có thể dễ dàng tạo một ActionBlock<TInput>mục đăng các mục lên chính nó sau khi nó được xử lý, hành động của nó và đợi một khoảng thời gian thích hợp.

Đầu tiên, hãy tạo một nhà máy sẽ tạo ra nhiệm vụ không bao giờ kết thúc của bạn:

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

Tôi đã chọn ActionBlock<TInput>lấy một DateTimeOffsetcấu trúc ; bạn phải truyền một tham số kiểu và nó cũng có thể truyền một số trạng thái hữu ích (bạn có thể thay đổi bản chất của trạng thái, nếu bạn muốn).

Ngoài ra, hãy lưu ý rằng ActionBlock<TInput>theo mặc định chỉ xử lý một mục tại một thời điểm, vì vậy bạn được đảm bảo rằng chỉ một hành động sẽ được xử lý (nghĩa là, bạn sẽ không phải đối phó với lần truy cập lại khi nó gọi lại Postphương thức tiện ích mở rộng ).

Tôi cũng đã chuyển CancellationTokencấu trúc cho cả phương thức khởi tạo ActionBlock<TInput>Task.Delayphương thức ; nếu quá trình bị hủy bỏ, việc hủy bỏ sẽ diễn ra ở cơ hội có thể đầu tiên.

Từ đó, thật dễ dàng cấu trúc lại mã của bạn để lưu trữ ITargetBlock<DateTimeoffset>giao diện được triển khai bởi ActionBlock<TInput>(đây là phần trừu tượng cấp cao hơn đại diện cho các khối là người tiêu dùng và bạn muốn có thể kích hoạt mức tiêu thụ thông qua lệnh gọi đến Postphương thức mở rộng):

CancellationTokenSource wtoken;
ActionBlock<DateTimeOffset> task;

StartWorkPhương pháp của bạn :

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

Và sau đó là StopWorkphương pháp của bạn :

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

Tại sao bạn muốn sử dụng TPL Dataflow ở đây? Một vài lý do:

Tách mối quan tâm

Các CreateNeverEndingTaskphương pháp hiện nay là một nhà máy tạo ra "dịch vụ" của bạn như vậy để nói chuyện. Bạn kiểm soát khi nào nó bắt đầu và dừng lại, và nó hoàn toàn khép kín. Bạn không phải đan xen quyền kiểm soát trạng thái của bộ hẹn giờ với các khía cạnh khác của mã của bạn. Bạn chỉ cần tạo khối, khởi động nó và dừng nó khi bạn hoàn thành.

Sử dụng hiệu quả hơn các luồng / nhiệm vụ / tài nguyên

Bộ lập lịch mặc định cho các khối trong luồng dữ liệu TPL giống nhau đối với a Task, là nhóm luồng. Bằng cách sử dụng ActionBlock<TInput>để xử lý hành động của bạn, cũng như một lệnh gọi Task.Delay, bạn đang cấp quyền kiểm soát chuỗi mà bạn đang sử dụng khi bạn không thực sự làm bất cứ điều gì. Đúng vậy, điều này thực sự dẫn đến một số chi phí khi bạn sinh ra cái mới Tasksẽ xử lý tiếp tục, nhưng điều đó sẽ nhỏ, vì bạn không xử lý điều này trong một vòng lặp chặt chẽ (bạn đang đợi mười giây giữa các lần gọi).

Nếu DoWorkhàm thực sự có thể được thực hiện ở trạng thái chờ đợi (cụ thể là nó trả về a Task), thì bạn có thể (có thể) tối ưu hóa điều này hơn nữa bằng cách tinh chỉnh phương thức gốc ở trên để lấy a Func<DateTimeOffset, CancellationToken, Task>thay vì a Action<DateTimeOffset>, như sau:

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

Tất nhiên, sẽ là một thực tiễn tốt để đưa CancellationTokenra phương pháp của bạn (nếu nó chấp nhận một phương pháp), được thực hiện ở đây.

Điều đó có nghĩa là sau đó bạn sẽ có một DoWorkAsyncphương thức với chữ ký sau:

Task DoWorkAsync(CancellationToken cancellationToken);

Bạn sẽ phải thay đổi (chỉ một chút và bạn sẽ không phải lo lắng về việc tách biệt các mối quan tâm ở đây) StartWorkphương pháp tính cho chữ ký mới được chuyển cho CreateNeverEndingTaskphương thức, như sau:

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

Xin chào, tôi đang thử triển khai này nhưng tôi đang gặp phải sự cố. Nếu DoWork của tôi không có đối số, task = CreateNeverEndingTask (now => DoWork (), wtoken.Token); cho tôi lỗi bản dựng (kiểu không khớp). Mặt khác, nếu DoWork của tôi nhận một tham số DateTimeOffset, thì cùng một dòng đó cho tôi một lỗi xây dựng khác, cho tôi biết rằng không có quá tải nào cho DoWork nhận 0 đối số. Bạn có thể vui lòng giúp tôi tìm ra điều này không?
Bovaz

1
Trên thực tế, tôi đã giải quyết vấn đề của mình bằng cách thêm một cast vào dòng nơi tôi gán nhiệm vụ và truyền tham số cho DoWork: task = (ActionBlock <DateTimeOffset>) CreateNeverEndingTask (now => DoWork (now), wtoken.Token);
Bovaz

Bạn cũng có thể thay đổi loại tác vụ "ActionBlock <DateTimeOffset>;" đến tác vụ ITargetBlock <NgàyTimeOffset>;
XOR

1
Tôi tin rằng điều này có khả năng phân bổ bộ nhớ mãi mãi, do đó cuối cùng dẫn đến tràn.
Nate Gardner

@NateGardner Trong phần nào?
casperOne

75

Tôi thấy giao diện dựa trên Tác vụ mới rất đơn giản để làm những việc như thế này - thậm chí còn dễ dàng hơn so với việc sử dụng lớp Bộ hẹn giờ.

Có một số điều chỉnh nhỏ bạn có thể thực hiện cho ví dụ của mình. Thay vì:

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

Bạn có thể làm được việc này:

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

Bằng cách này, việc hủy bỏ sẽ xảy ra ngay lập tức nếu bên trong Task.Delay, thay vì phải đợiThread.Sleep kết thúc.

Ngoài ra, sử dụng Task.DelayhơnThread.Sleep có nghĩa là bạn không buộc một chuỗi không làm gì trong suốt thời gian ngủ.

Nếu có thể, bạn cũng có thể DoWork()chấp nhận mã thông báo hủy và việc hủy sẽ nhanh hơn nhiều.


1
Whatch ra những nhiệm vụ bạn sẽ nhận được nếu bạn sử dụng lambda async như tham số của Task.Factory.StartNew - blogs.msdn.com/b/pfxteam/archive/2011/10/24/10229468.aspx Khi bạn làm task.Wait ( ); sau khi yêu cầu hủy bỏ, bạn sẽ phải chờ đợi đến tác vụ không chính xác.
Lukas Pirkl

Có, đây thực sự phải là Task.Run now, có quá tải chính xác.
porges

Theo http://blogs.msdn.com/b/pfxteam/archive/2011/10/24/10229468.aspx, có vẻ như Task.Runsử dụng nhóm luồng, vì vậy ví dụ của bạn bằng cách sử dụng Task.Runthay vì Task.Factory.StartNewwith TaskCreationOptions.LongRunningkhông thực hiện chính xác điều tương tự - nếu tôi cần tác vụ để sử dụng LongRunningtùy chọn, tôi sẽ không thể sử dụng Task.Runnhư bạn đã trình bày, hay tôi thiếu thứ gì đó?
Jeff

@Lumirris: Điểm của async / await là tránh buộc một luồng trong suốt thời gian nó đang thực thi (ở đây, trong quá trình gọi Delay, tác vụ không sử dụng một luồng). Vì vậy, sử dụng LongRunninglà loại không tương thích với mục tiêu không buộc các chủ đề. Nếu bạn muốn đảm bảo chạy trên chuỗi của riêng nó, bạn có thể sử dụng nó, nhưng ở đây bạn sẽ bắt đầu một chuỗi đang ngủ hầu hết thời gian. Trường hợp sử dụng là gì?
porges

@Porges Đã lấy điểm. Trường hợp sử dụng của tôi sẽ là một tác vụ chạy một vòng lặp vô hạn, trong đó mỗi lần lặp sẽ thực hiện một phần công việc và 'thư giãn' trong 2 giây trước khi thực hiện một phần công việc khác trong lần lặp tiếp theo. Nó chạy mãi mãi, nhưng thường xuyên nghỉ 2 giây. Tuy nhiên, bình luận của tôi thiên về việc bạn có thể chỉ định nó LongRunningbằng Task.Runcú pháp hay không. Từ tài liệu, có vẻ như Task.Runcú pháp rõ ràng hơn, miễn là bạn hài lòng với cài đặt mặc định mà nó sử dụng. Dường như không có quá tải với nó mà có một TaskCreationOptionsđối số.
Jeff

4

Đây là những gì tôi nghĩ ra:

  • Kế thừa NeverEndingTaskvà ghi đèExecutionCore phương thức với công việc bạn muốn làm.
  • Thay đổi ExecutionLoopDelayMscho phép bạn điều chỉnh thời gian giữa các vòng lặp, ví dụ như nếu bạn muốn sử dụng một thuật toán dự phòng.
  • Start/Stop cung cấp một giao diện đồng bộ để bắt đầu / dừng tác vụ.
  • LongRunning nghĩa là bạn sẽ nhận được một chuỗi chuyên dụng cho mỗi NeverEndingTask .
  • Lớp này không cấp phát bộ nhớ trong một vòng lặp không giống như ActionBlock giải pháp dựa trên.
  • Đoạn mã dưới đây là mã phác thảo, không nhất thiết phải là mã sản xuất :)

:

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();
    }
}
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.