【问题标题】:How can I read & write one file at same time by different threads on Android/iOS?如何通过 Android/iOS 上的不同线程同时读取和写入一个文件?
【发布时间】:2020-10-21 16:40:52
【问题描述】:

我有很多小文件。为了节省文件句柄和提高 IO 效率,这些文件被打包成一个大文件。但是,出于某种原因,这些小文件应该能够在运行时更新。所以需要不同线程同时更新和读取一个大文件的不同部分。

由于内存限制,mmap 不是一个好的选择。我必须自己实现它。但我担心在 iOS/Android 上同时读取和写入单个文件的不同部分是否安全。如何确保正在写入的块不会被其他线程读取。

我应该通过线程锁来实现整个功能还是已经有一些成熟的技术来做同样的工作?

顺便说一句,我在我的项目中使用 C++。但 Java 和 Obj-C 也是一种选择。

用户案例示例:

我的项目是一款 RPG 游戏。当人们看到一个未存储在原始包中的物品时,游戏会从服务器加载它并立即自动将其保存到磁盘中。

一个项目对应一个文件。每个文件差不多 300KB~1.5MB。服务器上有 3000~5000 个项目。在最坏的情况下,人们会在本地保存数千个文件。

好消息是我的用户可以按需加载项目以节省存储空间。并且在更新时,只会重新下载更改的项目。但是数千个文件将导致耗尽 FD 或其他资源的高风险。

这就是为什么我想将这些小文件打包成一个大包文件。但我仍然想保留更新/添加单个文件的能力。

【问题讨论】:

  • 如果您打算使用这种方法,是的,C 中的锁仍然是一个东西。但是您甚至没有提及您正在使用的语言,我将假设 lseek 到使文件处理程序跳转。您是否考虑过使用数据库来组织数据?在衡量它有多慢并知道它是否真的会影响您的性能之前,您是否尝试对其进行优化? mmap内存限制不是4GB吗,在这种情况下你真的打算在磁盘上有这么大的文件吗?为什么不使用 C api,在 ios 和 android 中都可用?
  • @Fabio 感谢您的回复。我没有考虑使用数据库来管理我的数据。我的数据由 3000~5000 个小文件组成,平均大小为 500KB。我没有使用数据库处理二进制文件的经验。它适合我的情况吗?
  • @Fabio 对于 mmap,我的项目很耗内存。此功能只有 10~20M 的内存预算。我认为 mmap 会占用与磁盘上文件大小相同大小的内存。我打算制作 3~4 个 500MB 的文件来处理这个小文件。在最坏的情况下,它将占用 2GB 内存。所以,基本上,如果我使用 C API 并保持所有线程不读取/写入文件的同一块,我的项目会正常工作吗?
  • 你是如何包装它们的?带拉链?
  • 这不是文件支持的 mmap 的工作方式,它是虚拟内存,因此并没有像您想象的那样真正使用 RAM。现在坏消息是,iOS 不会让你映射超过 700 MB stackoverflow.com/questions/13425558/why-does-mmap-fail-on-ios。现在,在为您的问题构建解决方案时,请编辑您的问题,使用您期望的写入/读取频率是多少,以及是否有用户交互,例如点击按钮并期望特定的二进制文件(又名 blob)立即在屏幕上加载某些内容.有很多选择,用户交互可能会推动最佳解决方案。

标签: android ios file io operating-system


【解决方案1】:

这里是前游戏开发者。

法比奥给出了一个非常好的和详细的答案。他对选项 1001 和 1002 的看法完全正确。我完全不会采用这种方法。

1 和 3 的组合是我的首选组合。您设置缓存大小,并在将新文件添加到缓存中时删除旧文件。

根据您的游戏设计(开放世界?游戏关卡),您可以有一个预处理程序,在关卡之前获取您需要的所有文件(同时显示加载屏幕),并确保它们在本地可用并从必要时联网。重新阅读您的帖子,您似乎已经这样做了?

但是数千个文件会导致耗尽 FD 或其他资源的高风险。

