【问题标题】:Calling a function by pointer located in an array while changing the array content在更改数组内容时通过位于数组中的指针调用函数
【发布时间】:2017-02-13 10:00:34
【问题描述】:

我有一个函数,它遍历函数指针数组并按顺序调用这些函数中的每一个。

如果函数指针数组的内容在这个过程中发生了变化,会发生什么?函数调用是否可以被认为是足够原子的,以确保不会发生任何意外,或者必须注意不要在例如压栈时更改函数指针?

下面的一些示例(伪)代码。 init() 在启动时运行一次,而 callFunctions() 定期运行,比如每秒一次。然后 changeFunctions() 出现并更改 functionPtrArray[] 的内容。这可能发生在任何时间点,因为代码在类似操作系统的环境中的不同进程中运行。

void (*functionPtrArray[3]) ( void );

void init( void )
{
   functionPtrArray[0] = function1;
   functionPtrArray[1] = function2;
   functionPtrArray[2] = function3;
}

void callFunctions( void )
{
   for ( i = 0; i < 3; i++ )
   {
      *functionPtrArray[i]();
   }
}

void changeFunctions( void )
{
   functionPtrArray[0] = newFunction1;
   functionPtrArray[1] = newFunction2;
   functionPtrArray[2] = newFunction3;
}

【问题讨论】:

  • 关键问题是,一旦执行 f[0],f[1] 和 f[2] 是否来自与 f[0] 相同的集合是否很重要。否则你的代码很好(假设检索和存储函数指针是原子的,基本上意味着它们是本机 CPU 和总线字大小,因此读/写周期在一个周期内获取/存储整个本机机器字)。
  • @PaulOgilvie 是什么让您认为任何给定的通用 CPU 可以在一个周期内读取一个字(x 字节)?即使该假设是正确的(事实并非如此),许多架构的地址都超出了机器字的大小,旨在用于存储代码和数据。

标签: c function function-pointers


【解决方案1】:

编辑:我太长了,被伦丁抢先了:https://stackoverflow.com/a/42201493/5592711

重要的一点:在C 中调用函数归结为跳转,因此当程序执行函数时,更改用于进行函数调用的假设指针不会改变程序流程,因为指令指针已经在函数内。

比如在这个sn-p中some_function()会正常执行,不受ptr的修改影响。

void(*ptr)(void) = &some_function;
(*ptr)();
// ... in another thread, while 'some_function()' is executing
ptr = &other_function;

然而,你在这里提出了一个重要的观点,因为内存操作不是原子的,一些线程可以在另一个线程正在读取它的同时修改 functionPtrArray[0] 元素,这将导致它跳转到一些垃圾地址并导致你的程序会失败,就像这个假设的例子一样。

为确保线程之间的同步,您可以使用mutexes,例如使用pthread 库(您可以自己谷歌一下,您会发现很多信息)。

在您的示例中,使用此类同步可能如下所示:

// You can use static mutex initialization
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

void callFunctions( void )
{
   pthread_mutex_lock(&mutex);
   for ( i = 0; i < 3; i++ )
   {
      *functionPtrArray[i]();
   }
   pthread_mutex_unlock(&mutex);
}

void changeFunctions( void )
{
   pthread_mutex_lock(&mutex);
   functionPtrArray[0] = newFunction1;
   functionPtrArray[1] = newFunction2;
   functionPtrArray[2] = newFunction3;
   pthread_mutex_unlock(&mutex);
}

这样,修改函数指针数组和执行函数是互斥的任务。 你应该知道这个例子有几个限制:

  1. 在整个函数执行过程中互斥锁被锁定(不需要,你可以锁定它来读取函数指针,解锁然后执行它们)
  2. 函数必须自己调用changeFunctions,否则最终会陷入死锁。

【讨论】:

  • “修改...而另一个线程正在读取它”:我认为从数组中获取函数地址是汇编程序中的原子操作,假设函数地址是本机机器字长。
  • 是与否,这取决于平台:stackoverflow.com/questions/1350994/…
  • 蒙蒂,感谢您的指点。我曾经读到:“你有从错误中学习的能力。你今天会学到很多东西”(“你”的意思是“我”)。
【解决方案2】:

callFunctions() 定期运行,例如每秒一次。然后 changeFunctions() 出现并更改 functionPtrArray[] 的内容。这可能发生在任何时间点,因为代码在类似操作系统环境的不同进程中运行。

根据您的描述,很明显functionPtrArray 数组被多个线程/进程以不同步的方式(即data race)修改和访问 4 次。 因此,您需要以一种或其他方式提供同步。

【讨论】:

    【解决方案3】:

    这与多线程场景中的任何变量的情况相同。不,除非您使用 C11 中的 _Atomic 限定符,否则它们不能被视为原子。

    在许多情况下,函数指针的读取不会是原子的。具有 16 位地址的 8 位 CPU 就是一个示例。其中一些架构具有确保安全、不间断地处理 16 位索引寄存器的指令,而其他架构则没有。另一个例子是任何支持超出默认地址总线宽度的扩展内存的架构(银行,“远指针”)。

    就代码而言,函数指针数组不得被另一个线程/进程/ISR/回调更改,否则可能会发生竞争条件错误。您必须使用 semaphore/mutex/critical section 保护对数组的访问。

    由于函数调用可能会占用一些执行时间,但您不想在执行期间阻塞所有其他线程。最好在本地复制函数指针,如以下伪代码所示:

    void callFunctions( void )
    {
      for ( i = 0; i < 3; i++ )
      {
        void(*local)(void);
    
        grab_mutex();
          local = functionPtrArray[i];
        release_mutex();
    
        local(); // call is completely thread-safe
      }
    }
    

    【讨论】:

    • 由于调用没有参数,调用将是原子的(符号mov ax, &amp;f,后跟call ax)。因此我认为这个解决方案不会添加任何东西(除非整个数组都被复制了)。
    • @PaulOgilvie 没有说明具体的系统。相信在所有系统上都能保证原子访问,可移植的,是非常幼稚的。首先,考虑具有 16 位地址的 8 位 MCU 的非常常见的情况——它们不能根据定义对函数指针进行原子访问。但可以肯定的是,假设世界上的每台计算机都是 PC,然后投票给我,因为 不知道任何其他计算机......
    • 伦丁。我可能确实有点天真。在解决方案编辑之前无法取消投票。道歉。
    猜你喜欢
    • 1970-01-01
    • 2013-11-13
    • 1970-01-01
    • 2020-06-20
    • 2010-09-29
    • 2020-07-01
    • 2011-07-03
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多