5

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

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

文件1.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
}

文件2.c:

function2A() {
    read_from_addr();
}

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

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

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

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

4

4 回答 4

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关键字添加到变量的声明中会阻止编译器进行此优化,但这不是volatileC 语言标准化时的预期用例。在此处添加volatile只会稍微减慢您的程序并掩盖真正的问题 - 缺乏适当的同步。

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

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)您也可以使用专门的无锁数据结构,但这些是一种高级技术,很容易出错

于 2013-09-03T19:13:56.967 回答
4

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

但我们可以稍微总结一下。全局变量位于所有线程可见的内存空间中。(另一种选择是线程本地存储,只有一个线程可以看到。)所以你会期望如果你有一个全局变量G,并且线程A将值x写入它,那么线程B将在读取该变量时看到x稍后的。总的来说,这是真的——最终。有趣的部分是“最终”之前发生的事情。

棘手的最大来源是内存一致性内存连贯性

连贯性描述了当线程A写入G并且线程B几乎同时尝试读取它时会发生什么。假设线程AB位于不同的处理器上(为简单起见,我们也称它们为 A 和 B)。当A写入一个变量时,它和线程B看到的内存之间有很多电路。首先,A可能会写入自己的数据缓存。它会将该值存储一段时间,然后再将其写回主存储器。将缓存刷新到主内存也需要时间:有一个必须在电线、电容器和晶体管上来回传输的信号数量,以及高速缓存和主存储器单元之间的复杂对话。同时,B有自己的缓存。当主存发生变化时,B可能不会立即看到它们——至少在它从该行重新填充其缓存之前是这样。等等。总而言之,在线程A的更改对B可见之前可能需要很多微秒。

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

在许多具有内存屏障操作的处理器上强制执行一致性和一致性。例如,PowerPC 有一个同步操作码,上面写着“保证任何线程对主内存进行的任何写入,在此同步操作之后对任何读取都是可见的”。(基本上,它通过针对主 RAM 重新检查每个缓存行来做到这一点。)如果您提前警告英特尔架构“此操作触及同步内存” ,英特尔架构会在某种程度上自动执行此操作。

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

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微秒查看一次内存中的一个位置,以查看当前在线路上的值. (这就是他们发明 C 时 I/O 的实际工作方式。)

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

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

于 2013-09-03T19:48:16.803 回答
0

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

大概。g_ptr由 threadB via 访问read_from_addr(),所以g_ptr一直都可以看到。这与 的“模块内全局性”无关g_ptr:如果g_ptr被声明static并具有内部链接,它也可以正常工作,因为正如您在此处编写的那样,它之前出现在文件范围内read_from_addr()

还是它仍然看到它指向 NULL?

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

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

所以你真的会想要使用适当的线程同步原语(例如读/写锁或互斥锁)来确保程序表现良好。在具有 pthread 的 Linux 上,您需要查看pthread_rwlock_*pthread_mutex_*. 我知道其他平台也有等价物,但我不知道它们是什么。

于 2013-09-03T19:32:24.940 回答
-1

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

例如:

结构 yalagur
{
字符名称 [200];
诠释rollno;
结构 yalagur *next;
}头;

int main()
{
thread1();
线程2();
线程3();
}

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

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

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

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

谢谢萨达

于 2014-01-07T06:57:18.887 回答