关于多线程:C-全局指针可以由不同的线程修改吗?

C - Can global pointers be modified by different threads?

全局指针在线程之间是否存在作用域?

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

file1.c:

1
2
3
4
5
6
7
8
9
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:

1
2
3
function2A() {
    read_from_addr();
}

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

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

如果不是这种情况,那么指针成为全局指针意味着什么? 以及如何确保在不同线程之间可以访问此指针?

请让我知道是否需要澄清任何事情。 谢谢


My question is: Does threadB see that g_ptr is modified? Or does it still see that it's pointing to NULL?

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

1
2
3
4
5
6
7
8
9
10
11
// Global variable
int global = 0;

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

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

在这种情况下,编译器可以看到在while循环内未修改global,并且它没有调用任何外部函数,因此可以将其"优化"为以下形式:

1
2
3
4
5
6
7
if (global == 0)
{
    while (1)
    {
        // Do nothing
    }
}

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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)您也可以使用专门的无锁数据结构,但这是一种高级技术,很容易出错


这个问题是导致并发编程困难的教科书示例。一个真正彻底的解释可以填满整本书,以及许多不同质量的文章。

好。

但是我们可以总结一下。全局变量位于所有线程可见的内存空间中。 (另一种选择是线程本地存储,只有一个线程可以看到。)因此,您可以期望,如果您有一个全局变量G,并且线程A向其写入值x,则线程B在读取该变量时将看到x。稍后的。总的来说,这是正确的-最终。有趣的部分是"最终"之前发生的事情。

好。

棘手的最大原因是内存一致性和内存一致性。

好。

一致性描述了当线程A写入G而线程B几乎同时尝试读取它时发生的情况。想象一下,线程A和B在不同的处理器上(为简单起见,我们也称它们为A和B)。当A写入变量时,它与线程B看到的内存之间存在许多电路。首先,A可能会写入其自己的数据缓存。它将存储该值一段时间,然后再将其写回主存储器。将高速缓存刷新到主存储器也需要时间:在导线,电容器和晶体管上必须来回传递许多信号,并且高速缓存与主存储器单元之间的对话很复杂。同时,B具有自己的缓存。当主存储器发生更改时,B可能至少不会立即看到它们,直到从该行重新填充其缓存为止。等等。总而言之,线程A的更改对于B来说可能要花几微秒的时间。

好。

一致性描述了当A先对变量G进行写入,然后对变量H进行写入时发生的情况。如果它先回读这些变量,它将看到写入按该顺序进行。但是线程B可能以不同的顺序查看它们,具体取决于H是否先从高速缓存刷新回主RAM。如果A和B同时(通过挂钟)同时写入G,然后尝试从中回读,会发生什么情况呢?他们会看到什么价值?

好。

在具有内存屏障操作的许多处理器上都必须实现一致性和一致性。例如,PowerPC有一个同步操作码,它说"保证在同步操作之后,任何线程都可以看到任何线程对主存所做的任何写操作。" (基本上,它是通过重新检查主RAM中的每个缓存行来完成此操作的。)如果您提前警告"此操作涉及同步内存",则英特尔架构会在某种程度上自动执行此操作。

好。

然后,您会遇到编译器重新排序的问题。这是代码

好。

1
2
3
4
5
6
7
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 ;
}

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

好。

1
2
3
4
5
6
7
8
9
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微秒查看一次内存中的位置,以查看当前线路上的值。 (这就是他们发明C时I / O的工作方式。)

好。

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

好。

例如,Windows提供了互锁的内存访问API,从而为您提供了在线程A和B之间进行内存通信的清晰方法。GCC尝试公开一些类似的功能。英特尔的线程构建块为x86 / x64平台提供了一个不错的接口,C ++ 11线程支持库也提供了一些功能。

好。

好。


My question is: Does threadB see that g_ptr is modified?

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

Or does it still see that it's pointing to NULL?

可能不会。分配完成后,所有线程均可见。

这里的问题是,如果有两个线程正在访问共享数据,而其中至少有一个线程正在写入共享数据(此处就是这种情况),则您需要同步对其进行访问,因为普通内存的读写不是原子的。例如,在POSIX中,这种情况下的行为在形式上是"未定义的",这基本上意味着所有赌注都已关闭,并且您的机器可以进行恶意攻击,并且就标准而言可以吃掉猫。

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


全局变量可用于所有线程。
例如:

结构雅拉古尔
{
字符名称[200];
int rollno;
struct yalagur *下一步;
}头;

int main()
{
thread1();
thread2();
thread3();
}

现在,以上结构在所有线程之间共享。

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

因此,这称为线程之间的共享内存。

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

谢谢
佐田