【问题标题】:Updating global variables from a single worker thread: Do I need mutexes?从单个工作线程更新全局变量:我需要互斥体吗?
【发布时间】:2010-10-20 23:57:07
【问题描述】:

似乎这个问题getsaskedfrequently,但我没有得出任何明确的结论。我需要一些帮助来确定在访问/修改全局变量时是否应该(或必须!)实现锁定代码:

  • 在文件范围内定义的全局变量
  • 单个“工作”线程读取/写入全局变量
  • 从主进程线程调用返回这些全局变量的访问器函数

所以问题是,我应该用互斥锁锁定对全局变量的访问吗?

更具体地说,我正在编写一个 C++ 库,它使用网络摄像头跟踪纸页上的对象——计算机视觉是 CPU 密集型的,因此性能至关重要。我有一个 single 工作线程,它在 Open() 函数中分离出来。该线程处理所有对象跟踪。它在调用 Close() 函数时终止(间接使用全局标志)。

感觉就像我只是要求内存损坏,但我没有观察到死锁问题,也没有遇到从这些访问器函数返回的任何错误值。经过几个小时的研究,我得到的总体印象是,“嗯,可能。随便。玩那个。”如果我确实应该使用互斥锁,为什么我还没有遇到任何问题?

这是对我当前程序的过度简化:

// *********** lib.h ***********
// Structure definitions
struct Pointer
{
  int x, y;
};
// more...

// API functions
Pointer GetPointer();
void Start();
void Stop();
// more...

实现看起来像这样......

// *********** lib.cpp ***********
// Globals
Pointer p1;
bool isRunning = false;
HANDLE hWorkerThread;
// more...

// API functions
Pointer GetPointer()
{
  // NOTE: my current implementation is actually returning a pointer to the
  // global object in memory, not a copy of it, like below...

  // Return copy of pointer data
  return p1;
}

// more "getters"...

void Open()
{
  // Create worker thread -- continues until Close() is called by API user
  hWorkerThread = CreateThread(NULL, 0, DoWork, NULL, 0, NULL);
}

void Close()
{
  isRunning = false;

  // Wait for the thread to close nicely or else you WILL get nasty
  // deadlock issues on close
  WaitForSingleObject(hWorkerThread, INFINITE);
}

DWORD WINAPI DoWork(LPVOID lpParam)
{
  while (isRunning)
  {
    // do work, including updating 'p1' about 10 times per sec
  }

  return 0;
}

最后,这段代码是从外部可执行文件中调用的。像这样的东西(伪代码):

// *********** main.cpp ***********
int main()
{
  Open();

  while ( <esc not pressed> )
  {
    Pointer p = GetPointer();
    <wait 50ms or so>
  }
  Close();
}

也许我应该采取不同的方法?今天这个非问题的问题让我发疯了:-/我需要确保这个库是稳定的并返回准确的值。任何见解将不胜感激。

谢谢

【问题讨论】:

  • 也许一些基础会有所帮助:en.wikipedia.org/wiki/Readers-writer_lock
  • 感谢大家的出色回答,尤其是向我暗示读者-作家互斥锁的想法(似乎这些概念在大学里并没有得到太多的报道......)。我最终使用了 boost::shared_mutex 来锁定对在主程序循环中修改的每个全局变量的访问。 (见stackoverflow.com/questions/989795/…)。如果有人感兴趣,我可以提供我的最终解决方案的基本示例。

标签: c++ multithreading opencv mutex


【解决方案1】:

如果只有一个线程访问一个对象(读取和写入),则不需要锁。

如果一个对象是只读的,那么就不需要锁。 (假设您可以保证在构造过程中只有一个线程访问该对象)。

如果任何线程写入(更改状态)对象。如果有其他线程访问该对象,则必须锁定 ALL 访问(读取和写入)。尽管您可以使用允许多个读者的读锁。但是写操作必须是独占的,并且在状态改变时没有读者可以访问对象。

【讨论】:

  • +1...除非,当然,您可以在没有锁的情况下自动更新状态;-)
【解决方案2】:

我想这取决于您在 DoWork() 函数中所做的事情。假设它将一个点值写入 p1。至少您有以下竞争条件的可能性,它将向主线程返回无效结果:

