pthread_create(3)和SMP体系结构中的内存同步保证

时间:2014-03-12 10:00:06

标签: c pthreads posix cpu-cache memory-model

我正在查看The Open Group Base Specifications Issue 7 (IEEE Std 1003.1, 2013 Edition), section 4.11文档的第4.11节,其中列出了内存同步规则。这是POSIX标准中最具体的,我已经设法详细介绍了POSIX / C内存模型。

这是一个引用

  

4.11内存同步

     

应用程序应确保更多地访问任何内存位置   比一个控制线程(线程或进程)受到限制   没有控制线程可以读取或修改内存位置   另一个控制线程可能正在修改它。这种访问是   限制使用同步线程执行的函数   使内存与其他线程同步。下列   函数使内存与其他线程同步:

     

fork()pthread_barrier_wait()pthread_cond_broadcast()   pthread_cond_signal()pthread_cond_timedwait()pthread_cond_wait()   pthread_create()pthread_join()pthread_mutex_lock()   pthread_mutex_timedlock()

     

pthread_mutex_trylock()pthread_mutex_unlock()pthread_spin_lock()   pthread_spin_trylock()pthread_spin_unlock()pthread_rwlock_rdlock()   pthread_rwlock_timedrdlock()pthread_rwlock_timedwrlock()   pthread_rwlock_tryrdlock()pthread_rwlock_trywrlock()

     

pthread_rwlock_unlock()pthread_rwlock_wrlock()sem_post()   sem_timedwait()sem_trywait()sem_wait()semctl()semop()wait()   waitpid函数()

(省略了要求的例外情况。)

基本上,解释上面的文档,规则是当应用程序读取或修改内存位置而另一个线程或进程可能修改它时,它们应该确保同步线程执行和内存关于其他线程通过调用列出的函数之一。其中,提到pthread_create(3)来提供内存同步。

我理解这基本上意味着每个函数都需要隐含某种memory barrier(虽然标准似乎没有使用这个概念)。因此,例如从pthread_create()返回,我们保证在调用之前由该线程进行的内存修改在它们同步内存之后出现在其他线程(可能运行不同的CPU /内核)上。但是新创建的线程怎么样 - 在线程开始运行线程函数之前是否存在隐含的内存障碍,以便它可靠地看到由pthread_create()同步的内存修改?这是由标准规定的吗?或者我们应该明确提供内存同步,以便能够信任我们根据POSIX标准读取的任何数据的正确性吗?

特殊情况(作为特殊情况回答上述问题):上下文切换是否提供内存同步,即,当启动或恢复进程或线程的执行时,内存是否与任何内容同步其他执行线程的内存同步?

示例:

线程#1创建从堆分配的常量对象。线程#1创建一个新线程#2,用于从对象中读取数据。如果我们可以假设新线程#2以内存同步开始,那么一切都很好。但是,如果运行新线程的CPU核心具有先前分配的副本,但由于其缓存内存中的丢弃数据而不是新值,则可能有错误的状态视图,并且应用程序可能无法正常运行。

更具体地......

  1. 以前在程序中(这是CPU#1缓存中的值)

     int i = 0;        
    
  2. CPU#0 中运行的线程T0

     pthread_mutex_lock(...);
     int tmp = i;
     pthread_mutex_unlock(...);
    
  3. CPU#1 中运行的帖子T1

     i = 42;
     pthread_create(...);
    
  4. CPU#0 中运行的新创建的线程T2

     printf("i=%d\n", i);    /* First step in the thread function */
    
  5. 没有内存屏障,没有同步线程T2内存,可能会发生输出

         i=0
    

    (以前缓存的,未同步的值)。

    更新 如果允许这种实现疯狂,许多使用POSIX线程库的应用程序将不是线程安全的。

2 个答案:

答案 0 :(得分:2)

  

在线程开始运行线程函数之前是否存在隐含的内存障碍   通过pthread_create()可靠地看到内存修改?

是。否则就没有必要将pthread_create用作内存同步(屏障)。

(这是afaik。没有明确说明posix,(nor does posix define a standard memory model), 因此,您必须决定是否相信您的实现是否可以做到唯一合理的事情 - 确保在新线程运行之前进行同步 - 我不会特别担心它。)

  

特殊情况(作为特殊情况回答上述问题):上下文切换是否提供内存同步,即,当启动或恢复进程或线程的执行时,内存是否与任何内容同步其他执行线程的内存同步?

不,上下文切换不会成为障碍。

  

线程#1创建从堆分配的常量对象。线程#1创建一个新线程#2,从对象中读取数据。如果我们可以假设新线程#2以内存同步开始,那么一切都很好。但是,如果运行新线程的CPU核心具有先前分配的副本,但由于其缓存内存中的丢弃数据而不是新值,则可能有错误的状态视图,并且应用程序可能无法正常运行。

由于pthread_create必须执行内存同步,因此不会发生这种情况。驻留在另一个核心上的cpu缓存中的任何旧内存都必须无效。 (幸运的是,常用的平台是缓存一致的,因此硬件会处理这个问题。)

现在,如果您在之后更改了对象,那么您已创建了2.线程,则需要再次进行内存同步,以便所有各方都能看到更改,否则可以避免竞争条件。 pthread互斥体通常用于实现这一点。

答案 1 :(得分:1)

从架构设计的角度来看,高速缓存一致的体系结构保证即使是分离的CPU(ccNUMA - 缓存一致的非统一内存体系结构),在访问内存位置时使用独立的内存通道也不会产生您在示例中描述的不一致性

这会发生重要的惩罚,但应用程序将正常运行。

线程#1在CPU0上运行,并将对象内存保存在缓存L1中。当CPU1上的线程#2读取相同的内存地址时(或更确切地说:相同的缓存行 - 查找 false sharing 以获取更多信息),它会在加载之前强制CPU0 上的缓存未命中缓存行