在线程之间安全地分发指针更新
safely distributing a pointer update between threads
tl;博士:
class Controller
{
public:
volatile Netconsole* nc;
void init(); //initialize the threads
void calculate(); // handler for the "mothership app"
void senderThreadLoop(); //also calls reinitNet() if connection is broken.
void listenerThreadLoop();
inline void reinitNet(){ delete nc; nc = new Netconsole(); }
}
//里面
Json::Value header = nc->Recv();
error: passing 'volatile Netconsole' as 'this' argument discards qualifiers [-fpermissive]
如果实用程序 class 被重新实例化,则指向两个线程之间共享的实用程序 class (Netconsole) 实例的指针必须在两个线程内更新,但将其声明为易失性会生成以上错误。如果它只在一个线程内更新,另一个线程可能仍然使用旧的、无效的指针。如何确保它在两者中都已更新但通过指针使用方法不会触发上述错误?
扩展信息:
我正在编写的 "smart glue logic" 库用于在第 3 方软件和自定义设备之间传递和转换消息。它由三个基本线程组成:
- 一个处理程序:第 3 方应用程序的主线程定期调用我的库中的一个 "calculate" 函数来处理新的更新 - 要发送的数据,接收到的数据
- 一个发送线程,它转换并发送处理程序推入发送缓冲区的任何内容
- 一个侦听器线程,用于转换从设备接收到的任何数据并将其推送到接收缓冲区。
发送者和侦听器线程都使用相同的实用程序 class 来处理与设备的网络通信;初始化后,class 创建到设备的连接,两个线程分别执行阻塞读取或等待新数据发送。如果出现任何问题,发送方线程会执行所有 "maintenance" 工作,而侦听器线程会进入等待 return 连接的安全状态。
现在,由于这两个线程共享一个到设备的连接,它们共享相同的通信实例 class,作为指向该 class 的指针。
问题出在重新连接的过程中 - 它涉及销毁和创建 helper class 实例,利用析构函数和构造函数中已经存在的安全关闭和初始化。结果指针改变了。如果没有 volatile
,侦听器很可能不会收到更新的指针。对于 volatile,它会抗议 - 不必要,因为 nc
(指针)不会在随机时刻改变 - 首先通知侦听器有问题,然后它进入不执行任何操作的安全状态在 'nc' 上并通知发件人它已准备就绪。只有这样,发送方才进行修复并通知监听方恢复正常运行。
那么在这种情况下正确的解决方案是什么?
您需要的是一系列操作。生产线程有 2 个相关操作:"initialize new Netconsole
" 和 "write pointer"。消费线程也有两个操作:"read pointer" 和 "use new Netconsole
object"。这 4 个操作必须按照 完全 的顺序排列,以便更新可见。
到目前为止,实现此目的的最简单方法是两个内存屏障。写屏障(指针写入上的std::memory_order_release
)防止前两个操作被重新排序,读取屏障(指针加载上的std::memory_order_acquire
)防止最后两个操作被重新排序。
由于两个线程运行 独立,您的程序正确性不应取决于特定对象更新是否发生在特定对象使用之前。更新线程可能只是有点慢,这不应该破坏您的程序。因此,写入和读取之间的第三次排序并不真正相关,您不应该尝试 "fix" 它。
总结一下:是的,4 个操作必须完全正确的顺序才能显示结果,但是如果第二个和第三个操作是
重新排序,然后更新对消费线程完全不可见。这是一个 atomic 更新,全有或全无。
还有清理旧对象的问题。生产线程不能假设消费线程已经看到指针更新。必须有同步以确保两个线程都同意旧对象未被使用。最简单的是如果生产线程在创建新对象后严格不使用旧对象(内存屏障在这里有帮助),并且消费线程一知道有新对象就清理旧对象(因为那严格发生在读取屏障之后,因此发生在写入屏障之后,进而发生在生产线程最后一次使用之后)
tl;博士:
class Controller
{
public:
volatile Netconsole* nc;
void init(); //initialize the threads
void calculate(); // handler for the "mothership app"
void senderThreadLoop(); //also calls reinitNet() if connection is broken.
void listenerThreadLoop();
inline void reinitNet(){ delete nc; nc = new Netconsole(); }
}
//里面 Json::Value header = nc->Recv();
error: passing 'volatile Netconsole' as 'this' argument discards qualifiers [-fpermissive]
如果实用程序 class 被重新实例化,则指向两个线程之间共享的实用程序 class (Netconsole) 实例的指针必须在两个线程内更新,但将其声明为易失性会生成以上错误。如果它只在一个线程内更新,另一个线程可能仍然使用旧的、无效的指针。如何确保它在两者中都已更新但通过指针使用方法不会触发上述错误?
扩展信息:
我正在编写的 "smart glue logic" 库用于在第 3 方软件和自定义设备之间传递和转换消息。它由三个基本线程组成:
- 一个处理程序:第 3 方应用程序的主线程定期调用我的库中的一个 "calculate" 函数来处理新的更新 - 要发送的数据,接收到的数据
- 一个发送线程,它转换并发送处理程序推入发送缓冲区的任何内容
- 一个侦听器线程,用于转换从设备接收到的任何数据并将其推送到接收缓冲区。
发送者和侦听器线程都使用相同的实用程序 class 来处理与设备的网络通信;初始化后,class 创建到设备的连接,两个线程分别执行阻塞读取或等待新数据发送。如果出现任何问题,发送方线程会执行所有 "maintenance" 工作,而侦听器线程会进入等待 return 连接的安全状态。
现在,由于这两个线程共享一个到设备的连接,它们共享相同的通信实例 class,作为指向该 class 的指针。
问题出在重新连接的过程中 - 它涉及销毁和创建 helper class 实例,利用析构函数和构造函数中已经存在的安全关闭和初始化。结果指针改变了。如果没有 volatile
,侦听器很可能不会收到更新的指针。对于 volatile,它会抗议 - 不必要,因为 nc
(指针)不会在随机时刻改变 - 首先通知侦听器有问题,然后它进入不执行任何操作的安全状态在 'nc' 上并通知发件人它已准备就绪。只有这样,发送方才进行修复并通知监听方恢复正常运行。
那么在这种情况下正确的解决方案是什么?
您需要的是一系列操作。生产线程有 2 个相关操作:"initialize new Netconsole
" 和 "write pointer"。消费线程也有两个操作:"read pointer" 和 "use new Netconsole
object"。这 4 个操作必须按照 完全 的顺序排列,以便更新可见。
到目前为止,实现此目的的最简单方法是两个内存屏障。写屏障(指针写入上的std::memory_order_release
)防止前两个操作被重新排序,读取屏障(指针加载上的std::memory_order_acquire
)防止最后两个操作被重新排序。
由于两个线程运行 独立,您的程序正确性不应取决于特定对象更新是否发生在特定对象使用之前。更新线程可能只是有点慢,这不应该破坏您的程序。因此,写入和读取之间的第三次排序并不真正相关,您不应该尝试 "fix" 它。
总结一下:是的,4 个操作必须完全正确的顺序才能显示结果,但是如果第二个和第三个操作是 重新排序,然后更新对消费线程完全不可见。这是一个 atomic 更新,全有或全无。
还有清理旧对象的问题。生产线程不能假设消费线程已经看到指针更新。必须有同步以确保两个线程都同意旧对象未被使用。最简单的是如果生产线程在创建新对象后严格不使用旧对象(内存屏障在这里有帮助),并且消费线程一知道有新对象就清理旧对象(因为那严格发生在读取屏障之后,因此发生在写入屏障之后,进而发生在生产线程最后一次使用之后)