与ManualResetEvent同步的问题

3
我已经编写了一个下载文件的方法,现在我正在尝试使其并行下载最多5个文件,对于其余的文件要等待前面的下载完成后再进行。为此,我使用了ManualResetEvent,但是当我包含同步部分时,它不再下载任何内容(如果没有同步部分,则可以正常工作)。
以下是该方法的代码:
    static readonly int maxFiles = 5;
    static int files = 0;
    static object filesLocker = new object();
    static System.Threading.ManualResetEvent sync = new System.Threading.ManualResetEvent(true);

    /// <summary>
    /// Download a file from wikipedia asynchronously
    /// </summary>
    /// <param name="filename"></param>
    public void DoanloadFileAsync(string filename)
    {
        ...
        System.Threading.ThreadPool.QueueUserWorkItem(
            (o) =>
            {
                bool loop = true;
                while (loop)
                    if (sync.WaitOne())
                        lock (filesLocker)
                        {
                            if (files < maxFiles)
                            {
                                ++files;
                                if (files == maxFiles)
                                    sync.Reset();
                                loop = false;
                            }
                        }
                try
                {
                    WebClient downloadClient = new WebClient();
                    downloadClient.OpenReadCompleted += new OpenReadCompletedEventHandler(downloadClient_OpenReadCompleted);
                    downloadClient.OpenReadAsync(new Uri(url, UriKind.Absolute));
                    //5 of them do get here
                }
                catch
                {
                    lock (filesLocker)
                    {
                        --files;
                        sync.Set();
                    }
                    throw;
                }
            });
    }

    void downloadClient_OpenReadCompleted(object sender, OpenReadCompletedEventArgs e)
    {
        try
        {
            //but none of the 5 get here
            ...Download logic... //works without the ManualResetEvent
        }
        finally
        {
            lock (filesLocker)
            {
                --files;
                sync.Set();
            }
        }
    }

我做错了什么吗?

这是为Windows Phone 7编写的Silverlight 4。

编辑:Silverlight 4中没有Semaphore或SemaphoreSlim。


为什么要使用锁,当你可以使用 System.Threading.Interlocked.Decrement() 方法呢? - Jaroslav Jandek
因为我也想调用sync.Set(),而且我认为有人可能会从另一个线程调用sync.Set(),然后我要进行减量操作,一些线程会增加并调用sync.Reset(),然后我会调用sync.Set()并得到超过maxFiles的线程下载。 - user182945
1
检查一下我的答案,它就是你要找的。同时,使用重置事件是可以的,只是我不认为有必要使用锁。哦,我使用的是AutoResetEvent,因为它正好适合这里的需求。 - Jaroslav Jandek
3个回答

4

我的评论意思是,当你可以使用Interlocked时,不要使用缓慢的lock。这样做的性能将更好。

最多同时有5个下载活动:

public class Downloader
{
 private int fileCount = 0;
 private AutoResetEvent sync = new AutoResetEvent(false);

 private void StartNewDownload(object o)
 {
  if (Interlocked.Increment(ref this.fileCount) > 5) this.sync.WaitOne();

  WebClient downloadClient = new WebClient();
  downloadClient.OpenReadCompleted += downloadClient_OpenReadCompleted;
  downloadClient.OpenReadAsync(new Uri(o.ToString(), UriKind.Absolute));
 }

 private void downloadClient_OpenReadCompleted(object sender, OpenReadCompletedEventArgs e)
 {
  try
  {
   // Download logic goes here.
  }
  catch {}
  finally
  {
   this.sync.Set();
   Interlocked.Decrement(ref this.fileCount);
  }
 }

