【问题标题】:When using FileStream.WriteAsync() what happens when the method is called again当使用 FileStream.WriteAsync() 再次调用该方法时会发生什么
【发布时间】:2015-02-20 06:48:55
【问题描述】:

我正在将视频流录制到磁盘。为此,我使用以下代码:

private async Task SaveToFile(IntPtr data, Int32 size)
{
    if (_FileStream == null) return;
    Byte[] buf = new Byte[size];
    Marshal.Copy(data, buf, 0, size);
    //dont await here - just continue
    _FileStream.WriteAsync(buf, 0, size);
}

到目前为止,它似乎没有问题。我只想确认的是,如果在上一次迭代完成之前调用此方法会发生什么。

你会注意到我没有等待WriteAsync() 电话。我不确定这是否正确,但我正在使用的 API 状态以在此方法中将操作保持在最低限度,以免在 API 内部阻塞回调。这似乎是正确的做法,只需将数据传递给流,然后立即返回。

如果最后一次WriteAsync() 通话尚未结束,我再次拨打WriteAsync() 时,谁能确认会发生什么?还是应该awaitWriteAsync() 调用?

编辑: 我还没有使用上述方法遇到异常,所以我想我的最后一个问题是从外部 DLL 回调内部使用 WriteAsync() 时是否有任何影响? DLL 是第三方 Directshow 组件。我无法验证它在内部是如何工作的,但它只是通过回调为我提供数据,我在其中保存到文件流。

【问题讨论】:

  • 如果你写一些简单的测试,你会看到多个WriteAsync没有await如果同时运行,会导致异常。
  • 我认为从外部 DLL 回调中调用它应该没问题,您还需要返回 Task 表单 _FileStream.WriteAsync(buf, 0, size);。也许这就是您没有看到异常的原因。
  • 您确实需要在这里await,正如编译器警告所暗示的那样。这并不意味着您不能并行使用多个WriteAsync,如果您真的需要的话。

标签: c# asynchronous async-await filestream


【解决方案1】:

异步重叠WriteAsync 操作没有任何问题,只要它们写入文件的不同、非重叠段:

using System;
using System.Linq;
using System.Threading.Tasks;

namespace ConsoleApplication
{
    class Program
    {
        static void Main(string[] args)
        {
            var tempFile = System.IO.Path.GetTempFileName();
            Console.WriteLine(tempFile);

            var fs = new System.IO.FileStream(
                tempFile, 
                System.IO.FileMode.Create, 
                System.IO.FileAccess.ReadWrite, 
                System.IO.FileShare.ReadWrite, 
                bufferSize: 256,
                useAsync: true);

            fs.SetLength(8192);

            var buff1 = Enumerable.Repeat((byte)0, 2048).ToArray();
            var buff2 = Enumerable.Repeat((byte)0xFF, 2048).ToArray();

            try
            {
                fs.Seek(0, System.IO.SeekOrigin.Begin);
                var task1 = fs.WriteAsync(buff1, 0, buff1.Length);

                fs.Seek(buff1.Length, System.IO.SeekOrigin.Begin);
                var task2 = fs.WriteAsync(buff2, 0, buff2.Length);

                Task.WhenAll(task1, task2).Wait();
                Console.ReadLine();
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex);
            }
        }
    }
}

在底层,这一切都归结为 WriteFile API 和非空 LPOVERLAPPED lpOverlapped,这是 Windows API 中支持的场景。

如果写入确实重叠,您仍然不会看到错误,但写入会竞争,最终结果可能无法预测。

【讨论】:

  • 我正要写一个答案,说同时启动多个 IO 是完全合法的。 +1。另一个答案谈到了与这里的问题无关的不同场景。
  • 您是否建议我可以简单地保留一个指向最新写入操作结束的指针,并在下次调用时寻找该位置?在实践中我没有看到上述代码中的任何问题,但这可能是由于我的 API 的工作原理。
  • @Simon,我相信您可以这样做,但我认为您还应该继续跟踪任何以前的 WriteAsync 操作的结果。喜欢this._pendingWrites.Add(_FileStream.WriteAsync(buf, 0, size))。在某些时候(当您完成直播时),您需要执行await Task.WhenAll(this._pendingWrites) 并处理它可能引发的任何错误。
  • 所以对WriteAsync() 的多次调用只有在每次写入调用Seek() 时才具有定义的行为?这是否意味着无法搜索的流的行为未定义?
  • @blinki,我相信是的,是的。
