匹配條件
資料爭用或競爭條件是多執行緒程式未正確同步時可能發生的問題。如果兩個或多個執行緒在沒有同步的情況下訪問同一個記憶體,並且至少有一個訪問是寫入操作,則會發生資料爭用。這導致程式的平臺依賴性,可能不一致的行為。例如,計算結果可能取決於執行緒排程。
writer_thread {
write_to(buffer)
}
reader_thread {
read_from(buffer)
}
簡單的解決方案:
writer_thread {
lock(buffer)
write_to(buffer)
unlock(buffer)
}
reader_thread {
lock(buffer)
read_from(buffer)
unlock(buffer)
}
如果只有一個讀取器執行緒,這個簡單的解決方案很有效,但如果有多個讀取器執行緒,則會不必要地減慢執行速度,因為讀取器執行緒可以同時讀取。
避免此問題的解決方案可能是:
writer_thread {
lock(reader_count)
if(reader_count == 0) {
write_to(buffer)
}
unlock(reader_count)
}
reader_thread {
lock(reader_count)
reader_count = reader_count + 1
unlock(reader_count)
read_from(buffer)
lock(reader_count)
reader_count = reader_count - 1
unlock(reader_count)
}
請注意,reader_count
在整個寫入操作中被鎖定,因此在寫入尚未完成時,沒有讀者可以開始閱讀。
現在許多讀者可以同時閱讀,但可能會出現一個新問題:reader_count
可能永遠不會到達 0
,這樣編寫器執行緒永遠不能寫入緩衝區。這被稱為飢餓 ,有不同的解決方案來避免它。
即使是看似正確的程式也可能存在問題:
boolean_variable = false
writer_thread {
boolean_variable = true
}
reader_thread {
while_not(boolean_variable)
{
do_something()
}
}
示例程式可能永遠不會終止,因為讀者執行緒可能永遠不會看到來自編寫器執行緒的更新。例如,如果硬體使用 CPU 快取,則可以快取這些值。並且由於對正常欄位的寫入或讀取不會導致重新整理快取,因此讀取執行緒可能永遠不會看到更改的值。
C++和 Java 在所謂的記憶體模型中定義了正確同步的含義: C++ Memory Model , Java Memory Model 。
在 Java 中,解決方案是將欄位宣告為 volatile:
volatile boolean boolean_field;
在 C++中,解決方案是將欄位宣告為原子:
std::atomic<bool> data_ready(false)
資料競爭是一種競爭條件。但並非所有競爭條件都是資料競賽。由多個執行緒呼叫的以下內容會導致競爭條件,但不會導致資料爭用:
class Counter {
private volatile int count = 0;
public void addOne() {
i++;
}
}
它根據 Java 記憶體模型規範正確同步,因此它不是資料競爭。但它仍會導致競爭條件,例如結果取決於執行緒的交錯。
並非所有資料競爭都是錯誤。所謂的良性競爭條件的一個例子是 sun.reflect.NativeMethodAccessorImpl:
class NativeMethodAccessorImpl extends MethodAccessorImpl {
private Method method;
private DelegatingMethodAccessorImpl parent;
private int numInvocations;
NativeMethodAccessorImpl(Method method) {
this.method = method;
}
public Object invoke(Object obj, Object[] args)
throws IllegalArgumentException, InvocationTargetException
{
if (++numInvocations > ReflectionFactory.inflationThreshold()) {
MethodAccessorImpl acc = (MethodAccessorImpl)
new MethodAccessorGenerator().
generateMethod(method.getDeclaringClass(),
method.getName(),
method.getParameterTypes(),
method.getReturnType(),
method.getExceptionTypes(),
method.getModifiers());
parent.setDelegate(acc);
}
return invoke0(method, obj, args);
}
...
}
這裡程式碼的效能比 numInvocation 的計數的正確性更重要。