c# - 进度栏不适用于zipfile?程序似乎挂起时如何给出反馈

标签 c# multithreading zipfile

我对C#和编码尚不陌生,因此其中某些内容可能会以错误的方式处理问题。我编写的程序可以正常工作并按预期压缩文件,但是如果源文件很大,则该程序似乎(对于Windows)挂起。我觉得我应该使用Thread,但不确定是否会有所帮助。

我将使用进度条,但使用System.IO.Compression中的zipfile的"new"(.net 4.5)库替换了Ionic.Zip.ZipFile没有报告进度的方法吗?有没有解决的办法?我应该使用Thread吗?或DoWork

问题在于用户和系统无法获得程序正在执行的反馈。

我不确定我是否以正确的方式提出这个问题。
下面是正在运行的代码,但同样,它似乎会挂起系统。

    private void beginBackup_Click(object sender, EventArgs e)
    {
        try
        {
            long timeTicks = DateTime.Now.Ticks;
            string zipName = "bak" + timeTicks + ".zip";
            MessageBox.Show("This Will take a bit, there is no status bar :(");
            ZipFile.CreateFromDirectory(Properties.Settings.Default.source,
                  Properties.Settings.Default.destination + "\\" + zipName);
            MessageBox.Show("Done!");
            this.Close();
        }
        catch (IOException err)
        {
            MessageBox.Show("Something went wrong" + System.Environment.NewLine
                + "IOException source: {0}", err.Source);
        }
    }

重要的一行是:
        `ZipFile.CreateFromDirectory(Properties.Settings.Default.source,
              Properties.Settings.Default.destination + "\\" + zipName);`

编辑
ZipFile.CreateFromDirectory()没有遍历目录,因此没有要增加的内容吗?它只会在没有报告的情况下开始和结束。除非我弄错了?

在此使用此方法:
        while (!completed)
    {
        // your code here to do something
        for (int i = 1; i <= 100; i++)
        {
            percentCompletedSoFar = i;
            var t = new Task(() => WriteToProgressFile(i));
            t.Start();
            await t;
            if (progress != null)
            {
                progress.Report(percentCompletedSoFar);
            }
            completed = i == 100;
        }
    }

for循环中的代码只能运行一次,因为Zipfile woudl仍会挂起程序,那么进度条会立即从0变为100?

最佳答案

I would use a progress bar but the 'new' (.net 4.5) library for zipfile from System.IO.Compression which replaced Ionic.Zip.ZipFile does not have a method to report progress? Is there a way around this? Should I be using a Thread? or DoWork?



