C - 可以通过不同的线程修改全局指针吗?

时间:2013-09-03 18:22:13

标签: c multithreading pointers

全局指针在线程之间是否存在范围?

例如,假设我有两个文件,file1.c和file2.c:

file1.c中:

uint64_t *g_ptr = NULL;

modify_ptr(&g_ptr) { 
    //code to modify g_ptr to point to a valid address 
}

read_from_addr() {
    //code which uses g_ptr to read values from the memory it's pointing to
}

file2.c中:

function2A() {
    read_from_addr();
}

所以我有threadA,它运行file1.c并执行modify_ptr(& g_ptr)和read_from_addr()。然后threadB运行,它通过执行function2A()的file2.c运行。

我的问题是:threadB是否看到g_ptr被修改了?或者它仍然看到它指向NULL?

如果不是这样,那么指针是全局的意味着什么?我如何确保不同线程之间可以访问此指针?

如果我需要澄清任何事情,请告诉我。感谢

4 个答案:

答案 0 :(得分:7)

  

我的问题是:threadB是否看到g_ptr被修改了?还是它仍然看到它指向NULL?

也许。如果在没有任何外部同步的情况下访问,您可能会看到奇怪的,高度不可重现的结果 - 在某些情况下,编译器可能会根据对代码的分析进行某些优化,这可能源于假设变量是在某些代码路径中未修改。例如,请考虑以下代码:

// Global variable
int global = 0;

// Thread 1 runs this code:
while (global == 0)
{
    // Do nothing
}

// Thread 2 at some point does this:
global = 1;

在这种情况下,编译器可以看到global循环中没有修改while,并且它不调用任何外部函数,所以它可以将它“优化”成这样的东西:

if (global == 0)
{
    while (1)
    {
        // Do nothing
    }
}

volatile关键字添加到变量的声明中会阻止编译器进行此优化,但是当C语言标准化时,这不是volatile的预期用例。在这里添加volatile只会以较小的方式减慢程序速度并掩盖真正的问题 - 缺乏正确的同步。

管理需要从多个线程同时访问的全局变量的正确方法是使用互斥锁来保护它们 1 。例如,这是使用POSIX线程mutex:{/ p>的modify_ptr的简单实现

uint64_t *g_ptr = NULL;
pthread_mutex_t g_ptr_mutex = PTHREAD_MUTEX_INITIALIZER;

void modify_ptr(uint64_t **ptr, pthread_mutex_t *mutex)
{
    // Lock the mutex, assign the pointer to a new value, then unlock the mutex
    pthread_mutex_lock(mutex);
    *ptr = ...;
    pthread_mutex_unlock(mutex);
}

void read_from_addr()
{
    modify_ptr(&g_ptr, &g_ptr_mutex);
}

互斥函数确保插入适当的内存屏障,因此对由互斥锁保护的变量所做的任何更改都将正确传播到其他CPU内核,前提是变量的每次访问(包括读取!)都受到保护。互斥。

1)您也可以使用专门的无锁数据结构,但这些是一种先进的技术,很容易出错

答案 1 :(得分:4)

这个问题是什么使并发编程变得困难的教科书示例。一个非常彻底的解释could fill an entire book,以及不同质量的lots of articles

但我们可以总结一下。全局变量位于所有线程可见的内存空间中。 (替代方案是thread-local storage,只有一个线程可以看到。)因此,如果你有一个全局变量 G ,并且线程 A 写入值,你会期望 x ,然后线程 B 将在稍后读取该变量时看到 x 。总的来说,这是事实 - 最终。有趣的部分是“最终”之前发生的事情。

棘手的最大来源是内存一致性 memory coherence

Coherence描述了当线程 A 写入 G 并且线程 B 几乎在同一时刻尝试读取它时会发生什么。想象一下,线程 A B 位于不同的处理器上(为简单起见,我们也称它们为A和B)。当 A 写入变量时,它与线程 B 看到的内存之间有很多电路。首先, A 可能会写入its own data cache。它将在writing it back to main memory之前存储该值一段时间。将高速缓存刷新到主存储器也需要时间:存在number of signals that have to go back and forth on wires and capacitors and transistors,以及高速缓存和主存储器单元之间的复杂对话。同时, B 有自己的缓存。当主内存发生更改时, B 可能无法立即看到它们 - 至少,直到它从该行重新填充其缓存。等等。总而言之,在 B 可以看到线程 A 的更改之前,可能需要很多微秒。

