WebClient DownloadData方法冻结Form

时间:2015-04-08 01:10:44

标签: c# multithreading webclient freeze webclient-download

我正在使用WebClient对象的DownloadData从几个网站下载favicon。

我收到Byte数组的响应,一切都运行良好,除了一件事: DownloadData 方法执行时,它会冻结我的表单直到方法返回

现在,我已经通过使用BackgroundWorker对象来解决这个问题,以完成工作,但我很好奇我将如何使用 System.Threading.Thread实现相同的功能

我尝试创建另一个下载了favicons的Thread,然后循环我的mainThread,直到Thread完成处理,然后使用Abort()方法中止线程,但到目前为止我的Form在执行另一个时被冻结线程。

这是我用来创建另一个Thread的代码:

    bool downloadFavIcon_Completed = false;
    private void downloadFavIcon()
    {
        downloadFavIcon_Completed = false;
        Byte[] dl;
        System.IO.MemoryStream dlMem;
        Bitmap favCollection = new Bitmap(96, 64);
        Graphics g = Graphics.FromImage(favCollection);
        Bitmap dlImg;
        String[] addr = new String[24];
        addr[0] = @"http://google.com/favicon.ico";
        addr[1] = @"http://microsoft.com/favicon.ico";
        addr[2] = @"http://freesfx.com/favicon.ico";
        addr[3] = @"http://yahoo.com/favicon.ico";
        addr[4] = @"http://downloadha.com/favicon.ico";
        addr[5] = @"http://hp.com/favicon.ico";
        addr[6] = @"http://bing.com/favicon.ico";
        addr[7] = @"http://webassign.com/favicon.ico";
        addr[8] = @"http://youtube.com/favicon.ico";
        addr[9] = @"https://twitter.com/favicon.ico";
        addr[10] = @"http://cc.com/favicon.ico";
        addr[11] = @"http://stackoverflow.com/favicon.ico";
        addr[12] = @"http://vb6.us/favicon.ico";
        addr[13] = @"http://facebook.com/favicon.ico";
        addr[14] = @"http://flickr.com/favicon.ico";
        addr[15] = @"http://linkedin.com/favicon.ico";
        addr[16] = @"http://blogger.com/favicon.ico";
        addr[17] = @"http://blogfa.com/favicon.ico";
        addr[18] = @"http://metal-archives.com/favicon.ico";
        addr[19] = @"http://wordpress.com/favicon.ico";
        addr[20] = @"http://metallica.com/favicon.ico";
        addr[21] = @"http://wikipedia.org/favicon.ico";
        addr[22] = @"http://visualstudio.com/favicon.ico";
        addr[23] = @"http://evernote.com/favicon.ico";
        for (int i = 0; i < addr.Length; i++)
        {
            using (System.Net.WebClient client = new System.Net.WebClient())
            {
                try
                {
                    dl = client.DownloadData(addr[i]);
                    dlMem = new System.IO.MemoryStream(dl);
                    dlImg = new Bitmap(dlMem);
                }
                catch (Exception)
                {
                    dlImg = new Bitmap(Properties.Resources.defaultFavIcon);
                }
            }
            g.DrawImage(dlImg, (i % 6) * 16, (i / 6) * 16, 16, 16);
        }
        passAddDisplay.Image = favCollection;
        downloadFavIcon_Completed = true;
    }

    private void button2_Click(object sender, EventArgs e)
    {
        Thread downloader = new Thread(new ThreadStart(downloadFavIcon));
        downloader.Start();
        while (!downloader.IsAlive) ;
        while (!downloadFavIcon_Completed) ;
        downloader.Abort();
    }

注意: passAddDisplay 是已放置在我表单上的pictureBox。

如何改进我的应用程序以避免在执行WebClient.DownloadData期间被冻结? (我不想使用Application.DoEvents())

3 个答案:

答案 0 :(得分:1)

是的,它是一个同步方法,它导致线程在没有处理消息的情况下等待,直到它返回为止;你应该尝试它的Async版本,它没有。

答案 1 :(得分:0)

欢迎使用Stack Overflow ...

通过检查循环,很明显你是在循环中锁定UI线程。

