Это не ошибка в StringBuilder
, это ошибка в вашем коде. И модификация, которую вы показали в своем последующем ответе (где вы заменяете Log.String
на цикл, извлекающий символы по одному за раз), не исправляет это. Он больше не будет генерировать исключение, но он также не будет работать должным образом.
Проблема в том, что вы используете StringBuilder
в двух местах многопоточного кода, и одно из них не пытается его заблокировать, а это означает, что чтение может происходить в одном потоке одновременно с записью в другом. В частности, проблема заключается в этой строке:
_log.Log.AppendLine(act.Invoke());
Вы делаете это внутри Parallel.ForEach
. Здесь вы не предпринимаете никаких попыток синхронизации, даже если это будет выполняться одновременно несколькими потоками. Итак, у вас есть две проблемы:
- Несколько вызовов на
AppendLine
могут выполняться одновременно в нескольких потоках
- Один поток может пытаться вызвать
Log.ToString
одновременно с тем, как один или несколько других потоков вызывают AppendLine
Вы получите только одно чтение за раз, потому что вы используете ключевое слово lock
для их синхронизации. Проблема в том, что вы не получаете такую же блокировку при вызове AppendLine
.
Ваше «исправление» на самом деле не является исправлением. Вам удалось только усложнить задачу. Теперь это просто пойдет не так по-разному и более тонкими способами. Например, я предполагаю, что ваш метод Write
продолжает вызывать Log.Clear
после того, как ваш цикл for
завершит свою последнюю итерацию. Между завершением этой последней итерации и выполнением вызова Log.Clear
возможно, что какой-то другой поток получит другой вызов AppendLine
, потому что синхронизация этих вызовов AppendLine
отсутствует.
В результате вы будете иногда пропускать вещи. Код будет записывать в конструктор строк вещи, которые затем очищаются, даже не записываясь в потоковую запись.
Кроме того, существует довольно высокая вероятность того, что одновременные вызовы AppendLine
вызовут проблемы. Если вам повезет, они будут время от времени падать. (Это хорошо, потому что дает понять, что у вас есть проблема, которую нужно исправить.) Если вам не повезет, вы будете время от времени получать повреждение данных - два потока могут закончить запись в одно и то же место в результате StringBuilder
или в беспорядке, или полностью потерянные данные.
Опять же, это не ошибка в StringBuilder
. Он не предназначен для поддержки одновременного использования из нескольких потоков. Ваша задача - убедиться, что только один поток за раз делает что-либо для конкретного экземпляра StringBuilder
. Как сказано в документации для этого класса, «ни один из членов экземпляра не гарантированно является потокобезопасным».
Очевидно, что вы не хотите удерживать блокировку при вызове act.Invoke (), потому что это, вероятно, та самая работа, которую вы хотите распараллелить. Поэтому я думаю, что-то вроде этого может работать лучше:
string result = act();
lock(_log.Log)
{
_log.Log.AppendLine(result);
}
Однако, если бы я оставил его там, я бы на самом деле не помогал вам, потому что это выглядит очень неправильно для меня.
Если вы когда-нибудь обнаружите, что блокируете поле в чужом объекте, это является признаком проблемы проектирования в вашем коде. Возможно, было бы более разумно изменить дизайн, чтобы метод LogBuilder.Write
принимал строку. Честно говоря, я даже не уверен, почему вы вообще используете StringBuilder
здесь, так как вы, кажется, используете его просто как область хранения для строки, которую вы немедленно записываете в потоковую запись. Что вы надеялись добавить сюда StringBuilder
? Следующее будет проще и, похоже, ничего не потеряет (кроме исходных ошибок параллелизма):
public class LogBuilder : IDisposable
{
private readonly object _lock = new object();
private FileStream _fileStream;
private StreamWriter _streamWriter;
public LogBuilder()
{
_fileStream = new FileStream("log.txt", FileMode.Create, FileAccess.ReadWrite, FileShare.ReadWrite);
_streamWriter = new StreamWriter(_fileStream) { AutoFlush = true };
}
public void Write(string logLine)
{
lock (_lock)
{
_streamWriter.WriteLine(logLine);
}
}
public void Dispose()
{
_streamWriter.Dispose(); fileStream.Dispose();
}
}