C# AsParallel并行时Random生成重复随机字符串引发文件占用问题
现有一个字符串数组,需要对数组内每个字符串执行对应处理任务,为提升处理效率采用并行方式执行,初始实现代码如下:
void Test() { sourcefiles.AsParallel().ForAll(MyMethod); } private void MyMethod(string source) { string tmp_string = RandomString(10); string path = Path.Combine(@"C:\myfolder", tmp_string); // 针对path的文件操作逻辑 } private static Random random = new Random(); public static string RandomString(int length) { const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; return new string(Enumerable.Repeat(chars, length) .Select(s => s[random.Next(s.Length)]).ToArray()); }
程序运行时持续抛出异常:Cannot access C:\myfolder\ABCDEFG because it is in use by another process。
该现象不符合预期:理论上不同并行任务生成的随机文件名应当互不重复,且该问题稳定复现,并非偶发事件。即使提升RandomString()方法生成字符串的复杂度也无法解决,调试时观测到两个不同执行任务会生成完全相同的tmp_string值,其余任务生成值均不重复,具体运行日志如下:
PROCESS - RANDOM_STRING 1937 - "r6MbODsNcF1654683907030" 1374 - "MqrdQe386M1654683928872" <--- 1518 - "iX33edEA5F1654683928873" 1691 - "MqrdQe386M1654683928872" <--- 1486 - "u46vqUrt601654684013613"
日志说明:字符串前10位为
RandomString()方法生成的随机字符,后续数字为Unix时间戳。
补充说明:有观点提出短时间内连续创建Random类实例会生成相同的种子,但该结论至少在.NET (Core)中并不成立。实际测试验证,通过加锁机制处理Random调用、或是采用线程安全的Guid生成随机字符串,均可正常解决该问题。
问题核心原因是**Random类本身不是线程安全的**。
本场景中全局共用了一个静态Random实例,多线程并行调用Next()方法时,会出现内部状态损坏:如果两个线程同时触发Next()调用,会导致内部种子状态的计算出现同步问题,最终返回完全相同的随机值,这就是稳定出现重复文件名、进而引发文件占用异常的根本原因,和Random实例创建时的种子重复问题无关。
可根据项目使用的.NET版本选择以下任意一种方案修复:
- 方案1:对
Random实例的调用加锁,保证同一时间只有一个线程执行Next()方法,避免并发修改实例内部状态,实现示例:
private static readonly Random random = new Random(); private static readonly object randLock = new object(); public static string RandomString(int length) { const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; lock (randLock) { return new string(Enumerable.Repeat(chars, length) .Select(s => s[random.Next(s.Length)]).ToArray()); } }
- 方案2:使用天然线程安全的
Guid生成随机文件名,无需额外处理锁逻辑,实现示例:
public static string RandomString(int length) { const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; // 若对随机字符范围有要求,也可基于Guid的字节值映射到指定字符集 return Guid.NewGuid().ToString("N").Substring(0, length).ToUpper(); }
- 方案3:如果项目基于.NET 6及以上版本,可以直接使用框架内置的线程安全全局随机实例
Random.Shared,无需自行实现锁逻辑,直接替换原有全局random实例即可:
public static string RandomString(int length) { const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; return new string(Enumerable.Repeat(chars, length) .Select(s => s[Random.Shared.Next(s.Length)]).ToArray()); }
内容的提问来源于stack exchange,提问作者Alessandro

