4

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

但是,我已经开始从事一个个人项目,我正在使用它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之间的时间里可能会变得很忙。而且,在检查和下一个项目出队 之间可能会变空。IsBusyDownloadDataASync()requestQueueCount

我的问题是处理这种情况以使其成为线程安全的最佳方法是什么?

这更像是一个抽象的问题,因为我意识到对于这种特定方法,必须有一个非常不方便的时间才能真正引起问题,并且为了涵盖这种情况,我可以适当地包装该方法,try-catch因为这两个部分都会抛出异常。但是还有其他选择吗?lock声明是否适用于此处?

4

3 回答 3

1

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

你可以在这里阅读:http ://www.albahari.com/threading/

于 2012-08-07T17:55:03.573 回答
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;
       } 
    });
于 2012-08-07T17:57:16.810 回答
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 或队列。

于 2012-08-11T03:25:22.487 回答