【解决方案2】:

您会注意到我没有等待 WriteAsync() 调用。我不确定这是否正确,但我使用的 API 状态是为了在此方法中将操作保持在最低限度,以免在 API 内部阻塞回调。

您没有显示足够的代码让我们任何人确切地知道您在这里做什么。但是,很明显,您可能需要一些帮助才能准确了解 async 方法的工作原理。

特别是,使用await 语句确实不会阻止方法的执行。事实上,恰恰相反:它会导致方法在该点返回。

它所做的另一件事是为等待的任务设置一个延续,以便当该任务完成时,继续执行。在您的方法中,对 WriteAsync() 方法的调用是该方法所做的最后一件事,因此实际上没有任何延续。但是使用await 会有另一个重要的效果:它允许your 方法返回的Task 正确指示整个方法的完成,包括对WriteAsync() 的调用。

就目前而言,您的方法在调用WriteAsync() 后立即返回(即在写操作实际完成之前),并且在调用者看来它已经完成,即使写操作尚未完成'不一定已经完成。

在您的示例中,您的方法写入一个明显的类成员_FileStream。在第一次完成之前再次调用该方法肯定会很糟糕;我怀疑您是否会遇到异常,但FileStream 绝不保证在您同时执行它们时会一致地写入文件。

这里最好的办法可能是不要使用async 并容忍写入阻塞。如果写入阻塞确实是您使用的 API 的问题,那么您应该使用缓冲区队列和单独的异步操作序列来写入文件,而不是每次 API 调用 WriteAsync()叫你写数据。

【讨论】:

  • 启动多个异步操作并不是一件坏事。事实上,它被设计为使用这种方式。请参阅讨论的另一个答案here
  • @SriramSakthivel:你说得对,它本质上不是一件坏事。但这就像在 OP 的代码中所做的那样,并且绝对需要非常小心才能正确执行。否则,文件中的数据将最终损坏。
  • 嗯,这是一个很好的论点。您需要非常小心地做正确的事情(不要在写入中使用重叠范围等)。而且我们也没有足够的信息来提供正确的方法。
  • 谢谢彼得,在这种情况下是否使用等待似乎对执行时间的上下文没有影响
【解决方案3】:

FileStream 不是线程安全的。从不同线程写入同一文件的两次写入将无法正常工作。

正如@Noseratio 所指出的,从同一线程对WriteAsync 进行重叠调用很好。

所以如果你是从不同的线程写入文件,你需要同步对文件的访问。

顺便说一句,我还会修改您的 SaveToFile 以返回任务,因为您没有使用 await,所以该方法也不需要是 async

private Task SaveToFile(IntPtr data, Int32 size)
{
    if (_FileStream == null) return;
    Byte[] buf = new Byte[size];
    Marshal.Copy(data, buf, 0, size);
    //dont await here - just continue
    return _FileStream.WriteAsync(buf, 0, size);
}

【讨论】:

  • 您的回答完全无关紧要。 msdn 中提供的信息只讨论如果另一个线程使用底层文件句柄并修改文件会发生什么。当多个线程使用相同的 FileStream 写入时不会。
  • 结果是一样的,两个不同的线程使用同一个句柄。
  • 我同意@SriramSakthivel。可以禁止多个线程同时访问同一个FileStream对象,但它与来自同一线程的多个异步WriteAsync操作无关。
  • @SriramSakthivel,只是一个小错误,FileStream 确实可以从多个线程访问,但不能同时访问。即,需要使用 lock 机制,就像许多其他 .NET 类一样。这包括WriteAsync,但只是方法调用本身,而不是异步操作的整个持续时间。
  • @Noseratio 是的,你是对的。这就是我试图用如果异步调用不交错这句话来解释的,但你做得更好。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-11-11
  • 2016-01-24
  • 1970-01-01
  • 2010-11-20
  • 2022-06-17
相关资源
最近更新 更多