匹配條件

資料爭用或競爭條件是多執行緒程式未正確同步時可能發生的問題。如果兩個或多個執行緒在沒有同步的情況下訪問同一個記憶體,並且至少有一個訪問是寫入操作,則會發生資料爭用。這導致程式的平臺依賴性,可能不一致的行為。例如,計算結果可能取決於執行緒排程。

讀者 - 作家問題

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 ModelJava 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 的計數的正確性更重要。