我有一个decimal变量,在多个线程同时访问。Interlocked类函数根本不支持decimal,所以我唯一剩下的选择是使用lock(){}。这似乎有点过度了。
有没有其他方法以线程安全的方式增加decimal变量的值?
有没有其他方法以线程安全的方式增加decimal变量的值?
使用锁并不过度。它是必需的。
像 System.Decimal 这样的结构类型从来都不是原子的,也不适合本地 CPU 字长。这就是为什么 Interlocked 也没有为其提供重载。
decimal
的内部表示过于复杂,无法使用CPU级别的原子操作进行修改(大多数情况下,Interlocked
使用的就是这种操作,这也是你感兴趣的)。当CPU无法原子处理某些数量时,手动锁定是唯一的选择。您可以选择同步原语(例如使用lock
还是互斥量),但仅限于此。您仍然可以使用InterLocked
,但是这时需要将十进制数转换为Int64
。在进行转换时,您需要决定要保留多少小数位以保持精度。例如,如果您想保留4位小数,可以执行以下操作:
//Declare up front accessible from all threads
Int64 totalAmount = 0;
//Inside the thread you do this
var amount = (Int64)(decimalAmount * 10000); //10.000 is to preserve 4 decimal places
Interlocked.Add(ref totalAmount, amount);
//After all threads have finished, go back to decimal type.
var totalDecimalAmount = totalAmount / 10000;
Decimal.MaxValue
为79,228,162,514,264,337,593,543,950,335
,而Int64.MaxValue
为9,223,372,036,854,775,807
。因此,非常大的数字无法容纳。在保留4位小数的情况下,在Int64溢出之前最大的数字是9,223,372,036,854,775,807 / 10000 = 922,337,203,685,477
。Parallel.For
循环中使用这种方式的Interlocked
比使用lock
或互斥更快。如果您不介意将总数作为对象封装的 decimal
,您可以使用以下方法:
private static object myTotal = 0M;
static void InterlockedAddTotal(decimal val) {
object next;
object current;
do {
current = myTotal;
next = val + (decimal)current;
} while (Interlocked.CompareExchange(ref myTotal, next, current) != current);
}
decimal
封装在一个对象中,这会带来自己的性能影响。根据情况,使用锁可能更便宜。class A
{
decimal Value{get;set;}
}
var x=new A(){Value=10};
var y=new A(){Value=20};
x=y;//atomic
Equals()
方法以比较它们的Value
属性。 - ArtemiousA
,执行自己的递增并写回自己的值,最后一个写入者获胜。 - Jon BatesA
,执行自己的递增操作并写回自己的值,最后一个写入的线程会获胜。 - undefined如果关键部分非常轻量级,您可以使用自旋锁来获得更好的性能。
由于decimal
在内部由4个整数组成,即使设置值也不是原子操作,因此在以后读取该值时还需要使用相同的自旋锁
public class Counter
{
private decimal _value = 0;
private SpinLock _lock = new ();
public void Increment(decimal amount)
{
var lockTaken = false;
try
{
_lock.Enter(ref lockTaken);
_value += amount;
}
finally
{
if (lockTaken)
{
_lock.Exit(true); // use true on x64
}
}
}
}
lock
也会在争用的情况下稍微旋转一下,因此我不认为 SpinLock
的性能会比 lock
更好。就个人而言,我根本不使用这种机制。引用 Albahari 的书中的话:“当编写自己的可重用同步构造时,SpinLock
是最有意义的。即使如此,自旋锁也不像听起来那么有用。它仍然限制并发性,并浪费 CPU 时间。” - Theodor Zoulias
/ 10000
操作之前,确保 totalAmount 在线程安全的情况下被累加即可。并非所有代码都是“关键部分”,只有总和计算是如此。在所有线程处理完毕后,将评估totalDecimalAmount
。 - Mike de Klerk