一致性描述当 A 写入变量 G 然后变量 H 时会发生什么。如果它读回那些变量,它将看到按顺序发生的写入。但是线程 B 可能会以不同的顺序看到它们,具体取决于 H 是否首先从缓存刷回主RAM。如果 A B 同时写入 G (通过挂钟)会发生什么,然后尝试从它?他们会看到哪个值?

通过memory barrier操作在许多处理器上实施一致性和一致性。例如,PowerPC具有sync操作码,该操作码表示“保证任何线程对主存储器所做的任何写操作都将在此 sync 操作之后的任何读取中可见。 “ (基本上它是通过针对主RAM重新检查每个缓存行来实现的。)英特尔架构does this automatically to some extent如果你提前警告“此操作触及同步内存”。

然后你有编译器重新排序的问题。这是代码

的地方
int foo( int *e, int *f, int *g, int *h) 
{
   *e = *g;
   *f = *h;
   // <-- another thread could theoretically write to g and h here
   return *g + *h ;
}

可以由编译器在内部转换为更像

的内容
int bar( int *e, int *f, int *g, int *h) 
{
  int b = *h;
  int a = *g;
  *f = b ;
  int result = a + b;
  *e = a ;
  return result;
}
如果另一个线程在上面给出的位置执行了写操作,那么

会给你一个完全不同的结果!另外,请注意写入在bar中以不同顺序发生的方式。这是 volatile 应该解决的问题 - 它阻止编译器将*g的值存储在本地,而是强制它每次从内存重新加载该值看到*g

正如您所看到的,这不足以强制执行许多处理器的内存一致性和一致性。它实际上是针对一个处理器尝试从内存映射硬件读取的情况发明的 - 比如串行端口,您希望每隔 n 微秒查看内存中的某个位置什么价值目前在电线上。 (这就是I / O在他们发明C时的工作方式。)

该怎么办?好吧,就像我说的那样,有关于这个主题的全书。但简短的回答是,您可能希望使用操作系统/运行时平台为同步内存提供的功能。

例如,Windows提供了interlocked memory access API,为您提供了一种在线程 A B 之间进行内存通信的清晰方法。 GCC tries to expose some similar functionsIntel's threading building blocks为x86 / x64平台提供了一个很好的界面,the C++11 thread support library也提供了一些设施。

答案 2 :(得分:0)

  

我的问题是:threadB是否看到g_ptr被修改了?

可能。线程B通过g_ptr访问read_from_addr(),因此始终可以看到相同的g_ptr。这与g_ptr的“模块内全局性”无关:如果g_ptr被声明为static并且具有内部链接,那么它也可以正常工作,因为正如您在此处所写的那样,它出现在read_from_addr()之前的文件范围内。

  

或者它仍然看到它指向NULL?

可能不是。完成任务后,所有线程都可以看到它。

这里的问题是,如果你有两个线程访问共享数据,其中至少有一个线程正在写入它(这是这里的情况),你需要同步对它的访问,因为普通的内存读写不是原子的。例如,在POSIX中,在这些情况下的行为是正式的“未定义”,这基本上意味着所有的赌注都已关闭,并且您的机器可能会变得流氓,并且就标准而言会吃掉你的猫。

因此,您确实希望使用适当的线程同步原语(例如读/写锁或互斥锁)来确保程序良好。在使用pthreads的Linux上,您需要查看pthread_rwlock_*pthread_mutex_*。我知道其他平台都有等价物,但我不知道它们是什么。

答案 3 :(得分:-1)

全局变量可供所有线程使用。

对于Ex:

struct yalagur
{
    char name [200];
    int rollno;
    struct yalagur * next;
}头;

int main()
{
   线程1();
   线程2();
   thread3();
}

现在上面的结构在所有线程之间共享。

任何线程都可以直接访问该结构。

所以这称为线程之间的共享内存。

你需要使用互斥/共享变量/ etc概念来更新/读取/删除共享内存。

由于 佐