Reputation: 391
i have sample program in which i have tried to replicate my actual application scenerio.Is there a way to lock only once rahter than for each loop which actual degrades the performance of parallel loop .If i remove the lock the performance is as expected but i run in race condition.I have certain code in the GetTotal method that also enters the race condition .Is parallel processing possible in such scenarios where multiple threads are trying to modify the shared variables .Is there a better to improve the long running performance
private static void Main()
{
var datetime = DateTime.Now;
int j = 0;
Parallel.ForEach(Enumerable.Range(0, 5), i =>
{
lock (SomeLockObject)
{
Console.WriteLine(j++);
GetTotal(j);
}
});
Console.WriteLine(DateTime.Now.Second - datetime.Second);
Console.ReadLine();
}
static long GetTotal(int j)
{
long total = 0;
for (int i = 1; i < 1000000000; i++) // Adjust this loop according
{ // to your computer's speed
total += i + j;
}
return total;
}
Upvotes: 0
Views: 4157
Reputation: 480
The main issue with the code is that
Parallel.ForEach(Enumerable.Range(0, 5), i =>{lock(SomeLockObject){/*some work*/}}
is not allowed to do anything in parallel. The lock(SomeLockObject) allows only one thread to enter the lock at a time completely defeating the Parallel.ForEach.
When attempting to parallelize any algorithm it is important to first figure out what parts can be isolated data-wise. In this case it is all but the j++ operation since j is the only interesting shared piece of memory. meaning that in order to get some degree of parallelism you would have to adjust your locking to
Parallel.ForEach(Enumerable.Range(0, 5), i =>
{
int tempj;
lock (SomeLockObject)
{
tempj = j++;
}
Console.WriteLine(tempj-1);
GetTotal(tempj);
});
note: Console is thread-safe see link.
This will give you a great speed increase but there is an even faster way of doing things by skipping the locking altogether.
Parallel.ForEach(Enumerable.Range(0, 5), i =>
{
int tempj = Interlocked.Increment(ref j);
Console.WriteLine(tempj-1);
GetTotal(tempj);
});
The problem with j++ is that it consists of several operation: get j, add 1 to j, store j. Below is a short codeblock to show how j++ done in two treads can give some weird results.
int j = 0; //intitialize j
int aj = j; //thread a gets j, aj = 0 , j = 0
aj = aj + 1; //thread a increments aj = 1, j = 0
int bj = j; //thread b gets j, aj = 1, bj = 0, j = 0
bj = bj + 1; //thread b increments bj = 1, aj = 1, j = 0
j = bj; //thread b writes j = 1, aj = 1
j = aj; //thread a writes j = 1
as you can see j is still only 1!
Interlocked solves this by providing atomic operations. Interlocked.Increment issues a command that tells the processor that it should increment j and return it as if it was just one operation, preventing the above race condition.
The use of the temporary variable is necessary because other threads will also be using j and we want GetTotal and Console.WriteLine to be Called with each j.
Final note: j++ first returns j and then increments its value hence the tempj-1
int j = 0;
int pre = j++; //pre = 0, j = 1
j = 0;
int after = ++j // after = 1, j = 1;
Upvotes: 2
Reputation: 479
You could look at the Concurrent libraries for C#. They allow for much faster threaded read/write of shared data.
This is from the Concurrent link above:
Some of the concurrent collection types use lightweight synchronization mechanisms such as SpinLock, SpinWait, SemaphoreSlim, and CountdownEvent, which are new in the .NET Framework 4. These synchronization types typically use busy spinning for brief periods before they put the thread into a true Wait state. When wait times are expected to be very short, spinning is far less computationally expensive than waiting, which involves an expensive kernel transition. For collection classes that use spinning, this efficiency means that multiple threads can add and remove items at a very high rate. For more information about spinning vs. blocking, see SpinLock and SpinWait.
These classes will only solve the issue of faster communication between threads. It also looks like you should do some improvement on only locking when you absolutely need to.
Here is an example of using the Interlocked class to lock on the incrementing of j without slowing down at all.
int j = 0;
Parallel.ForEach(Enumerable.Range(0, 5), i =>
{
Console.WriteLine(Interlocked.Increment(j));
GetTotal(j);
});
Upvotes: 1
Reputation: 134125
I don't know exactly what you're trying to demonstrate here. Incrementing j
is definitely something you want to protect, but the GetTotal
method is completely self-contained (i.e. doesn't reference shared state), so it doesn't need to be protected with a lock. I think you'd see quite a performance increase if you were to make a small change:
int j = 0;
Parallel.ForEach(Enumerable.Range(0, 5), i =>
{
lock (SomeLockObject)
{
Console.WriteLine(j++);
}
GetTotal();
}
Now only the code that requires synchronization is protected by the lock.
Your example is obviously contrived, so I can't say with any confidence that this will solve the real problem you're having.
Upvotes: 1