您不应该一次加载整个文件系统。只有特定级别需要的那些资产。如果您需要随时加载所有个文件,我建议您回到绘图板上重新审视您的设计和架构。

【讨论】:

  • 谢谢。因为游戏就像一个 MMORPG,所以很难判断会加载哪个项目。人们会从其他玩家那里加载物品,而且几乎是随机的。预处理不是很合适。在我当前的架构中,我同时只加载 50 ~ 100 个文件。超过限制的文件将按时间卸载。但是维护一个拥有数千个资源文件的系统需要花费很多。将数千个文件保存在一个文件夹中或在运行时卸载文件是高风险的。如果其他进程不小心对文件夹进行了迭代,就会导致卡住。卸载文件可能会导致资源丢失或崩溃。
  • 您不是使用服务器来集中存储所有文件吗?对等系统可能会变得非常复杂。我认为您需要 2 级缓存:内存和磁盘。所以假设我刚刚获得了一把我需要在游戏中渲染的剑。我需要一个 id,例如:ID_OBJECT_POINTY_SWORD 和磁盘上的路径位置。第 1 步:检查 ID_OBJECT_POINTY_SWORD 是否加载到内存中。如果它在内存中,只需抓取数据块并加载它。第 2 步:如果它不在内存中,请检查它是否在磁盘上。如果它在磁盘上,则将其加载到内存中并缓存它。
  • 第 3 步:如果它不在磁盘上,则从服务器获取。保存在磁盘上,加载到内存中。将缓存中的每个文件 ID 标记为上次使用的时间。您需要根据预算修复两个缓存的大小。然后,您需要一个定期进程,根据上次使用缓存的时间戳来清理缓存。这就像我可以概念化的一样简单。我建议不要试图过度思考或过度设计。
【解决方案2】:

简而言之,锁仍然是处理该问题的最佳方式,并且将永远成为开发人员工具带中的重要内容。

这类问题与解决它的方法一样普遍,几乎使这个答案基于意见。我会在这里和那里发表我的意见,但您需要根据对您来说最好或更容易做出自己的决定。

首先,在我看来,管理一个大小可变的大文件,其中包含许多大小可变的小东西,并使用多个线程即时删除和创建,这在我看来就像设计和实现文件系统一样复杂。与以下方法相比,我认为没有任何优势 - 好吧,也许它会非常快。但相信我,你既不需要也不想走那条路。

所以我不会完全回答您最初的问题,而是想向您展示一种风险较小的方法来解决您的问题。

出于实用目的,我将游戏项目称为asset。此外,我假设这些assets 不适合由 GPU 直接使用,例如纹理,这可能需要我没有经验的全新体验。

=========

1- 网络缓存方法

  • 找到一个缓存网络请求的库。
  • 每次您需要资产时,您都假装您是从网络中获取的,它会为您提供二进制文件。如果是第一次,它会从服务器请求它,否则它很可能在库缓存中找到一个副本。

ups:设置非常简单快捷。配置缓存大小并根据 LRU(最近最少使用)驱逐旧对象。如果服务器设置正确,您的应用程序就会知道它是否具有最新版本的资产或是否有新的要下载。并且无需关心锁和线程安全。

downs:如果您设置错误的缓存策略并且您的服务器没有正确公开缓存标头,则效率可能非常低。

对于这种方法,我可以推荐 Okhttp 版本 4,它是用 kotlin 编写的。这意味着你可以让它在 android 或 iOS 中运行,并且应该相对容易与 C/C++/Obj-C 交互(虽然我没有亲自尝试过),并且在 java 中很简单。

当然还有其他的库,但我知道没有其他库可以同时用于 C 和 Java/JVM。

=========

2- 单独跟踪个人assets

您可能需要一个中心类来确定资产是否可用、不可用或正在下载。您最终将需要它来检查较新的版本,并最终删除其中的几个以节省空间。

每个asset 都需要记住很多信息。我觉得自然的方法是拥有一个用于跟踪此类状态的数据库。