假设工作线程想要更​​新 p1 的值。例如,让我们将 p1 的值从 (A, B) 更改为 (C, D)。这将涉及至少两个操作,将 C 存储在 x 中并将 D 存储在 y 中。如果主线程决定在 GetPointer() 函数中读取 p1 的值,它还必须执行至少两个操作,加载 x 的值和加载 y 的值。如果操作顺序是:

  1. 更新线程:存储 C
  2. 主线程:加载x(主线程接收C)
  3. 主线程:加载y(主线程接收B)
  4. 更新线程:商店 D

主线程会得到点(C,B),这是不正确的。

这个特殊的问题不是很好地使用线程,因为主线程没有做任何实际的工作。我会使用单线程和像 WaitForMultipleObjectsEx 这样的 API,它允许您同时等待来自键盘标准输入句柄的输入、来自相机的 I/O 事件和超时值。

【讨论】:

  • 感谢您说明这个概念。虽然很难验证这是否发生在我的程序中,但我能够在测试程序中创建这个无效条件(使用线程睡眠命令来指示长时间操作)。如果我的xy 值在一两次迭代中像这样分散,对最终用户来说可能并不重要,但我不能放手! :-) 最终,我使用了 Boost 库的 shared_mutex 和作者优先锁……我所要做的就是重组整个程序,呃!
【解决方案3】:

您不会遇到死锁,但您可能会偶尔看到一些极低概率的错误值:由于读取和写入需要不到一纳秒的时间,而您每秒只读取变量 50 次,因此碰撞大约是五千万分之一。

如果这种情况发生在 Intel 64 上,“指针”与 8 字节边界对齐,并且在一个操作中读取和写入(所有 8 个字节都带有一条汇编指令),那么访问是原子的,而你不需要'不需要互斥体。

如果其中任何一个条件不满足,阅读器就有可能得到错误数据。

为了安全起见,我设置了一个互斥体,因为它每秒只会使用 50 次,而且不会成为性能问题。

【讨论】:

  • 您的一些假设是有问题的。他的代码似乎不太可能以原子方式更新 x 和 y 成员,尽管我认为如果 CPU 支持 64 位原子存储操作,编译器可以这样优化它。另一个问题是,如果没有内存屏障,就不能保证主线程必须在主线程中读取内存,也不能保证更新线程实际上必须写入内存。如果他可以利用他的 CPU/OS 提供的原子操作,那么他就不需要互斥体了。
【解决方案4】:

情况非常明确——读者可能看不到更新,直到触发同步(互斥锁、内存屏障、原子操作......)。许多事情进程确实会隐式触发这种同步 - 例如外部函数调用(原因解释了 Usenet 线程常见问题解答(http://www.lambdacs.com/cpt/FAQ.html) - 请参阅 Dave Butenhof 对 volatile 的重新需要的回答,所以如果您的代码处理的值很小足以使它们不能写一半(例如数字而不是字符串,固定地址而不是动态(重新)分配),那么它可以在没有显式同步的情况下跛行。

如果您对性能的想法是通过编写代码获得更多循环,那么如果您省略同步,您会得到一个更好的数字。但是,如果您对最小化平均延迟和最坏情况延迟以及读取器实际看到多少不同的更新感兴趣,那么您应该从写入器进行同步。

【讨论】:

  • 感谢您的意见。你的评论把我推到了边缘,并说服我在我的变量上实现 MultiReader-SingleWriter 锁。
【解决方案5】:

由于 Pointer 中信息的性质,您可能看不到问题。如果它正在跟踪某个移动速度不是很快的对象的坐标,并且在读取过程中更新了位置,那么坐标可能会“有点偏离”,但不足以引起注意。

例如,假设更新后 p.x 为 100,p.y 为 100。您正在跟踪的对象移动了一点,因此在下一次更新后,p.x 为 102,p.y 为 102。如果您碰巧读入本次更新的中间,x更新后y更新前,你会得到一个指针值p.x为102,p.y为100。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-09-02
    • 1970-01-01
    • 2011-01-20
    相关资源
    最近更新 更多