C 如何防止get()和set()之间的竞争条件行为?

C 如何防止get()和set()之间的竞争条件行为?,c,synchronization,race-condition,C,Synchronization,Race Condition,考虑以下全局变量foo,及其get()和set()函数: int32_t foo; mutexType fooMutex; int32_t getfoo(void) { int32_t aux; MutexLock(fooMutex); aux = foo; MutexUnlock(fooMutex); return aux; } void setfoo(int32_t value) { MutexLock(fooMutex); foo

考虑以下全局变量
foo
,及其
get()
set()
函数:

int32_t foo;
mutexType fooMutex;

int32_t getfoo(void)
{
    int32_t aux;
    MutexLock(fooMutex);
    aux = foo;
    MutexUnlock(fooMutex);
    return aux;
}
void setfoo(int32_t value)
{
    MutexLock(fooMutex);
    foo = value;
    MutexUnlock(fooMutex);
}
以下任务修改foo:

void ResetTask()
{
    while(1)
    {
        setfoo(resetvalue);
        AccessPeripheral2();
        wait(resetPeriod);
    }
}

void ActTask()
{
    while(1)
    {
        foocopy = getfoo();
        if (foocopy > 0)
        {
            x = AccessPeripheral1();
            setfoo( foocopy - x);
        }
        wait(actPeriod);
    }
}
accessPeripheralX()函数内部有一些关键部分,但它们保护不同的资源

如果实现了If条件(foopcopy>0),但在ActTask()的getfoo()和setfoo()之间重置了foo,那么在循环结束时,它将被设置为过时的无效值,从而产生错误的计算

如何防止这种竞争状况?
将ActTask()重写为:

在实践中消除了它,但从理论上讲,该任务仍然可以在获取和设置foo之间抢占先机

我还考虑在处理foo时引入另一个关键部分:

[Enter critical section]
foocopy = getfoo();
if (foocopy > 0)
{
    x = AccessPeripheral1();
    setfoo( foocopy - x);
}
[Leave critical section]


但我倾向于避免这种选择,因为我发现嵌套的关键部分会大大增加复杂性(以及bug的可能性)。

您只有一个关键部分(请参见下面的假设),它从读取
foo
到编写,因为编写取决于读取值。写入值并不取决于读取值,而是取决于写入的事实

只要读取值不影响写入(无论是值还是写入本身),读取本身就没有竞争条件。
例如,读取值并将其打印到控制台供人类思考是无害的

只要要写入的值不依赖于当前值,写入本身就没有竞争条件。例如,试图将5写入包含4的变量是无害的。然而,如果想法是在写入之后,变量应该比写入之前高一个,那么您就有了与任何其他增量访问的竞争条件。
例如,4上的两个抢占增量,预计都会产生影响,即正确的结果应为6,如果不受保护,则可能最终为5

我假设读/写单个变量是原子的。也就是说,如果您试图写入一个给定的值,并且抢占发生在另一个要写入的值上,那么任何一个值都会出现在变量中,而不是两者的混合,例如,一个值的高字节,另一个值的低字节。与读取相同,即使用抢占写入访问进行读取将产生旧的一致值或新的一致值。
严格地说,这是不正确的,也就是说,不需要符合标准的编译器来保证这一点。如果您担心这个特殊的原子性问题,那么将保护保留在getter和setter中,或者使用隐式保证这一点的写入机制

您描述的问题从读取访问开始

foocopy = getfoo();
因为此处读取的值会影响写入访问

setfoo( foocopy - x);
如果变量包含大于零的值,则只应写入新值,这一点从

if (foocopy > 0)
即,需要保护的实际临界截面如下

foocopy = getfoo();
if (foocopy > 0)
{
    x = AccessPeripheral1();
    setfoo( foocopy - x);
}   
当然,这正是您在倒数第二个代码片段中保护的部分。
然而,没有必要保护所有最后的代码片段。在易受攻击的关键部分期间,只需防止写入foo。这并不是因为写作本身是一个脆弱的批评部分;这只是“攻击者”,潜在的问题。需要将其锁定,以防止上述脆弱的临界段

综上所述,我提议如下:

int32_t foo;
mutexType paranoiaMutex;
/* only needed to protect the unrelated critical section of reading or writing,
   separatly, if that is not atomic */

int32_t getfoo(void)
{
    int32_t aux;
    MutexLock(paranoiaMutex);   /* or use atomic mechanism */
    aux = foo;
    MutexUnlock(paranoiaMutex); /* or use atomic mechanism */

    /* note this by the way,  
       you might have overlooked something in your design here */
    return aux; /* not foo */
}

void setfoo(int32_t value)
{
    MutexLock(paranoiaMutex);   /* or use atomic mechanism */
    foo = value;
    MutexUnlock(paranoiaMutex); /* or use atomic mechanism */

    /* Using fooMutex additionally here is imaginable,
       but in order to minimise the confusion of nested mutexes,
       I propose to use that mutex on the same "layer" of coding.
       Note that only one mutex and one layer of mutex nesting
       is occuring inside this code part.

       You ARE right about being careful with that...
     */
}

有可能对锁定互斥锁的持续时间进行优化,这是“昂贵的”。
该优化假设:

  • AccessPeripheral1()
    速度慢(大多数外围设备访问速度慢)

  • 可以
    访问外围设备1()
    if
    fooa添加一个额外的函数updateFoo()以原子方式处理foo的更改怎么样?非常感谢您的全面回答,我刚刚再次访问了它以解决类似的问题。第二段和第三段特别有用——我需要更新关键部分的正确定义。
    
    foocopy = getfoo();
    if (foocopy > 0)
    {
        x = AccessPeripheral1();
        setfoo( foocopy - x);
    }   
    
    int32_t foo;
    mutexType paranoiaMutex;
    /* only needed to protect the unrelated critical section of reading or writing,
       separatly, if that is not atomic */
    
    int32_t getfoo(void)
    {
        int32_t aux;
        MutexLock(paranoiaMutex);   /* or use atomic mechanism */
        aux = foo;
        MutexUnlock(paranoiaMutex); /* or use atomic mechanism */
    
        /* note this by the way,  
           you might have overlooked something in your design here */
        return aux; /* not foo */
    }
    
    void setfoo(int32_t value)
    {
        MutexLock(paranoiaMutex);   /* or use atomic mechanism */
        foo = value;
        MutexUnlock(paranoiaMutex); /* or use atomic mechanism */
    
        /* Using fooMutex additionally here is imaginable,
           but in order to minimise the confusion of nested mutexes,
           I propose to use that mutex on the same "layer" of coding.
           Note that only one mutex and one layer of mutex nesting
           is occuring inside this code part.
    
           You ARE right about being careful with that...
         */
    }
    
    mutexType fooMutex;
    /* Note that only one mutex and one layer of mutex nesting 
       is occuring inside this code part. */
    
    void ResetTask()
    {
        while(1)
        {
            MutexLock(fooMutex);
            setfoo(resetvalue);
            MutexUnlock(fooMutex);
            AccessPeripheral2();
            wait(resetPeriod);
        }
    }
    
    void ActTask()
    {
        while(1)
        {
            MutexLock(fooMutex);
            foocopy = getfoo();
            if (foocopy > 0)
            {
                x = AccessPeripheral1();
                setfoo( foocopy - x);
            }
            MutexUnlock(fooMutex);
            wait(actPeriod);
        }
    }
    
    void ActTask()
    {
        while(1)
        {
            x = AccessPeripheral1();
            MutexLock(fooMutex);
            foocopy = getfoo();
            if (foocopy > 0)
            {                
                setfoo( foocopy - x);
            }
            MutexUnlock(fooMutex);
            wait(actPeriod);
        }
    }