while (!downloader.IsAlive) ;
while (!downloadFavIcon_Completed) ;

您的UI锁定的原因。理想情况下,这将是后台工作者的工作,因为它被设计为在后台运行并提供事件以回溯到您的UI线程。这可以使用Thread写入,但是应该使用后台工作程序。

现在,如果您真的想使用Thread对象编写此内容,那么我建议您为下载程序创建一个类并创建事件。因此我们可以编写简单的事件,例如

  1. 已完成的活动
  2. 已取消的活动
  3. UI进度事件
  4. 我不推荐这种方法(进一步阅读)

    首先创建一个新类Downloader,这是一个样本(最低限度)

    public class Downloader
    {
    
        /// <summary>
        /// Delegate Event Handler for the downloading progress
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        public delegate void DownloaderProgressEventHandler(Downloader sender, DownloaderProgressEventArgs e);
    
        /// <summary>
        /// Delegate Event Handler for the completed event
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        public delegate void DownloaderCompletedEventHandler(Downloader sender, DownloaderCompletedEventArgs e);
    
        /// <summary>
        /// The completed event
        /// </summary>
        public event DownloaderCompletedEventHandler Completed;
    
        /// <summary>
        /// The cancelled event
        /// </summary>
        public event EventHandler Cancelled;
    
        /// <summary>
        /// the progress event
        /// </summary>
        public event DownloaderProgressEventHandler Progress;
    
        /// <summary>
        /// the running thread
        /// </summary>
        Thread thread;
    
        /// <summary>
        /// the aborting flag
        /// </summary>
        bool aborting = false;
    
        //the addresses
        String[] addr = new String[] { 
            "http://google.com/favicon.ico", 
            "http://microsoft.com/favicon.ico", 
            "http://freesfx.com/favicon.ico", 
            "http://yahoo.com/favicon.ico", 
            "http://downloadha.com/favicon.ico",
            "http://hp.com/favicon.ico", 
            "http://bing.com/favicon.ico", 
            "http://webassign.com/favicon.ico", 
            "http://youtube.com/favicon.ico", 
            "https://twitter.com/favicon.ico", 
            "http://cc.com/favicon.ico", 
            "http://stackoverflow.com/favicon.ico", 
            "http://vb6.us/favicon.ico", 
            "http://facebook.com/favicon.ico", 
            "http://flickr.com/favicon.ico", 
            "http://linkedin.com/favicon.ico",
            "http://blogger.com/favicon.ico",
            "http://blogfa.com/favicon.ico",
            "http://metal-archives.com/favicon.ico",
            "http://wordpress.com/favicon.ico",
            "http://metallica.com/favicon.ico",
            "http://wikipedia.org/favicon.ico", 
            "http://visualstudio.com/favicon.ico",
            "http://evernote.com/favicon.ico" 
        };
    
    
        /// <summary>
        /// Starts the downloader
        /// </summary>
        public void Start()
        {
            if (this.aborting)
                return;
            if (this.thread != null)
                throw new Exception("Already downloading....");
            this.aborting = false;
    
            this.thread = new Thread(new ThreadStart(runDownloader));
            this.thread.Start();
    
        }
    
        /// <summary>
        /// Starts the downloader
        /// </summary>
        /// <param name="addresses"></param>
        public void Start(string[] addresses)
        {
            if (this.aborting)
                return;
    
            if (this.thread != null)
                throw new Exception("Already downloading....");
    
            this.addr = addresses;
            this.Start();
        }
    
        /// <summary>
        /// Aborts the downloader
        /// </summary>
        public void Abort()
        {
            if (this.aborting)
                return;
            this.aborting = true;
            this.thread.Join();
            this.thread = null;
            this.aborting = false;
    
            if (this.Cancelled != null)
                this.Cancelled(this, EventArgs.Empty);
        }
    
        /// <summary>
        /// runs the downloader
        /// </summary>
        void runDownloader()
        {
            Bitmap favCollection = new Bitmap(96, 64);
            Graphics g = Graphics.FromImage(favCollection);
    
            for (var i = 0; i < this.addr.Length; i++)
            {
                if (aborting)
                    break;
    
                using (System.Net.WebClient client = new System.Net.WebClient())
                {
                    try
                    {
                        byte[] dl = client.DownloadData(addr[i]);
                        using (var stream = new MemoryStream(dl))
                        {
                            using (var dlImg = new Bitmap(stream))
                            {
                                g.DrawImage(dlImg, (i % 6) * 16, (i / 6) * 16, 16, 16);
                            }
                        }
                    }
                    catch (Exception)
                    {
                        using (var dlImg = new Bitmap(Properties.Resources.defaultFacIcon))
                        {
                            g.DrawImage(dlImg, (i % 6) * 16, (i / 6) * 16, 16, 16);
                        }
                    }
                }
                if (aborting)
                    break;
    
                if (this.Progress != null)
                    this.Progress(this, new DownloaderProgressEventArgs
                    {
                        Completed = i + 1,
                        Total = this.addr.Length
                    });
            }
    
            if (!aborting && this.Completed != null)
            {
                this.Completed(this, new DownloaderCompletedEventArgs
                {
                    Bitmap = favCollection
                });
            }
            this.thread = null;
        }
    
        /// <summary>
        /// Downloader progress event args
        /// </summary>
        public class DownloaderProgressEventArgs : EventArgs
        {
            /// <summary>
            /// Gets or sets the completed images
            /// </summary>
            public int Completed { get; set; }
    
            /// <summary>
            /// Gets or sets the total images
            /// </summary>
            public int Total { get; set; }
        }
    
        /// <summary>
        /// Downloader completed event args
        /// </summary>
        public class DownloaderCompletedEventArgs : EventArgs
        {
            /// <summary>
            /// Gets or sets the bitmap
            /// </summary>
            public Bitmap Bitmap { get; set; }
        }
    
    }
    

    现在这是分配代码,但我们可以快速查看它。首先,我们为Completed和Progress事件定义了2个委托。这些代理接受下载器实例作为底部列出的发送方和特殊事件arg类。接下来是我们的3个事件(如上所列),这些事件将用于指示下载器的更改。

    接下来我们定义了字段。

    Thread thread;这是对调用&#39; Start()方法时创建的线程的引用。

    bool aborting = false;这是一个向线程发出信号的简单标志,我们应该中止。现在我决定使用一个标志,让线程优雅地完成,而不是调用Thread.Abort()方法。这样可以确保所有清理工作都能正常进行。

    string[] addres =....我们的初始地址。

    到目前为止,这一切都很简单。接下来是我们的Start()方法。我提供了两种不同的方法。其中一种方法接受新的string[]地址下载(不那么重要)。

    你会注意到这个方法

    /// <summary>
    /// Starts the downloader
    /// </summary>
    public void Start()
    {
        if (this.aborting)
            return;
        if (this.thread != null)
            throw new Exception("Already downloading....");
        this.aborting = false;
    
        this.thread = new Thread(new ThreadStart(runDownloader));
        this.thread.Start();
    }
    

    我们要做的第一件事就是检查是否设置了中止标志。如果是,则忽略启动调用(您可以抛出异常)。接下来,我们检查线程是否为空。如果线程不为null,则我们的下载器正在运行。最后,我们只需将我们的中止标记重置为false并启动新的Thread

    向下移动到Abort()方法。此方法将首先检查是否设置了aborting标志。如果是这样,那就什么也不做。接下来,我们将aborting标志设置为true。下一步,我将警告你, WILL 阻止你的调用线程调用方法Thread.Join(),它将把线程加入我们的调用线程。基本上等待线程退出。

    最后,我们只需将线程实例设置为null,将aborting标志重置为false并触发Cancelled事件(如果已订阅)。

    接下来是下载的主要方法。首先,您会注意到我移动了变量,并使用using语句作为一次性对象。 (这是另一个话题)。

    runDownloader()方法的一大特色就是它会定期检查“中止”。旗。如果此标记设置为true,那么downloader就会停止。现在请注意,在WebClient下载图像时,可能会出现中止的情况。理想情况下,您可以让WebClient完成请求,正确处理然后退出循环。

    每次下载图像后,都会触发progress事件(如果已订阅)。最后,当迭代完成并且所有图像都下载了&#34;已完成&#34;使用编译的位图图像触发事件。

    喘息机的暂停

    现在这一切都很棒..但你如何使用它。很简单,我创建了一个带有按钮,进度条和图片框的表单。该按钮将用于启动和停止下载程序,进度条用于处理进度事件以及完成图像的图片框。

    以下是我对其中部分内容进行评论的示例程序。

    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
    
            this.progressBar1.Visible = false;
            this.progressBar1.Enabled = false;
        }
    
        Downloader downloader;
    
        /// <summary>
        /// starts \ stop button pressed
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void button1_Click(object sender, EventArgs e)
        {
            //if downloader is not null then abort it
            if (downloader != null)
            {
                downloader.Abort();
                return;
            }
    
            //setup and start the downloader
            this.progressBar1.Value = 0;
            this.progressBar1.Minimum = 0;
            this.progressBar1.Enabled = true;
            this.progressBar1.Visible = true;
            this.downloader = new Downloader();
            this.downloader.Progress += downloader_Progress;
            this.downloader.Completed += downloader_Completed;
            this.downloader.Cancelled += downloader_Cancelled;
            this.downloader.Start();
        }
    
        /// <summary>
        /// downloader cancelled event handler
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void downloader_Cancelled(object sender, EventArgs e)
        {
            this.unhookDownloader();
    
            if (this.InvokeRequired)
                this.Invoke((MethodInvoker)delegate
                {
                    this.progressBar1.Enabled = false;
                    this.progressBar1.Visible = false;
                    MessageBox.Show(this, "Cancelled");
                });
            else
            {
                this.progressBar1.Enabled = false;
                this.progressBar1.Visible = false;
                MessageBox.Show(this, "Cancelled");
            }
    
        }
    
        /// <summary>
        /// downloader completed event handler
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void downloader_Completed(Downloader sender, Downloader.DownloaderCompletedEventArgs e)
        {
            this.unhookDownloader();
            if (this.InvokeRequired)
                this.Invoke((MethodInvoker)delegate
                {
                    this.progressBar1.Enabled = false;
                    this.progressBar1.Visible = false;
                    this.pictureBox1.Image = e.Bitmap;
                    MessageBox.Show(this, "Completed");
                });
            else
            {
                this.progressBar1.Enabled = false;
                this.progressBar1.Visible = false;
                this.pictureBox1.Image = e.Bitmap;
                MessageBox.Show(this, "Completed");
            }
        }
    
        /// <summary>
        /// downloader progress event handler
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void downloader_Progress(Downloader sender, Downloader.DownloaderProgressEventArgs e)
        {
            if (this.progressBar1.InvokeRequired)
                this.progressBar1.Invoke((MethodInvoker)delegate
                {
                    this.progressBar1.Value = e.Completed;
                    this.progressBar1.Maximum = e.Total;
                });
            else
            {
                this.progressBar1.Value = e.Completed;
                this.progressBar1.Maximum = e.Total;
            }
    
        }
    
        /// <summary>
        /// unhooks the events handlers and sets the downloader to null
        /// </summary>
        void unhookDownloader()
        {
            this.downloader.Progress -= downloader_Progress;
            this.downloader.Completed -= downloader_Completed;
            this.downloader.Cancelled -= downloader_Cancelled;
            this.downloader = null;
        }
    }
    

    这是一个如何使用Thread对象来完成工作的简单实现。在我看来,这是太多的工作。现在让我们在后台工作者中进行。

    我强烈推荐这种方法

    你为什么这么说?那么后台工作者为我们提供了我们试图实现的测试支持的方法(以及更多)。您会注意到下载图像和检测取消的实际工作是相对相同的。但是,您会注意到在发布已完成和进度事件时,我并不担心(尽可能多)关于跨线程问题。

    private void button2_Click(object sender, EventArgs e)
    {
        if (this.worker != null && this.worker.IsBusy)
        {
            this.worker.CancelAsync();
            return;
        }
    
        string[] addr = new string[] {".... our full addresss lists" };
    
        this.progressBar1.Maximum = addr.Length;
        this.progressBar1.Value = 0;
        this.progressBar1.Visible = true;
        this.progressBar1.Enabled = true;
        this.worker = new BackgroundWorker();
        this.worker.WorkerSupportsCancellation = true;
        this.worker.WorkerReportsProgress = true;
        this.worker.ProgressChanged += (s, args) =>
        {
            this.progressBar1.Value = args.ProgressPercentage;
        };
    
        this.worker.RunWorkerCompleted += (s, args) =>
        {
            this.progressBar1.Visible = false;
            this.progressBar1.Enabled = false;
    
            if (args.Cancelled)
            {
                MessageBox.Show(this, "Cancelled");
                worker.Dispose();
                worker = null;
                return;
            }
    
            var img = args.Result as Bitmap;
            if (img == null)
            {
                worker.Dispose();
                worker = null;
                return;
            }
            this.pictureBox1.Image = img;
    
            MessageBox.Show(this, "Completed");
            worker.Dispose();
            worker = null;
        };
    
        this.worker.DoWork += (s, args) =>
        {
            Bitmap favCollection = new Bitmap(96, 64);
            Graphics g = Graphics.FromImage(favCollection);
    
            for (var i = 0; i < addr.Length; i++)
            {
                if (worker.CancellationPending)
                    break;
    
                using (System.Net.WebClient client = new System.Net.WebClient())
                {
                    try
                    {
                        byte[] dl = client.DownloadData(addr[i]);
                        using (var stream = new MemoryStream(dl))
                        {
                            using (var dlImg = new Bitmap(stream))
                            {
                                g.DrawImage(dlImg, (i % 6) * 16, (i / 6) * 16, 16, 16);
                            }
                        }
                    }
                    catch (Exception)
                    {
                        using (var dlImg = new Bitmap(Properties.Resources.defaultFacIcon))
                        {
                            g.DrawImage(dlImg, (i % 6) * 16, (i / 6) * 16, 16, 16);
                        }
                    }
                }
                if (worker.CancellationPending)
                    break;
    
                this.worker.ReportProgress(i);
            }
    
            if (worker.CancellationPending)
            {
                g.Dispose();
                favCollection.Dispose();
                args.Cancel = true;
                return;
            }
            args.Cancel = false;
            args.Result = favCollection;
        };
        worker.RunWorkerAsync();
    }
    

    我希望这有助于理解一些可能的方法来实现您想要实现的目标。

    -Nico

