Add opt-in multi-threading support with SupportsMultiThreadedExtraction flag

- Added IArchive.SupportsMultiThreadedExtraction property to indicate if multi-threading is supported
- Added ReaderOptions.EnableMultiThreadedExtraction option to opt-in to multi-threading
- Updated SeekableZipFilePart, TarFilePart, and SeekableFilePart to check the flag
- Added test to verify multi-threading flag behavior
- Multi-threading is now disabled by default for backward compatibility

Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-01-18 16:27:59 +00:00
parent 3e23a6e5a6
commit 4a6e5232ae
8 changed files with 129 additions and 18 deletions

View File

@@ -145,6 +145,19 @@ public abstract class AbstractArchive<TEntry, TVolume> : IArchive, IAsyncArchive
/// </summary>
public virtual bool IsEncrypted => false;
/// <summary>
/// Returns whether multi-threaded extraction is supported for this archive.
/// Multi-threading is supported when:
/// 1. The archive is opened from a FileInfo or file path (not a stream)
/// 2. Multi-threading is explicitly enabled in ReaderOptions
/// 3. The archive is not SOLID (SOLID archives should use sequential extraction)
/// </summary>
public virtual bool SupportsMultiThreadedExtraction =>
_sourceStream is not null
&& _sourceStream.IsFileMode
&& ReaderOptions.EnableMultiThreadedExtraction
&& !IsSolid;
/// <summary>
/// The archive can find all the parts of the archive needed to fully extract the archive. This forces the parsing of the entire archive.
/// </summary>

View File

@@ -44,4 +44,12 @@ public interface IArchive : IDisposable
/// Returns whether the archive is encrypted.
/// </summary>
bool IsEncrypted { get; }
/// <summary>
/// Returns whether multi-threaded extraction is supported for this archive.
/// Multi-threading is supported when the archive is opened from a FileInfo or file path
/// (not a stream) and the format supports random access (e.g., Zip, Tar, Rar).
/// SOLID archives (some Rar, all 7Zip) should use sequential extraction for best performance.
/// </summary>
bool SupportsMultiThreadedExtraction { get; }
}

View File

