multithreading 比赛条件

例子

数据竞争或竞争条件是多线程程序未正确同步时可能发生的问题。如果两个或多个线程在没有同步的情况下访问同一内存,并且至少其中一个访问是“写”操作,则会发生数据竞争。这会导致程序的平台相关的、可能不一致的行为。例如,计算结果可能取决于线程调度。

读者-作者问题

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++内存模型Java内存模型

在 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的计数的正确性更重要。