您这里确实有两个问题:
  • 的NETt_code类的.NET版本不包括进度报告。
  • ZipFile方法将阻塞,直到创建了整个存档为止。

  • 我对Ionic/DotNetZip库不是很熟悉,但是浏览文档时,我看不到任何用于从目录创建文件的异步方法。因此,无论如何,#2都是一个问题。解决它的最简单方法是在后台线程中运行工作,例如使用CreateFromDirectory()

    至于#1问题,我不会将.NET Task.Run()类描述为已替换了Ionic库。是的,它是新的。但是.NET在以前的版本中已经具有.zip存档支持。只是不是ZipFile这样的便利类。而且,早期对.zip归档文件和ZipFile的支持都没有“开箱即用”的进度报告。因此,它们本身都无法真正取代Ionic DLL。

    所以恕我直言,在我看来,如果您正在使用Ionic DLL并且对您有用,那么最好的解决方案就是继续使用它。

    如果您真的不想使用它,那么您的选择将受到限制。 .NET ZipFile只是不执行您想要的操作。您可以做一些骇人听闻的事情,以解决缺乏功能的问题。对于编写文件,您可以估算压缩后的大小,然后监视正在写入的文件大小,并根据该大小计算估算的进度(即每秒在一个单独的异步任务中轮询文件大小)。为了提取文件,您可以监视正在生成的文件,并以此方式计算进度。

    但归根结底,这种方法远非理想。

    另一种选择是使用较旧的基于ZipFile的功能来监视进度,自己明确编写存档并跟踪从源文件读取的字节。为此,您可以编写ZipArchive实现,该实现包装实际的输入流,并在读取字节时提供进度报告。

    这是Stream可能看起来像的一个简单示例(为说明起见,请注意注释……实际上最好委派所有虚拟方法,而不仅仅是委托(delegate)您使用的两个方法):

    注意:在寻找与此问题相关的现有问题的过程中,我发现了一个本质上是重复的问题,除了it's asking for a VB.NET answer instead of C#之外。除创建存档外,它还要求从存档中提取文件时进行进度更新。因此,我在这里针对VB.NET修改了答案,添加了提取方法,并对实现进行了一些调整。我更新了下面的答案以合并这些更改。

    StreamWithProgress.cs
    class StreamWithProgress : Stream
    {
        // NOTE: for illustration purposes. For production code, one would want to
        // override *all* of the virtual methods, delegating to the base _stream object,
        // to ensure performance optimizations in the base _stream object aren't
        // bypassed.
    
        private readonly Stream _stream;
        private readonly IProgress<int> _readProgress;
        private readonly IProgress<int> _writeProgress;
    
        public StreamWithProgress(Stream stream, IProgress<int> readProgress, IProgress<int> writeProgress)
        {
            _stream = stream;
            _readProgress = readProgress;
            _writeProgress = writeProgress;
        }
    
        public override bool CanRead { get { return _stream.CanRead; } }
        public override bool CanSeek {  get { return _stream.CanSeek; } }
        public override bool CanWrite {  get { return _stream.CanWrite; } }
        public override long Length {  get { return _stream.Length; } }
        public override long Position
        {
            get { return _stream.Position; }
            set { _stream.Position = value; }
        }
    
        public override void Flush() { _stream.Flush(); }
        public override long Seek(long offset, SeekOrigin origin) { return _stream.Seek(offset, origin); }
        public override void SetLength(long value) { _stream.SetLength(value); }
    
        public override int Read(byte[] buffer, int offset, int count)
        {
            int bytesRead = _stream.Read(buffer, offset, count);
    
            _readProgress?.Report(bytesRead);
            return bytesRead;
        }
    
        public override void Write(byte[] buffer, int offset, int count)
        {
            _stream.Write(buffer, offset, count);
            _writeProgress?.Report(count);
        }
    }
    

    有了它,使用Stream监视进度来显式地处理存档创建相对简单:

    ZipFileWithProgress.cs
    static class ZipFileWithProgress
    {
        public static void CreateFromDirectory(string sourceDirectoryName, string destinationArchiveFileName, IProgress<double> progress)
        {
            sourceDirectoryName = Path.GetFullPath(sourceDirectoryName);
    
            FileInfo[] sourceFiles =
                new DirectoryInfo(sourceDirectoryName).GetFiles("*", SearchOption.AllDirectories);
            double totalBytes = sourceFiles.Sum(f => f.Length);
            long currentBytes = 0;
    
            using (ZipArchive archive = ZipFile.Open(destinationArchiveFileName, ZipArchiveMode.Create))
            {
                foreach (FileInfo file in sourceFiles)
                {
                    // NOTE: naive method to get sub-path from file name, relative to
                    // input directory. Production code should be more robust than this.
                    // Either use Path class or similar to parse directory separators and
                    // reconstruct output file name, or change this entire method to be
                    // recursive so that it can follow the sub-directories and include them
                    // in the entry name as they are processed.
                    string entryName = file.FullName.Substring(sourceDirectoryName.Length + 1);
                    ZipArchiveEntry entry = archive.CreateEntry(entryName);
    
                    entry.LastWriteTime = file.LastWriteTime;
    
                    using (Stream inputStream = File.OpenRead(file.FullName))
                    using (Stream outputStream = entry.Open())
                    {
                        Stream progressStream = new StreamWithProgress(inputStream,
                            new BasicProgress<int>(i =>
                            {
                                currentBytes += i;
                                progress.Report(currentBytes / totalBytes);
                            }), null);
    
                        progressStream.CopyTo(outputStream);
                    }
                }
            }
        }
    
        public static void ExtractToDirectory(string sourceArchiveFileName, string destinationDirectoryName, IProgress<double> progress)
        {
            using (ZipArchive archive = ZipFile.OpenRead(sourceArchiveFileName))
            {
                double totalBytes = archive.Entries.Sum(e => e.Length);
                long currentBytes = 0;
    
                foreach (ZipArchiveEntry entry in archive.Entries)
                {
                    string fileName = Path.Combine(destinationDirectoryName, entry.FullName);
    
                    Directory.CreateDirectory(Path.GetDirectoryName(fileName));
                    using (Stream inputStream = entry.Open())
                    using(Stream outputStream = File.OpenWrite(fileName))
                    {
                        Stream progressStream = new StreamWithProgress(outputStream, null,
                            new BasicProgress<int>(i =>
                            {
                                currentBytes += i;
                                progress.Report(currentBytes / totalBytes);
                            }));
    
                        inputStream.CopyTo(progressStream);
                    }
    
                    File.SetLastWriteTime(fileName, entry.LastWriteTime.LocalDateTime);
                }
            }
        }
    }
    

    笔记:
  • 这使用一个称为Stream的类(请参见下文)。我在控制台程序中测试了代码,内置的BasicProgress<T>类将使用线程池执行Progress<T>事件处理程序,这又可能导致进度报告困惑。 ProgressChanged只是直接调用处理程序,从而避免了该问题。在使用BasicProgress<T>的GUI程序中,事件处理程序的执行将按顺序分派(dispatch)到UI线程。恕我直言,仍然应该在库中使用同步Progress<T>,但是使用BasicProgress<T>可以很好地使用UI程序的客户端代码(实际上,这可能是更好的选择,因为它代表您在那里处理跨线程分派(dispatch))。
  • 在进行任何工作之前,这将计算文件长度的总和。当然,这会产生少量的启动成本。在某些情况下,仅报告已处理的总字节数就足够了,让客户端代码担心是否需要进行初始计数。

  • BasicProgress.cs
    class BasicProgress<T> : IProgress<T>
    {
        private readonly Action<T> _handler;
    
        public BasicProgress(Action<T> handler)
        {
            _handler = handler;
        }
    
        void IProgress<T>.Report(T value)
        {
            _handler(value);
        }
    }
    

    当然,还有一个测试所有程序的程序:

    Program.cs
    class Program
    {
        static void Main(string[] args)
        {
            string sourceDirectory = args[0],
                archive = args[1],
                archiveDirectory = Path.GetDirectoryName(Path.GetFullPath(archive)),
                unpackDirectoryName = Guid.NewGuid().ToString();
    
            File.Delete(archive);
            ZipFileWithProgress.CreateFromDirectory(sourceDirectory, archive,
                new BasicProgress<double>(p => Console.WriteLine($"{p:P2} archiving complete")));
    
            ZipFileWithProgress.ExtractToDirectory(archive, unpackDirectoryName,
                new BasicProgress<double>(p => Console.WriteLine($"{p:P0} extracting complete")));
        }
    }
    

    关于c# - 进度栏不适用于zipfile?程序似乎挂起时如何给出反馈,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/42430559/

    相关文章:

    c# - 有条件地添加一个新的 json.net JProperty

    spring - 阻止 Spring MVC Controller ?

    .NET Core Web API 从内存流返回 Zip 文件

    download - Cypress - 如何验证文件是否已下载?

    c# - 当指定条件 != null 时,Automapper 未正确映射 null 列表成员

    c# - 记录 ActionResult 返回值的可接受方式

    c# - 在系统关闭时强制关闭应用程序

    java - 如果自定义线程池队列已满,如何拒绝调用?

    c++ - 如何在 Windows C++ 上分离线程