【问题标题】:WebClient DownloadData method freezes FormWebClient DownloadData 方法冻结 Form
【发布时间】:2015-04-08 01:10:44
【问题描述】:

我正在使用来自 WebClient 对象的 DownloadData 从几个网站下载网站图标。

我收到一个字节数组的响应,一切正常,除了一件事:DownloadData 方法被执行时,它会冻结我的表单,直到该方法返回

现在,我已经通过使用 BackgroundWorker 对象解决了这个问题来完成工作,但我很好奇如何使用 System.Threading.Thread 实现同样的事情强>.

我尝试创建另一个下载网站图标的线程,然后循环我的 mainThread 直到线程完成处理,然后使用 Abort() 方法中止线程,但到目前为止,我的表单在另一个执行过程中被冻结线程。

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

    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 是一个已放置在我的表单上的图片框。

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

【问题讨论】:

  • 应该存在运行时错误,因为您正在从后台线程downloader 访问 UI 元素,这是不允许的。你怎么处理这个?您是否通过设置this.CheckForIllegalCrossThreadCalls = false 来抑制它?
  • 什么 UI 元素?你说的是 passAddDisplay 吗?
  • 是的,我指的是图片框。
  • 代码在 Form1 类中,使用相同的命名空间。运行此代码时没有编译器错误或运行时错误。一切正常,除了我的问题是冻结问题。
  • 请不要在线程上调用.Abort() - 这很危险。

标签: c# multithreading freeze webclient


【解决方案1】:

是的,它是一种同步方法,它导致线程等待而不处理消息,直到它返回;你应该试试它的异步版本,它没有。

【讨论】:

  • 感谢@nathan 的回答
【解决方案2】:

欢迎来到 Stack Overflow...

通过检查您的循环,很明显您将 UI 线程锁定在循环中。

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

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

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

  1. 活动结束
  2. 活动取消
  3. UI 进度事件

我不推荐这种方法(进一步阅读)

首先创建一个新类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; 这是对调用“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();
}

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

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

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

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

runDownloader() 方法的重要部分是它定期检查“中止”标志。如果此标志设置为truedownloader 将停在那里。现在请注意,您可能会遇到在 WebClient 正在下载图像时调用 abort 的情况。理想情况下,您会让WebClient 完成请求,正确处理然后退出循环。

每次下载图像后,都会触发进度事件(如果已订阅)。最后,当迭代完成并下载所有图像时,“Completed”事件将与编译的位图图像一起触发。

暂停一下

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

这是一个示例程序,我已对其部分进行了注释。

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 对象完成工作的简单实现。在我看来,这是太多的工作。现在让我们在后台工作人员中进行。

我强烈推荐这种方法

为什么你会说?好吧,Background Worker 为我们提供了我们尝试实现的 testedsupported 方法(以及更多)。您会注意到下载图像和检测取消的实际工作是相对相同的。但是您会注意到,在发布完成和进度事件时,我并不担心(同样多)跨线程问题。

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();
}

我希望这有助于了解实现您想要实现的目标的几种可能方法。

-妮可

【讨论】:

  • 感谢您抽出宝贵时间详细回答我的问题,非常感谢。它真的帮助我解决了我的困惑。正如我所提到的,我已经通过使用 BackgroundWorker 解决了它,但我不得不说,你的比我的要完整得多。我现在知道该怎么做!再次感谢你。 :)
【解决方案3】:

我会考虑为此使用 Microsoft 的响应式框架。它会自动处理后台线程,并且会非常有效地清理所有一次性引用。 NuGet“Rx-Main”和“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;
    });

我测试了查询,它工作正常。

【讨论】:

    猜你喜欢
    • 2011-11-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-10-27
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多