转载请说明出处:http://blog.csdn.net/cywosp/article/details/26469435

   在Linux系统中使用C/C++进行多线程编程时,我们遇到最多的就是对同一变量的多线程读写问题。大多情况下遇到这类问题都是通过锁机制来处理,但这对程序的性能带来了非常大的影响,当然对于那些系统原生支持原子操作的数据类型来说,我们能够使用原子操作来处理,这能对程序的性能会得到一定的提高。那么对于那些系统不支持原子操作的自己定义数据类型,在不使用锁的情况下怎样做到线程安全呢?本文将从线程局部存储方面。简单解说处理这一类线程安全问题的方法。
一、数据类型
    在C/C++程序中常存在全局变量、函数内定义的静态变量以及局部变量。对于局部变量来说。其不存在线程安全问题,因此不在本文讨论的范围之内。全局变量和函数内定义的静态变量,是同一进程中各个线程都能够訪问的共享变量,因此它们存在多线程读写问题。

在一个线程中改动了变量中的内容,其它线程都能感知而且能读取已更改过的内容,这对数据交换来说是非常快捷的。可是因为多线程的存在,对于同一个变量可能存在两个或两个以上的线程同一时候改动变量所在的内存内容,同一时候又存在多个线程在变量在改动的时去读取该内存值,假设没有使用相应的同步机制来保护该内存的话,那么所读取到的数据将是不可预知的,甚至可能导致程序崩溃。

    假设须要在一个线程内部的各个函数调用都能訪问、但其它线程不能訪问的变量。这就须要新的机制来实现,我们称之为Static memory local to a thread (线程局部静态变量),同一时候也可称之为线程特有数据(TSD: Thread-Specific
Data)或者线程局部存储(TLS: Thread-Local Storage)。这一类型的数据,在程序中每一个线程都会分别维护一份变量的副本(copy)。而且长期存在于该线程中。对此类变量的操作不影响其它线程。

例如以下图:

                                   

watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvY3l3b3Nw/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" alt="" />

二、一次性初始化
   在解说线程特有数据之前,先让我们来了解一下一次性初始化。

多线程程序有时有这样的需求:不管创建多少个线程,有些数据的初始化仅仅能发生一次。列如:在C++程序中某个类在整个进程的生命周期内仅仅能存在一个实例对象。在多线程的情况下,为了能让该对象能够安全的初始化。一次性初始化机制就显得尤为重要了。——在设计模式中这样的实现经常被称之为单例模式(Singleton)。

Linux中提供了例如以下函数来实现一次性初始化:

#include <pthread.h>
// Returns 0 on success, or a positive error number on error
int pthread_once (pthread_once_t *once_control, void (*init)
(void));
利用參数once_control的状态,函数pthread_once()能够确保不管有多少个线程调用多少次该函数,也仅仅会运行一次由init所指向的由调用者定义的函数。init所指向的函数没有不论什么參数,形式例如以下:
void init (void)
{
   // some variables initializtion in here
}
另外,參数once_control必须是pthread_once_t类型变量的指针,指向初始化为PTHRAD_ONCE_INIT的静态变量。

在C++0x以后提供了相似功能的函数std::call_once ()。使用方法与该函数相似。

使用实例请參考https://github.com/ApusApp/Swift/blob/master/swift/base/singleton.hpp实现。

三、线程局部数据API
    在Linux中提供了例如以下函数来对线程局部数据进行操作
#include <pthread.h>
// Returns 0 on success, or a positive error number on error
int pthread_key_create (pthread_key_t *key, void (*destructor)(void *));
// Returns 0 on success, or a positive error number on error
int pthread_key_delete (pthread_key_t key);
// Returns 0 on success, or a positive error number on error
int pthread_setspecific (pthread_key_t key, const
void *value);
// Returns pointer, or NULL if no thread-specific data is associated with key
void *pthread_getspecific (pthread_key_t key);
函数pthread_key_create()为线程局部数据创建一个新键,并通过key指向新创建的键缓冲区。

因为全部线程都能够使用返回的新键,所以參数key能够是一个全局变量(在C++多线程编程中一般不使用全局变量。而是使用单独的类对线程局部数据进行封装,每一个变量使用一个独立的pthread_key_t)。destructor所指向的是一个自己定义的函数,其格式例如以下:

