Marcello
Marcello

Reputation: 1289

How to run tasks that use same external variables?

I have a web service (ExternalWebService) that receives a period (start and end date) and returns all logs for this period, and I want to make a call for this service passing a long period. The problem is that this service allows only a small amount of data to be sent per request, and long periods means large amount of data, what causes an error. So, I decided to loop through the months of the period passed as parameter and make calls for this service in parallel using tasks, concatenating the results at the end of the execution of the tasks. Here is the code:

public List<object> GetList(DateTime start, DateTime end)
{
    List<object> finalList = new List<object>();
    object lockList = new object();

    DateTime current = start;

    List<Task> threads = new List<Task>();

    do
    {
        current = new DateTime(Math.Min(current.AddMonths(1).Ticks, end.Ticks));

        Task thread = Task.Run(() => {
            List<object> partialList = ExternalWebService.GetListByPeriod(from: start, to: current);
            lock (lockList)
            {
                finalList = finalList.Concat(partialList).ToList();
            }
        });

        threads.Add(thread);

        start = current;
    }
    while (current < end);

    Task.WhenAll(threads).Wait();

    return finalList;
}

This code works but has an unexpected result, because the variables start and current change before being used inside the thread. So, what can I do to guarantee that the start and current date used inside Task.Run have the same values they had when the thread was created?

Upvotes: 1

Views: 1033

Answers (3)

Marcello
Marcello

Reputation: 1289

Here is the code that worked for me:

protected override List<object> GetList(DateTime start, DateTime end)
{
    List<object> list = new List<object>();
    object lockList = new object();

    DateTime current = start;

    List<Task> threads = new List<Task>();

    do
    {
        current = new DateTime(Math.Min(current.AddMonths(1).Ticks, end.Ticks));

        Task thread = Task.Run(GetMethodFunc(start, current)).ContinueWith((result) => 
        {
            lock (lockList)
            {
                list = list.Concat(result.Result).ToList();
            }
        });

        threads.Add(thread);

        start = current;
    }
    while (current < end);

    Task.WhenAll(threads).Wait();

    return list;
}

private Func<List<object>> GetMethodFunc(DateTime start, DateTime end)
{
    return () => {
        List<object> partialList = ExternalWebService.GetListByPeriod(from: start, to: end);
        return partialList;
    };
}

Upvotes: 0

JSteward
JSteward

Reputation: 7091

It would be best not to both share and mutate your dates. You can spin up a set of tasks that query your web service asynchronously and flatten the results.

public class GetData {

    public async Task<IEnumerable<object>> GetDataAsync(DateTime startDate, DateTime endDate) {
        var daysPerChunk = 28;
        var totalChunks = (int)Math.Ceiling((endDate - startDate).TotalDays / daysPerChunk);
        var chunks = Enumerable.Range(0, totalChunks);

        var dataTasks = chunks.Select(chunkIndex => {
            var start = startDate.AddDays(chunkIndex * daysPerChunk);
            var end = new DateTime(Math.Min(start.AddDays(daysPerChunk).Ticks, endDate.Ticks));
            return ExternalWebService.GetListByPeriodAsync(from: start, to: end);
        });
        var results = await Task.WhenAll(dataTasks);

        var data = results.SelectMany(_ => _);
        return data.ToList();
    }
}

public class ExternalWebService {

    private static HttpClient Client {
        get;
    } = new HttpClient();


    public async static Task<IEnumerable<object>> GetListByPeriodAsync(DateTime from, DateTime to) {
        var response = await Client.GetAsync("GetListByPeriodFromToUri");
        if (response != null && response.IsSuccessStatusCode) {
            using (var stream = await response.Content.ReadAsStreamAsync()) {
                using (var reader = new StreamReader(stream)) {
                    var str = reader.ReadToEnd();
                    return JsonConvert.DeserializeObject<IEnumerable<object>>(str);
                }
            }
        }
        return Enumerable.Empty<object>();
    }
}

Upvotes: 3

dcg
dcg

Reputation: 4219

You can create a method that receives the DateTime you want and return the delegate to pass it to the Task.Run method, something like:

private Action GetMethodAction(DateTime current) 
{
     return () => { /* your code here */ }
}

That way the value of current is bound to the action you are returning. Hope it helps.

Upvotes: 0

Related Questions