 public void Run()
 {
  string o = "url1";
  System.Threading.ThreadPool.QueueUserWorkItem(this.StartNewDownload, o);
  Thread.Sleep(100);

  o = "url2";
  System.Threading.ThreadPool.QueueUserWorkItem(this.StartNewDownload, o);

  o = "url3";
  System.Threading.ThreadPool.QueueUserWorkItem(this.StartNewDownload, o);
  Thread.Sleep(200);

  o = "url4";
  System.Threading.ThreadPool.QueueUserWorkItem(this.StartNewDownload, o);

  o = "url5";
  System.Threading.ThreadPool.QueueUserWorkItem(this.StartNewDownload, o);

  o = "url6";
  System.Threading.ThreadPool.QueueUserWorkItem(this.StartNewDownload, o);

  o = "url7";
  System.Threading.ThreadPool.QueueUserWorkItem(this.StartNewDownload, o);
  Thread.Sleep(200);

  o = "url8";
  System.Threading.ThreadPool.QueueUserWorkItem(this.StartNewDownload, o);
  Thread.Sleep(400);
 }
}

这个类应该是静态的(代码对于静态同步也有效)。 - Jaroslav Jandek
是的,这种方式更高效...但我仍然有同样的问题...也许不是来自AutoResetEvent / ManualResetEvent。 - user182945
@user182945:你试过我的代码吗?它永远无法达到5个并发下载。如果你的回调函数没有执行,也许问题出在其他地方。也许是你的下载逻辑中的某些问题。我将其保留为空以进行测试,它运行良好。 - Jaroslav Jandek

1

看起来你正在尝试限制可以同时进入关键部分——文件下载的线程数量。与其手工制作,不如使用 System.Threading.Semaphore——它就是为此而设计的!


在Silverlight for Windows Phone中似乎没有信号量。 有没有其他类似信号量的东西可以使用? - user182945
啊 - 是的 - 抱歉 - 或许不是。我之前看的是完整框架的文档。在这里,我找到了一些有用的链接,人们在Silverlight中讨论替代方案:https://dev59.com/vUzSa4cB1Zd3GeqPjx8A 和 http://forums.silverlight.net/forums/p/20199/69433.aspx - Rob Levine
Rob是正确的;你正在使用错误的同步原语。如果你正在使用.NET 4.0,那么请使用System.Threading.SemaphoreSlim代替 - 它轻量级、快速,并支持取消令牌,这是在执行长时间操作(如下载文件)时可能会有用的功能。编辑:打字太慢了... - Allon Guralnek
@Allon Guralnek 这个在 Silverlight 中存在吗? - Tim Lloyd
@Allon Guralnek,Silverlight 4中没有Semaphore或SempahoreSlim。 @Rob Levine,我会检查那些链接。 - user182945
3
通过反射查看SemaphoreSlim的反汇编,你或许可以将整个类复制粘贴到你的项目中 - 该类内部使用ManualResetEvent: http://pastebin.com/cFmgihdB。在Silverlight中排除许多内容的原因通常是为了保持Silverlight插件的下载大小,而不是技术原因。当Reflector正常工作时,它是弥补这些缺失的好方法。 - Allon Guralnek

0
看起来在创建WebClient之前就已经触发了WaitOne()。由于调用Set()的所有代码都在事件处理程序或异常处理程序中,因此它永远不会被触发。
也许您错误地将WebClient代码包含在线程池线程方法中,而应该将其放在外面。
    System.Threading.ThreadPool.QueueUserWorkItem(
        (o) =>
        {
            bool loop = true;
            while (loop)
                if (sync.WaitOne())
                    lock (filesLocker)
                    {
                        if (files < maxFiles)
                        {
                            ++files;
                            if (files == maxFiles)
                                sync.Reset();
                            loop = false;
                        }
                    }

        });

//Have the try catch OUTSIDE the background thread.
            try
            {
                WebClient downloadClient = new WebClient();
                downloadClient.OpenReadCompleted += new OpenReadCompletedEventHandler(downloadClient_OpenReadCompleted);
                downloadClient.OpenReadAsync(new Uri(url, UriKind.Absolute));
                //5 of them do get here
            }
            catch
            {
                lock (filesLocker)
                {
                    --files;
                    sync.Set();
                }
                throw;
            }

我想创建WebClient,并在当前DownloadAsync调用中获取++文件后才调用OpenReadAsync。这样,它一次只会下载maxFiles,对于其余的文件,则等待前面的1个下载完成。(当我调试时,我不会得到关于该部分的NullReferenceException,所以我猜它被正确创建了)。 - user182945

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接