Sorry for big chunk of code, I couldn’t explain that with less.Basically I’m trying to write into a file from many tasks.
Can you guys please tell me what I’m doing wrong? _streamWriter.WriteLine() throws the ArgumentOutOfRangeException.
class Program
{
private static LogBuilder _log = new LogBuilder();
static void Main(string[] args)
{
var acts = new List<Func<string>>();
var rnd = new Random();
for (int i = 0; i < 10000; i++)
{
acts.Add(() =>
{
var delay = rnd.Next(300);
Thread.Sleep(delay);
return "act that that lasted "+delay;
});
}
Parallel.ForEach(acts, act =>
{
_log.Log.AppendLine(act.Invoke());
_log.Write();
});
}
}
public class LogBuilder : IDisposable
{
public StringBuilder Log = new StringBuilder();
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()
{
lock (Log)
{
if (Log.Length <= 0) return;
_streamWriter.WriteLine(Log.ToString()); //throws here. Although Log.Length is greater than zero
Log.Clear();
}
}
public void Dispose()
{
_streamWriter.Close(); _streamWriter.Dispose(); _fileStream.Close(); fileStream.Dispose();
}
}
This is not a bug in
StringBuilder, it’s a bug in your code. And the modification you shown in your followup answer (where you replaceLog.Stringwith a loop that extracts characters one at a time) doesn’t fix it. It won’t throw an exception any more, but it won’t work properly either.The problem is that you’re using the
StringBuilderin two places in your multithreaded code, and one of them does not attempt to lock it, meaning that reading can occur on one thread simultaneously with writing occurring on another. In particular, the problem is this line:You’re doing that inside your
Parallel.ForEach. You are not making any attempt at synchronization here, even though this will run on multiple threads at once. So you’ve got two problems:AppendLinemay be in progress simultaneously on multiple threadsLog.ToStringat the same time as one or more other threads are callingAppendLineYou’ll only get one read at a time because you are using the
lockkeyword to synchronize those. The problem is that you’re not also acquiring the same lock when callingAppendLine.Your ‘fix’ isn’t really a fix. You’ve succeeded only in making the problem harder to see. It will now merely go wrong in different and more subtle ways. For example, I’m assuming that your
Writemethod still goes on to callLog.Clearafter yourforloop completes its final iteration. Well in between completing that final iteration, and making the call toLog.Clear, it’s possible that some other thread will have got in another call toAppendLinebecause there’s no synchronization on those calls toAppendLine.The upshot is that you will sometimes miss stuff. Code will write things into the string builder that then get cleared out without ever being written to the stream writer.
Also, there’s a pretty good chance of concurrent
AppendLinecalls causing problems. If you’re lucky they will crash from time to time. (That’s good because it makes it clear you have a problem to fix.) If you’re unlucky, you’ll just get data corruption from time to time – two threads may end up writing into the same place in theStringBuilderresulting either in a mess, or completely lost data.Again, this is not a bug in
StringBuilder. It is not designed to support being used simultaneously from multiple threads. It’s your job to make sure that only one thread at a time does anything to any particular instance ofStringBuilder. As the documentation for that class says, “Any instance members are not guaranteed to be thread safe.”Obviously you don’t want to hold the lock while you call act.Invoke() because that’s presumably the very work you want to parallelize. So I’d guess something like this might work better:
However, if I left it there, I wouldn’t really be helping you, because this looks very wrong to me.
If you ever find yourself locking a field in someone else’s object, it’s a sign of a design problem in your code. It would probably make more sense to modify the design, so that the
LogBuilder.Writemethod accepts a string. To be honest, I’m not even sure why you’re using aStringBuilderhere at all, as you seem to use it just as a holding area for a string that you immediately write to a stream writer. What were you hoping theStringBuilderwould add here? The following would be simpler and doesn’t seem to lose anything (other than the original concurrency bugs):