Adam
Adam

Reputation: 1885

Manage Program Flow with Tasks or Events

My program executes other programs remotely using a service that does not implement change or complete notification. To determine if the program is complete there is a background thread that runs an infinite loop (until the cancellation token is passed). When a job is submitted it is added to a list of active jobs in the status queue and the background thread checks the status of each job in the list and fires an event with a job as arguments when it completes and another event when the queue is empty.

While this solution does work, I wonder if this is the best possible solution for this problem. It seems to me that tasks are ideally suited for this situation because they are what I would use if I wasn't relying on the server to dispatch the job.

Is there an advantage to using tasks instead of events in this case?

Upvotes: 1

Views: 449

Answers (2)

Adam
Adam

Reputation: 1885

This is a sample solution following the selected answer

public class MainViewModel
{
    private static void Main()
    {
        Task.Run(() => JobMonitor.Start());
        MainAsync().Wait();
    }

    public async Task MainAsync()
    {
        var test = new string[2];
        var jobs = test.Select(x => randomTask());
        var tasks = jobs.Select(x => x.TCS.Task);
        await Task.WhenAll(tasks);        
    }

    public Job randomTask()
    {
        var job = new Job();
        job.Submit();
        job.TCS.Task.ContinueWith(task => WelcomeTitle += "\n" + DateTime.Now, TaskScheduler.FromCurrentSynchronizationContext());
        return job;
    } 
}

public class Job
{
    public TaskCompletionSource<object> TCS = new TaskCompletionSource<object>();

    public readonly DateTime Expires;
    public bool IsExpired
    {
        get
        {
            var ret = Expires < DateTime.Now;
            return ret;
        }
    }

    public Job()
    {
        Random rnd = new Random();
        System.Threading.Thread.Sleep(20);
        var num = rnd.Next(1, 20);
        Expires = DateTime.Now.AddSeconds(num);
    }

    internal void Submit()
    {
        JobMonitor.SubmitJob(this);
    }
}

class JobMonitor
{
    public static List<Job> activeJobs = new List<Job>();
    private static object _lock = new object();
    public static void SubmitJob(Job job)
    {
        lock(_lock)
        {
            activeJobs.Add(job);
        }
    }

    public static void Start()
    {
        while (true)
        {
            lock (_lock)
            {
                var expired = activeJobs.Where(job => job.IsExpired).ToList();
                foreach (var job in expired)
                {
                    job.TCS.SetResult(null);
                    activeJobs.Remove(job);
                }
            }

            System.Threading.Thread.Sleep(1000);

        }
    }
}

Upvotes: 0

Kirill
Kirill

Reputation: 3454

About asynchronous programming patterns

Is there an advantage to using tasks instead of events in this case?

I think Tasks may make this particular code clearer. You're implementing asynchronous operation — submit job and wait it to complete. There are different patterns for that kind of operations, they're called Asynchronous Programming Patterns.

Tasks or Task-based Asynchronous Pattern (TAP) is the most recent asynchronous programming pattern. The others patterns are Asynchronous Programming Model (APM) and Event-based Asynchronous Pattern (EAP). Currently you're using EAP and it is about events. APM is the pattern where you have BeginOperation and EndOperation methods.

See this page about Asynchronous Programming Patterns. It says that EAP and APM is no longer recommended for new development and TAP is the recommended pattern.

I agree with these recommendations, I already forgot when I used events or Begin/End methods in my code. It doesn't mean that TAP should be used everywhere, EAP or APM may fit better in some situations but in your particular situation TAP may be the best choice.

How it could be implemented with TAP

We need a Task for each job which we will await. So the top-level code will be something like this:

static void Main(string[] args)
{
    MainAsync(args).Wait();
}

static async Task MainAsync(string[] args)
{
    // creating Enumerable of jobs
    // ...
    IEnumerable<Task> tasks = jobs.Select(job => client.SubmitJob(job));
    await Task.WhenAll(tasks);
}

client.SubmitJob returns Task. It could be implemented with TaskCompletionSource:

var tcs = new TaskCompletionSource<object>();
var jobInfo = new JobInfo {Job = job, TaskCompletionSource = tcs};
activeJobs.Add(jobInfo);
return tcs.Task;

TaskCompletionSource for Task is just like CancellationTokenSource for CancellationToken. We set Task to finish state with taskCompletionSource.SetResult() call. So we keep track of all active jobs with assigned TaskCompletionSource and the background thread calls SetResult:

// ... in the backgroung thread
// ... when the job is completed
jobInfo.TaskCompletionSource.SetResult(null);

It would be the same code as for events, you just need to keep the list of TaskCompletionSource for each job and call SetResult instead of trigger the event.

You can read the details about TAP such as exception and timeout handling here or download a great document about TAP here.

Further adventures

In the document you can find interesting method called Interleaved which you may want to use. It allows processing an IEnumerable<Task> in the order of completion — which tasks completes first will be returned first.

You can return Task<Job> from SumbitJob method and call SetResult(job) in the background thread. Then you can do this:

IEnumerable<Task<Job>> tasks = jobs.Select(job => client.SubmitJob(job));
IEnumerable<Task<Job>> interleaved = Interleaved(tasks);
foreach (var task in interleaved)
{
    Job job = await task;
    Console.WriteLine($"Job {job.Title} is completed");
}

The jobs will be reported as they completes.

Upvotes: 1

Related Questions