void Dest (void *value)
{
    // Release storage pointed to by 'value'
}
仅仅要线程终止时与key关联的值不为NULL,则destructor所指的函数将会自己主动被调用。假设一个线程中有多个线程局部存储变量。那么对各个变量所相应的destructor函数的调用顺序是不确定的。因此。每一个变量的destructor函数的设计应该相互独立。

函数pthread_key_delete()并不检查当前是否有线程正在使用该线程局部数据变量。也不会调用清理函数destructor,而仅仅是将其释放以供下一次调用pthread_key_create()使用。在Linux线程中。它还会将与之相关的线程数据项设置为NULL。

因为系统对每一个进程中pthread_key_t类型的个数是有限制的,所以进程中并不能创建无限个的pthread_key_t变量。Linux中能够通过PTHREAD_KEY_MAX(定义于limits.h文件里)或者系统调用sysconf(_SC_THREAD_KEYS_MAX)来确定当前系统最多支持多少个键。Linux中默认是1024个键。这对于大多数程序来说已经足够了。假设一个线程中有多个线程局部存储变量,通常能够将这些变量封装到一个数据结构中。然后使封装后的数据结构与一个线程局部变量相关联,这样就能降低对键值的使用。

函数pthread_setspecific()用于将value的副本存储于一数据结构中。并将其与调用线程以及key相关联。

參数value通常指向由调用者分配的一块内存,当线程终止时,会将该指针作为參数传递给与key相关联的destructor函数。当线程被创建时,会将全部的线程局部存储变量初始化为NULL,因此第一次使用此类变量前必须先调用pthread_getspecific()函数来确认是否已经于相应的key相关联,假设没有。那么pthread_getspecific()会分配一块内存并通过pthread_setspecific()函数保存指向该内存块的指针。

參数value的值也能够不是一个指向调用者分配的内存区域。而是不论什么能够强制转换为void*的变量值。在这样的情况下。先前的pthread_key_create()函数应将參数

destructor设置为NULL

函数pthread_getspecific()正好与pthread_setspecific()相反,其是将pthread_setspecific()设置的value取出。

在使用取出的值前最好是将void*转换成原始数据类型的指针。

四、深入理解线程局部存储机制
    1. 深入理解线程局部存储的实现有助于对其API的使用。在典型的实现中包括下面数组:
  • 一个全局(进程级别)的数组,用于存放线程局部存储的键值信息
pthread_key_create()返回的pthread_key_t类型值仅仅是对全局数组的索引,该全局数组标记为pthread_keys。其格式大概例如以下:
                          

数组的每一个元素都是一个包括两个字段的结构,第一个字段标记该数组元素是否在用,第二个字段用于存放针对此键、线程局部存储变的解构函数的一个副本,即destructor函数。

  • 每一个线程还包括一个数组,存有为每一个线程分配的线程特有数据块的指针(通过调用pthread_setspecific()函数来存储的指针,即參数中的value)

   2. 在常见的存储pthread_setspecific()函数參数value的实现中。大多数都相似于下图的实现。图中假设pthread_keys[1]分配给func1()函数,pthread API为每一个函数维护指向线程局部存储数据块的一个指针数组。当中每一个数组元素都与图线程局部数据键的实现(上图)中的全局pthread_keys中元素一一相应。

                     

watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvY3l3b3Nw/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" alt="" />

五、总结
    使用全局变量或者静态变量是导致多线程编程中非线程安全的常见原因。在多线程程序中,保障非线程安全的经常使用手段之中的一个是使用相互排斥锁来做保护。这样的方法带来了并发性能下降。同一时候也仅仅能有一个线程对数据进行读写。假设程序中能避免使用全局变量或静态变量,那么这些程序就是线程安全的,性能也能够得到非常大的提升。假设有些数据仅仅能有一个线程能够訪问。那么这一类数据就能够使用线程局部存储机制来处理,尽管使用这样的机制会给程序运行效率上带来一定的影响,但对于使用锁机制来说。这些性能影响将能够忽略。

Linux
C++的线程局部存储简单实现可參考https://github.com/ApusApp/Swift/blob/master/swift/base/threadlocal.h,更具体且高效的实现可參考Facebook的folly库中的ThreadLocal实现。更高性能的线程局部存储机制就是使用__thread,这将在下一节中讨论。

參考:
[1] Linux/UNIX系统编程手冊(上)
[2] http://www.groad.net/bbs/thread-2182-1-1.html
[3] http://baike.baidu.com/view/598128.htm