现在您有 2 个选项。您可以将 asset 作为 blob 存储在数据库中。或者获取一个唯一的文件名,自己将其保存到磁盘并将文件名存储在数据库中。我强烈建议使用后者,这将使您的调试变得更容易,风险也更小。

或者,您可以在应用启动时创建一个类,扫描可用的文件和版本,并将所有这些信息保存在内存中。

ups:将每个asset 单独存储为磁盘上的文件或blob。您可以跟踪使用它的次数,并根据需要提出删除它们的策略。 缺点:选择数据库可能需要很长时间。特别是,SQLite 和 RealmDb 可以在 android 和 iOS 上运行,所以你可以分享一些东西。

在阅读此答案时,我发现这篇非常有趣的文章声称在某些操作系统(包括 Android)上,从 sqlite 读取存储的小 blob(10kB)比从磁盘读取要快。有趣的惊喜,但只是稍微快一点,所以不值得仅仅为了这个收益而这样做。由于并行读取多个 blob 可能会在数据库上产生瓶颈。 https://www.sqlite.org/fasterthanfs.html

您只需要从磁盘读取与assets 一样多的文件描述符。之后,你应该把它保存在内存中并关闭fd?

================

3- 网络缓存,但带有内存缓存 因此,这是在 (1) 之上的优化,以防某些事情变得太慢。但与所有性能优化一样,我强烈建议您在花费时间之前进行测量。所以最后你知道你节省了多少时间,以及在你完成后是否值得额外的维护,而忘记它是如何工作的。

在这里,您可以汇总一个可以在内存中保存例如 50 个assets 的类,以便非常快速地访问。当它没有asset 时,它会请求网络库。

ups:它比 (1) 更高效,比 (2) 更简单。 缺点:它仍然比 (1) 复杂。

=================

1001 - 大文件和mmap

为什么我将此选项编号为 1001?因为它们是我推荐的顺序,我真的不推荐这种方法。

我多年前使用过 mmap,所以我希望我能正确记住它的细节。充其量它们仅适用于我使用它的具有 1 个核心处理器的 linux,并且请验证您是否在所需的平台上获得了相同的行为。

如果您创建一个 1GB 的文件并对其进行映射,您将不会消耗 1GB 的 RAM,因为那只是虚拟内存。当你读/写文件时,它消耗的物理内存与来自页面错误的页面数量成正比。

您不需要任何锁来读取或写入映射文件。只需读取和写入它,您就可以让下一次读取镜像上次写入。现在,我早在 2004 年就在旧的 1 核 cpu 计算机上完成了这项工作。它们在现代多核 cpu 中的表现如何,您如何确保在核心 1 写入内存位置(即文件区域)后,您可以在核心 2 上读取相同的值,而不是之前写入的值?我不知道,并敦促您在没有先学习之前不要实施它。

您需要为每个asset 分配offset 的算法使用锁/信号量和线程安全。当您的游戏请求asset 时,您需要确定它是否在磁盘上,这也意味着您知道它在磁盘上的位置。让我们称之为“哪里”offset。如果不是,您需要决定将其存储在何处,下载它并将文件offset 存储在某处。这就是你的代码中容易出现竞争条件的部分。

ups:快。但不确定比以前的方法快多少。如果您第一次需要资产,您仍然需要等待页面错误,这将从磁盘读取该文件区域并将其加载到物理内存中。 缺点:管理内存偏移和跨内核同步页面错误将使您成为更好的程序员,但代价是大量的时间和眼泪。根据我的经验,我很确定在 ios 或 Android 上会发生一些与预期不同的奇怪事情。赞Why does mmap fail on iOS?

https://medium.com/i0exception/memory-mapped-files-5e083e653b1

==================

1002 - 大文件和 lseek

是的,还有另一种我不推荐的方法。基本上是上面的,但是不是用 mmap 读写,而是为同一个文件创建一个或多个文件描述符,并使用lseek 来读/写内存区域。

它具有上一个选项的所有缺点,最多也具有相同的优点。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2012-12-26
    • 2018-11-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-03-04
    相关资源
    最近更新 更多