@@ -27,9 +27,13 @@ internal class SeekableFilePart : RarFilePart
{
Stream streamToUse;
// If the stream is a SourceStream in file mode, create an independent stream
// to support concurrent multi-threaded extraction
if (_stream is SourceStream sourceStream && sourceStream.IsFileMode)
// If the stream is a SourceStream in file mode with multi-threading enabled,
// create an independent stream to support concurrent extraction
if (
_stream is SourceStream sourceStream
&& sourceStream.IsFileMode
&& sourceStream.ReaderOptions.EnableMultiThreadedExtraction
)
{
var independentStream = sourceStream.CreateIndependentStream(0);
if (independentStream is not null)

View File

@@ -23,9 +23,13 @@ internal sealed class TarFilePart : FilePart
{
if (_seekableStream is not null)
{
// If the seekable stream is a SourceStream in file mode, create an independent stream
// to support concurrent multi-threaded extraction
if (_seekableStream is SourceStream sourceStream && sourceStream.IsFileMode)
// If the seekable stream is a SourceStream in file mode with multi-threading enabled,
// create an independent stream to support concurrent extraction
if (
_seekableStream is SourceStream sourceStream
&& sourceStream.IsFileMode
&& sourceStream.ReaderOptions.EnableMultiThreadedExtraction
)
{
var independentStream = sourceStream.CreateIndependentStream(0);
if (independentStream is not null)

View File

@@ -61,11 +61,15 @@ internal class SeekableZipFilePart : ZipFilePart
private void LoadLocalHeader()
{
// Use an independent stream for loading the header if possible
// Use an independent stream for loading the header if multi-threading is enabled
Stream streamToUse = BaseStream;
bool disposeStream = false;
if (BaseStream is SourceStream sourceStream && sourceStream.IsFileMode)
if (
BaseStream is SourceStream sourceStream
&& sourceStream.IsFileMode
&& sourceStream.ReaderOptions.EnableMultiThreadedExtraction
)
{
var independentStream = sourceStream.CreateIndependentStream(0);
if (independentStream is not null)
@@ -110,11 +114,15 @@ internal class SeekableZipFilePart : ZipFilePart
private async ValueTask LoadLocalHeaderAsync(CancellationToken cancellationToken = default)
{
// Use an independent stream for loading the header if possible
// Use an independent stream for loading the header if multi-threading is enabled
Stream streamToUse = BaseStream;
bool disposeStream = false;
if (BaseStream is SourceStream sourceStream && sourceStream.IsFileMode)
if (
BaseStream is SourceStream sourceStream
&& sourceStream.IsFileMode
&& sourceStream.ReaderOptions.EnableMultiThreadedExtraction
)
{
var independentStream = sourceStream.CreateIndependentStream(0);
if (independentStream is not null)
@@ -162,9 +170,13 @@ internal class SeekableZipFilePart : ZipFilePart
protected override Stream CreateBaseStream()
{
// If BaseStream is a SourceStream in file mode, create an independent stream
// to support concurrent multi-threaded extraction
if (BaseStream is SourceStream sourceStream && sourceStream.IsFileMode)
// If BaseStream is a SourceStream in file mode with multi-threading enabled,
// create an independent stream to support concurrent extraction
if (
BaseStream is SourceStream sourceStream
&& sourceStream.IsFileMode
&& sourceStream.ReaderOptions.EnableMultiThreadedExtraction
)
{
// Create a new independent stream for this entry
var independentStream = sourceStream.CreateIndependentStream(0);

View File

@@ -28,4 +28,12 @@ public class ReaderOptions : OptionsBase
/// When set, progress updates will be reported as entries are extracted.
/// </summary>
public IProgress<ProgressReport>? Progress { get; set; }
/// <summary>
/// Enable multi-threaded extraction support when the archive is opened from a FileInfo or file path.
/// When enabled, multiple threads can extract different entries concurrently by creating
/// independent file streams. This is only effective for archives opened from files, not streams.
/// Default is false for backward compatibility.
/// </summary>
public bool EnableMultiThreadedExtraction { get; set; }
}

View File

@@ -18,7 +18,16 @@ public class TarMultiThreadTests : TestBase
var testArchive = Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar");
var fileInfo = new FileInfo(testArchive);
using var archive = TarArchive.OpenArchive(fileInfo);
var options = new SharpCompress.Readers.ReaderOptions
{
EnableMultiThreadedExtraction = true,
};
using var archive = TarArchive.OpenArchive(fileInfo, options);
// Verify multi-threading is supported
Assert.True(archive.SupportsMultiThreadedExtraction);
var entries = archive.Entries.Where(e => !e.IsDirectory).Take(5).ToList();
// Extract multiple entries concurrently
@@ -62,7 +71,12 @@ public class TarMultiThreadTests : TestBase
var testArchive = Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar");
var fileInfo = new FileInfo(testArchive);
using var archive = TarArchive.OpenArchive(fileInfo);
var options = new SharpCompress.Readers.ReaderOptions
{
EnableMultiThreadedExtraction = true,
};
using var archive = TarArchive.OpenArchive(fileInfo, options);
var entries = archive.Entries.Where(e => !e.IsDirectory).Take(5).ToList();
// Extract multiple entries concurrently

View File

@@ -11,6 +11,35 @@ namespace SharpCompress.Test.Zip;
public class ZipMultiThreadTests : TestBase
{
[Fact]
public void Zip_Archive_Without_MultiThreading_Enabled()
{
// Test that extraction still works when multi-threading is NOT enabled
var testArchive = Path.Combine(TEST_ARCHIVES_PATH, "Zip.none.zip");
var fileInfo = new FileInfo(testArchive);
// Default options - multi-threading disabled
using var archive = ZipArchive.OpenArchive(fileInfo);
// Verify multi-threading is NOT supported
Assert.False(archive.SupportsMultiThreadedExtraction);
var entry = archive.Entries.First(e => !e.IsDirectory);
var outputFile = Path.Combine(SCRATCH_FILES_PATH, entry.Key!);
var dir = Path.GetDirectoryName(outputFile);
if (dir != null)
{
Directory.CreateDirectory(dir);
}
using var entryStream = entry.OpenEntryStream();
using var fileStream = File.Create(outputFile);
entryStream.CopyTo(fileStream);
Assert.True(File.Exists(outputFile));
}
[Fact]
public void Zip_Archive_Concurrent_Extraction_From_FileInfo()
{
@@ -18,7 +47,16 @@ public class ZipMultiThreadTests : TestBase
var testArchive = Path.Combine(TEST_ARCHIVES_PATH, "Zip.none.zip");
var fileInfo = new FileInfo(testArchive);
using var archive = ZipArchive.OpenArchive(fileInfo);
var options = new SharpCompress.Readers.ReaderOptions
{
EnableMultiThreadedExtraction = true,
};
using var archive = ZipArchive.OpenArchive(fileInfo, options);
// Verify multi-threading is supported
Assert.True(archive.SupportsMultiThreadedExtraction);
var entries = archive.Entries.Where(e => !e.IsDirectory).Take(5).ToList();
// Extract multiple entries concurrently
@@ -62,7 +100,12 @@ public class ZipMultiThreadTests : TestBase
var testArchive = Path.Combine(TEST_ARCHIVES_PATH, "Zip.none.zip");
var fileInfo = new FileInfo(testArchive);
using var archive = ZipArchive.OpenArchive(fileInfo);
var options = new SharpCompress.Readers.ReaderOptions
{
EnableMultiThreadedExtraction = true,
};
using var archive = ZipArchive.OpenArchive(fileInfo, options);
var entries = archive.Entries.Where(e => !e.IsDirectory).Take(5).ToList();
// Extract multiple entries concurrently
@@ -105,7 +148,12 @@ public class ZipMultiThreadTests : TestBase
// Test concurrent extraction when opening from path (should use FileInfo internally)
var testArchive = Path.Combine(TEST_ARCHIVES_PATH, "Zip.none.zip");
using var archive = ZipArchive.OpenArchive(testArchive);
var options = new SharpCompress.Readers.ReaderOptions
{
EnableMultiThreadedExtraction = true,
};
using var archive = ZipArchive.OpenArchive(testArchive, options);
var entries = archive.Entries.Where(e => !e.IsDirectory).Take(5).ToList();
// Extract multiple entries concurrently