Reputation:
This code:
object obj = new object { };
Stopwatch watch = new Stopwatch();
watch.Start();
for (int i = 0; i < 90000; i++)
{
new Thread(() =>
{
lock (obj)
{
string file = new JavaScriptSerializer().Serialize(saeed);
File.AppendAllText(string.Format(@"c:\Temp\{0}.txt", i), file);
}
}).Start();
}
watch.Stop();
runs in like 15 minutes, while this code:
Stopwatch watch = new Stopwatch();
watch.Start();
for (int i = 0; i < 90000; i++)
{
{
string file = new JavaScriptSerializer().Serialize(saeed);
File.AppendAllText(string.Format(@"c:\Temp\{0}.txt", i), file);
}
}
watch.Stop();
runs in like 45 seconds. Why is the first application so much slower while it's threaded? Isn't it true that using threads is a technique for improving performance of an application?
Update: Even by using closure concepts and referencing a middle-variable instead of i
in my thread instead of using lock, which makes threads really async, still creating those files takes more than 5 minutes.
Stopwatch watch = new Stopwatch();
watch.Start();
for (int i = 0; i < 90000; i++)
{
var x = i;
new Thread(() =>
{
string file = new JavaScriptSerializer().Serialize(saeed);
File.AppendAllText(string.Format(@"c:\Temp\{0}.txt", i), file);
}).Start();
}
watch.Stop();
Upvotes: 6
Views: 5391
Reputation: 2451
I would note that most answers haven't read the example the code. This is not about spawning a bunch of threads and writing to disk, this is about spawning a bunch of threads, doing some work new JavaScriptSerializer().Serialize(saeed); and then writing to disk!
This is important to note, because the longer that work takes the more benefit simple threading provides by making sure the disk isn't idling while computation takes place.
The long and short of it is because you wrote some simplistic code, as others have explained:
A quick and easy way to get into threading - that's slightly less dangerous (though you can still stuff it up) is to use the Task Parallel Library. For example:
using System;
using System.Diagnostics;
using System.IO;
using System.Threading.Tasks;
namespace ConsoleApplication15
{
class Program
{
const int FILE_COUNT = 9000;
const int DATA_LENGTH = 100;
static void Main(string[] args)
{
if (Directory.Exists(@"c:\Temp\")) Directory.Delete(@"c:\Temp\", true);
Directory.CreateDirectory(@"c:\Temp\");
var watch = Stopwatch.StartNew();
for (int i = 0; i < FILE_COUNT; i++)
{
string data = new string(i.ToString()[0], DATA_LENGTH);
File.AppendAllText(string.Format(@"c:\Temp\{0}.txt", i), data);
}
watch.Stop();
Console.WriteLine("Wrote 90,000 files single-threaded in {0}ms", watch.ElapsedMilliseconds);
Directory.Delete(@"c:\Temp\", true);
Directory.CreateDirectory(@"c:\Temp\");
watch = Stopwatch.StartNew();
Parallel.For(0, FILE_COUNT, i =>
{
string data = new string(i.ToString()[0], DATA_LENGTH);
File.AppendAllText(string.Format(@"c:\Temp\{0}.txt", i), data);
});
watch.Stop();
Console.WriteLine("Wrote 90,000 files multi-threaded in {0}ms", watch.ElapsedMilliseconds);
}
}
}
The single threaded version runs in about 8.1 seconds, and the multi-threaded version runs in about 3.8 seconds. Note that my test values are different than yours.
While the TPL's default settings aren't always optimised for the scenario you're working on, they provide a much better basis than running 90,000 threads! You'll also note that in this case I don't have to do any locking nor do I have to handle the closure - because the API presented already handles that for me.
Upvotes: 7
Reputation: 941277
Threading can speedup your code by giving you more execution engines. But you are exploring very different resource limits in the first snippet.
The first one is the ability of the machine to commit 90 gigabytes of memory. The space required for the stacks of the threads. That takes a while, your harddisk if probably furiously working to create the backup store for that much memory. .NET is a bit unusual in that it commits the stack space for a thread, it provides an execution guarantee. Something you can turn off btw, the <disableCommitThreadStack>
element in the app.exe.config file ought to have a very noticeable effect.
The second resource limit you are exploring is the ability of the file system to modify that many files concurrently. It will be greatly hampered by the first limitation, you are stealing lots of RAM away from the file system cache. When it runs out of space, you are seeing the effect of these threads all trying to commandeer the disk write head. Forcing it to zip back and forth between the file clusters. Disk seeks are very slow, by far the slowest operation on a disk. It is a mechanical operation, the drive head arm needs to physically move, something that takes many milliseconds. The hard page faults your code is very likely to generate makes it a lot worse as well.
The lock in your threaded code will reduce this thrashing but won't eliminate it. With the large memory demand, your program is liable to generate a great many deal of page faults. Worse case is on every thread context switch. The thread will be blocked while the disk performs a seek + read to satisfy the page-in request.
Well, kudos to Windows by letting you do this and not falling over. But clearly this was a bad idea. Use at most a few threads. Or just one if the writes are going to saturate the file system cache anyway so you'll avoid the seek penalty.
Upvotes: 19
Reputation: 34489
The reason is two fold
expensive
in that it takes a non-trivial amount of time to do.obj
, this actually ensures only a single thread can run at a time in this example, so you're not actually running in a multi-threaded way. Upvotes: 4
Reputation: 48975
1) you are currently creating 90000 threads, which is not efficient at all. Don't create every time a thread, use a thread pool instead, so you reuse threads that are already created. Remember creating a thread takes some time and memory.
2) you are locking the whole code block with lock
, which means each thread is blocked until another thread completed its work. So you basically are defeating the whole purpose of multi-threading here.
3) Disk I/O doesn't work well with multi-threading for complex hardware-related reasons (buffers....etc.). Generally it's not a good idea to multi-thread this part of code.
About comments concerning disk I/O and multi-threading : that's quite complicated actually.
For magnetic disks, the disk arm has to move in order to read/write bytes on the good sector/cylinder/track. If you write 2 different files at the same time (case of two threads, each writing a different file), depending on the physical file location on disk, you may ask your disk arm to switch from one physical location to another very quickly, which destroys performances. Writing multiple disk sectors for the first file on one physical location, then moving the disk arm to another location, then writing some disks sectors for the second file would be much more efficient. You can see this effect when you compare the time to copy two files at the same time vs copying one file, then the other.
So for this very basic example, performance gain/loss depends on:
My humble advice: try to avoid multiple reads/writes in multiple threads if performances is your primary goal.
Upvotes: 34
Reputation: 15357
Because a thread is made inside the for loop with the lock. So the threads are performed one by one and not simultaneously like in the second example.
Upvotes: 3