答案 2 :(得分:0)

我会考虑使用微软的Reactive Framework。它自动处理背景线程,并将非常有效地清理所有一次性参考。 NuGet“Rx-Main”&amp; “RX-的WinForms”/ “RX-WPF”。

首先,从您的地址数组开始:

var addr = new []
{
    "http://google.com/favicon.ico",
    // DELETED FOR BREVITY
    "http://evernote.com/favicon.ico",
};

现在,定义一个异步获取图像的查询:

var query =
    from a in addr.ToObservable().Select((url, i) => new { url, i })
    from dl in Observable
        .Using(
            () => new System.Net.WebClient(),
            wc => Observable.FromAsync(() => wc.DownloadDataTaskAsync(a.url)))
    from bitmap in Observable
        .Using(
            () => new System.IO.MemoryStream(dl),
            ms => Observable.Start(() => new Bitmap(ms)))
        .Catch(ex => Observable.Return(new Bitmap(Properties.Resources.defaultFavIcon)))
    select new { x = (a.i % 6) * 16, y = (a.i / 6) * 16, bitmap };

最后,等待所有图像进入,然后在UI线程上创建合成图像并将其分配给passAddDisplay控件。

query
    .ToArray()
    .ObserveOn(passAddDisplay)
    .Subscribe(images =>
    {
        var favCollection = new Bitmap(96, 64);
        using(var g = Graphics.FromImage(favCollection))
        {
            foreach (var image in images)
            {
                g.DrawImage(image.bitmap, image.x, image.y, 16, 16);
                image.bitmap.Dispose();
            }
        }
        passAddDisplay.Image = favCollection;
    });

我测试了查询,它运行正常。