查询队列和线程安全

时间:2012-08-07 17:33:51

标签: c# c#-4.0 thread-safety

Thread-Safety不是我担心的一个方面,因为我编写的简单应用程序和库通常只在主线程上运行,或者没有直接修改我需要担心的任何类中的属性或字段之前。

但是,我已经开始处理一个个人项目,我使用WebClient从远程服务器异步下载数据。有一个Queue<Uri>包含一系列URI的预构建队列以下载数据。

所以请考虑以下代码段(这不是我真正的代码,但我希望能解释一下我的问题:

private WebClient webClient = new WebClient();
private Queue<Uri> requestQueue = new Queue<Uri>();

public Boolean DownloadNextASync()
{
    if (webClient.IsBusy)
        return false;

    if (requestQueue.Count == 0)
        return false

    var uri = requestQueue.Dequeue();

    webClient.DownloadDataASync(uri);

    return true;

}

如果我理解正确,这个方法不是线程安全的(假设多个线程知道这个对象的特定实例)。我的推理webClient可能会在IsBusy检查和DownloadDataASync()方法调用之间变为忙碌。此外,在requestQueue检查和下一个项目出列时,Count可能会变空。

我的问题是,处理此类情况以使其线程安全的最佳方法是什么?

这更像是一个抽象的问题,因为我意识到这个特定的方法必须有一个非常不方便的时间来实际导致问题,并且为了涵盖这种情况我可以将方法包装在适当的{ {1}}因为两个部分都会抛出异常。但还有另一种选择吗? try-catch声明是否适用于此处?

3 个答案:

答案 0 :(得分:1)

我强烈推荐Joseph Albahari阅读“C#中的线程”。我已经看了它,为我的第一次(错误)冒险进入线程做准备,而且它非常全面。

您可以在此处阅读:http://www.albahari.com/threading/

答案 1 :(得分:1)

如果您的目标是.Net 4.0,您可以使用任务并行库获取帮助:

var queue = new BlockingCollection<Uri>();
var maxClients = 4;

// Optionally provide another producer/consumer collection for the data
// var data = new BlockingCollection<Tuple<Uri,byte[]>>();

// Optionally implement CancellationTokenSource

var clients = from id in Enumerable.Range(0, maxClients)
              select Task.Factory.StartNew(
    () =>
    {
        var client = new WebClient();
        while (!queue.IsCompleted)
        {
            Uri uri;
            if (queue.TryTake(out uri))
            {
                byte[] datum = client.DownloadData(uri); // already "async"
                // Optionally pass datum along to the other collection
                // or work on it here
            }
            else Thread.SpinWait(100);
        }
    });

// Add URI's to search
// queue.Add(...);

// Notify our clients that we've added all the URI's
queue.CompleteAdding();

// Wait for all of our clients to finish
clients.WaitAll();

要将此方法用于进度指示,您可以使用TaskCompletionSource<TResult>来管理基于事件的并行性:

public static Task<byte[]> DownloadAsync(Uri uri, Action<double> progress)
{
    var source = new TaskCompletionSource<byte[]>();
    Task.Factory.StartNew(
        () =>
        {
            var client = new WebClient();
            client.DownloadProgressChanged
                += (sender, e) => progress(e.ProgressPercentage);
            client.DownloadDataCompleted
                += (sender, e) =>
                {
                    if (!e.Cancelled)
                    {
                        if (e.Error == null)
                        {
                            source.SetResult((byte[])e.Result);
                        }
                        else
                        {
                            source.SetException(e.Error);
                        }
                    }
                    else
                    {
                        source.SetCanceled();
                    }
               };
        });

    return source.Task;
}

像这样使用:

// var urls = new List<Uri>(...);
// var progressBar = new ProgressBar();

Task.Factory.StartNew(
    () =>
    {
       foreach (var uri in urls)
       {
           var task = DownloadAsync(
               uri,
               p =>
                   progressBar.Invoke(
                       new MethodInvoker(
                       delegate { progressBar.Value = (int)(100 * p); }))
               );

           // Will Block!
           // data = task.Result;
       } 
    });

答案 2 :(得分:1)

您提出的两个线程安全问题都是有效的。此外,WebClient和Queue都记录为不是线程安全的(在MSDN文档的底部)。例如,如果两个线程同时出列,它们实际上可能导致队列内部不一致或者可能导致非敏感的返回值。例如,如果Dequeue()的实现类似于:

1. var valueToDequeue = this._internalList[this._startPointer];
2. this._startPointer = (this._startPointer + 1) % this._internalList.Count;
3. return valueToDequeue;

并且两个线程在继续到第2行之前执行第1行,然后两者都返回相同的值(这里也存在其他潜在问题)。这不一定会抛出异常,所以你应该使用一个lock语句来保证一次只有一个线程在方法内:

private readonly object _lock = new object();

...

lock (this._lock) { 
    // body of method
}

如果您知道没有其他人会同步它们,您也可以锁定WebClient或队列。