每天进步一点点——Linux中的线程局部存储(一)的更多相关文章

  1. 每天进步一点点——Linux中的线程局部存储(二)

    转载请说明出处:http://blog.csdn.net/cywosp/article/details/26876231     在Linux中另一种更为高效的线程局部存储方法,就是使用keyword ...

  2. linux中的线程局部存储(TLS)

    http://blog.csdn.net/cywosp/article/details/26469435

  3. 在Linux中使用线程

    我并不假定你会使用Linux的线程,所以在这里就简单的介绍一下.如果你之前有过多线程方面的编程经验,完全可以忽略本文的内容,因为它非常的初级. 首先说明一下,在Linux编写多线程程序需要包含头文件p ...

  4. Linux中-POSIX 线程详解

    一种支持内存共享的简捷工具   摘自https://www.ibm.com/developerworks/cn/linux/thread/posix_thread1/ 线程是有趣的 了解如何正确运用线 ...

  5. Linux中epoll+线程池实现高并发

    服务器并发模型通常可分为单线程和多线程模型,这里的线程通常是指“I/O线程”,即负责I/O操作,协调分配任务的“管理线程”,而实际的请求和任务通常交由所谓“工作者线程”处理.通常多线程模型下,每个线程 ...

  6. 每天进步一点点——Linux中的文件描写叙述符与打开文件之间的关系

    转载请说明出处:http://blog.csdn.net/cywosp/article/details/38965239 1. 概述     在Linux系统中一切皆能够看成是文件,文件又可分为:普通 ...

  7. linux中的线程同步:生产者、消费者问题

    #include <stdio.h> #include <semaphore.h> #include <unistd.h> #include <stdlib. ...

  8. Linux查看进程线程个数

    1.根据进程号进行查询: # pstree -p 进程号 # top -Hp 进程号 2.根据进程名字进行查询: # pstree -p `ps -e | grep server | awk '{pr ...

  9. Linux中的task,process, thread 简介

    本文的主要目的是介绍在Linux内核中,task,process, thread这3个名字之间的区别和联系.并且和WINDOWS中的相应观念进行比较.如果你已经很清楚了,那么就不用往下看了. LINU ...

随机推荐

  1. MapReduce: number of mappers/reducers

    14 down vote It's the other way round. Number of mappers is decided based on the number of splits. I ...

  2. SQL: 拼接列

    1. 因工作需要,需把两列(id,created_by)拼接成一列,结果很有意思,前5个值都是null. 2.解决方法:null加减乘除任何值都等于null,所以使用isnull函数先处理下列的值再拼 ...

  3. IDEA,与gradle引入jar包报错

    Warning:<i><b>root project 'netty_lecture': Unable to resolve additional project configu ...

  4. 初学libcurl

    最近想把某网站上的些数据download下来,在网上找到了cURL,正好来边学边弄一下! 下载libcurl源代码 cURL的官网是http://curl.haxx.se/ ,直接下载源代码包来编译吧 ...

  5. Duilib 创建不规则窗口(转载)

    方法一: 转载:http://blog.csdn.net/chenlycly/article/details/46447297 转载:http://blog.csdn.net/harvic880925 ...

  6. Tomcat 发布项目 conf/Catalina/localhost 配置 及数据源配置

    本文介绍通过在tomcat的conf/Catalina/localhost目录下添加配置文件,来发布项目.因为这样对 tomcat 的入侵性最小,只需要新增一个配置文件,不需要修改原有配置:而且支持动 ...

  7. Ubuntu Budgie 18.04 是最好的Remix【转】

    本文转载子:https://www.linuxidc.com/Linux/2018-05/152223.htm [日期:2018-05-05] 来源:Linux公社  作者:醉落红尘 [字体:大 中  ...

  8. hdu 6430 线段树 暴力维护

    Problem E. TeaTree Time Limit: 8000/4000 MS (Java/Others)    Memory Limit: 524288/524288 K (Java/Oth ...

  9. 将日期转换为指定的格式:比如转换成 年月日时分秒 这种格式:yyyy-MM-dd hh:mm:ss 或者 yyyy-MM-dd。总结下。

    可以为Date原型添加如下的方法: Date.prototype.format = function(fmt) { var o = { "M+" : this.getMonth() ...

  10. 子网掩码与ip有实际关系吗?

    子网掩码是作为ip地址的标识,还是本身就是ip地址的一部分?例如10.10.10.1/24和10.10.10.1/25是同一个ip地址吗? 作者:知乎用户链接:https://www.zhihu.co ...