You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何使StreamDocumentRepresentation类的异步方法实现线程安全,确保文件仅上传一次

Thread-Safe Implementation for StreamDocumentRepresentation

Great question! The core problem with your current code is that multiple concurrent calls to GetDocumentUriAsync() could all pass the initial string.IsNullOrWhiteSpace(_fileName) check before the first thread finishes uploading the document. This leads to duplicate uploads—definitely not what you want.

Let's cover two robust, async-friendly solutions to ensure the document is uploaded exactly once, no matter how many threads call the method.


Solution 1: Async Mutex with SemaphoreSlim (Double-Checked Locking)

Since we're dealing with async code, we can't use a standard lock statement (it blocks threads and doesn't play nice with await). Instead, SemaphoreSlim is perfect here—it supports asynchronous waiting and acts as a lightweight mutex.

Here's how to modify your class:

public class StreamDocumentRepresentation: DocumentRepresentation { 
    private readonly IFileStorageService _fileStorageService; 
    private readonly MemoryStream _documentStream; 
    private string _fileName;
    // Semaphore with initial count 1 to enforce mutual exclusion
    private readonly SemaphoreSlim _uploadLock = new SemaphoreSlim(1, 1);

    public StreamDocumentRepresentation(MemoryStream documentStream, IFileStorageService fileStorageService) { 
        _documentStream = documentStream; 
        _fileStorageService = fileStorageService; 
    } 

    public override async Task<string> GetDocumentUriAsync() { 
        // First quick check to avoid locking if we already have the filename
        if (string.IsNullOrWhiteSpace(_fileName)) { 
            await _uploadLock.WaitAsync();
            try {
                // Double-check after acquiring the lock—another thread might have finished uploading while we waited
                if (string.IsNullOrWhiteSpace(_fileName)) { 
                    await UploadDocument(); 
                }
            } finally {
                // Always release the lock, even if an exception occurs
                _uploadLock.Release();
            }
        } 
        return _fileName; 
    } 

    private async Task UploadDocument() { 
        _fileName = $"{Guid.NewGuid()}.pdf"; 
        // Optional: Reset stream position to start before uploading (in case the stream was read previously)
        _documentStream.Position = 0;
        await _fileStorageService.UploadFile(_fileName, _documentStream); 
    } 
}

Key Notes:

  • Double-Checked Locking: The first check skips locking if we already have the filename. The second check inside the lock ensures we don't re-upload if another thread completed the work while we waited for the lock.
  • SemaphoreSlim Cleanup: Always release the lock in a finally block to prevent deadlocks if an exception is thrown during upload.
  • Stream Position: Added _documentStream.Position = 0 as a safety measure—if the stream was read elsewhere before upload, resetting the position ensures the full content is uploaded.

Solution 2: AsyncLazy (Cleaner, More Maintainable)

If you prefer a more declarative approach, using an AsyncLazy wrapper (either from a NuGet package or a custom implementation) handles the thread safety automatically. This wraps the upload logic in a lazy-initialized task that only runs once.

Step 1: Implement a Custom AsyncLazy (if you don't want to use a package)

public class AsyncLazy<T> {
    private readonly Lazy<Task<T>> _lazyTask;

    public AsyncLazy(Func<Task<T>> taskFactory) {
        _lazyTask = new Lazy<Task<T>>(taskFactory);
    }

    public Task<T> GetValueAsync() => _lazyTask.Value;
}

Step 2: Modify StreamDocumentRepresentation

public class StreamDocumentRepresentation: DocumentRepresentation { 
    private readonly IFileStorageService _fileStorageService; 
    private readonly MemoryStream _documentStream;
    // Lazy-initialized task that handles upload exactly once
    private readonly AsyncLazy<string> _lazyFileName;

    public StreamDocumentRepresentation(MemoryStream documentStream, IFileStorageService fileStorageService) { 
        _documentStream = documentStream; 
        _fileStorageService = fileStorageService;
        // Initialize the lazy task with our upload logic
        _lazyFileName = new AsyncLazy<string>(async () => {
            var fileName = $"{Guid.NewGuid()}.pdf";
            _documentStream.Position = 0;
            await _fileStorageService.UploadFile(fileName, _documentStream);
            return fileName;
        });
    } 

    public override Task<string> GetDocumentUriAsync() { 
        // Simply return the lazy task—no manual locking needed!
        return _lazyFileName.GetValueAsync();
    } 
}

Key Notes:

  • Automatic Thread Safety: The Lazy<Task<T>> under the hood ensures the upload factory method runs exactly once, even with concurrent calls.
  • Cleaner Code: Eliminates manual lock handling, making the class easier to read and maintain.
  • NuGet Alternative: If you don't want to implement AsyncLazy yourself, use the Microsoft.VisualStudio.Threading package which includes a robust AsyncLazy<T> implementation.

Which Should You Choose?

  • Use SemaphoreSlim if you need fine-grained control over the locking logic or can't add external dependencies.
  • Use AsyncLazy for a cleaner, more maintainable solution—especially if you're already using packages like Microsoft.VisualStudio.Threading.

Both approaches guarantee that your document is uploaded exactly once, no matter how many threads call GetDocumentUriAsync().

内容的提问来源于stack exchange,提问作者Denys Doskach

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.30 11:02:31