67677新澳门手机版 > 服务器&运维 > Linux环境多线程编程基础设施
Linux环境多线程编程基础设施
2020-03-12 23:47

本文介绍多线程环境下并行编程的基础设施。主要包括:

##概念

   下面先来一个实例。我们通过创建两个线程来实现对一个数的递加。
或许这个实例没有实际运用的价值,但是稍微改动一下,我们就可以用到其他地方去拉。

线程分类

  • class="wp_keywordlink">Volatile
  • __thread
  • Memory Barrier
  • __sync_synchronize
  • 原来指向main()的线程叫做主线程(main thread)
  • 使用pthread_create()创建出来的线程,叫做子线程(child thread)
  • 主/子线程只有在创建时才有区别, 创建完了就一视同仁, 都是一样的独立个体, 可以有交流、共享和私有, 但没有上下级, 这一点和多进程一样, 只有在创建的瞬间才有parent process 和child process 的区别, 创建完了就都是一样的独立个体
  • 创建完子线程之后,两个线程之间独立运行,线程的执行先后次序由OS的调度算法决定
  • 线程之间相互独立也相互影响,因为主线程结束时,会导致进程结束,进程结束时,会导致该进程的所有线程结束
  • 多个线程共享一个进程, 而一个进程只有一个输出终端, So一定要调度好, 要不有的线程输出会看不到, 最low的做法就是sleep()一下保证线程可以执行完 新澳门手机版mg游戏 1

下面是我们的代码:

    线程按照其调度者可以分为用户级线程和核心级线程两种。

volatile

编译器有时候为了优化性能,会将一些变量的值缓存到寄存器中,因此如果编译器发现该变量的值没有改变的话,将从寄存器里读出该值,这样可以避免内存访问。

但是这种做法有时候会有问题。如果该变量确实(以某种很难检测的方式)被修改呢?那岂不是读到错的值?是的。在多线程情况下,问题更为突出:当某个线程对一个内存单元进行修改后,其他线程如果从寄存器里读取该变量可能读到老值,未更新的值,错误的值,不新鲜的值。

如何防止这样错误的“优化”?方法就是给变量加上volatile修饰。

volatile int i=10;//用volatile修饰变量i
......//something happened 
int b = i;//强制从内存中读取实时的i的值

OK,毕竟volatile不是完美的,它也在某种程度上限制了优化。有时候是不是有这样的需求:我要你立即实时读取数据的时候,你就访问内存,别优化;否则,你该优化还是优化你的。能做到吗?

不加volatile修饰,那么就做不到前面一点。加了volatile,后面这一方面就无从谈起,怎么办?伤脑筋。

其实我们可以这样:

int i = 2; //变量i还是不用加volatile修饰

#define ACCESS_ONCE(x) (* (volatile typeof(x) *) &(x))

需要实时读取i的值时候,就调用ACCESS_ONCE(i),否则直接使用i即可。

这个技巧,我是从《Is parallel programming hard?》上学到的。

听起来都很好?然而险象环生:volatile常被误用,很多人往往不知道或者忽略它的两个特点:在C/C++语言里,volatile不保证原子性;使用volatile不应该对它有任何Memory Barrier的期待。

第一点比较好理解,对于第二点,我们来看一个很经典的例子:

volatile int is_ready = 0;
char message[123];
void thread_A
{
  while(is_ready == 0)
  {
  }
  //use message;
}
void thread_B
{
  strcpy(message,"everything seems ok");
  is_ready = 1;
}

线程B中,虽然is_readyvolatile修饰,但是这里的volatile不提供任何Memory Barrier,因此12行和13行可能被乱序执行,is_ready = 1被执行,而message还未被正确设置,导致线程A读到错误的值。

这意味着,在多线程中使用volatile需要非常谨慎、小心。

##模型

/*thread_example.c : c multiple thread programming in linux
 *author : falcon
 *E-mail : tunzhj03@st.lzu.edu.cn
 */
#include <pthread.h>
#include <stdio.h>
#include <sys/time.h>
#include <string.h>
#define MAX 10

pthread_t thread[2];
pthread_mutex_t mut;
int number=0, i;

void *thread1()
{
    printf ("thread1 : I'm thread 1/n");

    for (i = 0; i < MAX; i++)
    {
        printf("thread1 : number = %d/n",number);
        pthread_mutex_lock(&mut);
            number++;
        pthread_mutex_unlock(&mut);
        sleep(2);
    }


    printf("thread1 :主函数在等我完成任务吗?/n");
    pthread_exit(NULL);
}

void *thread2()
{
    printf("thread2 : I'm thread 2/n");

    for (i = 0; i < MAX; i++)
    {
        printf("thread2 : number = %d/n",number);
        pthread_mutex_lock(&mut);
            number++;
        pthread_mutex_unlock(&mut);
        sleep(3);
    }


    printf("thread2 :主函数在等我完成任务吗?/n");
    pthread_exit(NULL);
}

void thread_create(void)
{
    int temp;
    memset(&thread, 0, sizeof(thread));     //comment1
    /*创建线程*/
    if((temp = pthread_create(&thread[0], NULL, thread1, NULL)) != 0) //comment2   
        printf("线程1创建失败!/n");
    else
        printf("线程1被创建/n");

    if((temp = pthread_create(&thread[1], NULL, thread2, NULL)) != 0) //comment3
        printf("线程2创建失败");
    else
        printf("线程2被创建/n");
}

void thread_wait(void)
{
    /*等待线程结束*/
    if(thread[0] !=0)      {       //comment4          pthread_join(thread[0],NULL);
        printf("线程1已经结束/n");
     }
    if(thread[1] !=0)      {         //comment5        pthread_join(thread[1],NULL);
        printf("线程2已经结束/n");
     }
}

int main()
{
    /*用默认属性初始化互斥锁*/
    pthread_mutex_init(&mut,NULL);

    printf("我是主函数哦,我正在创建线程,呵呵/n");
    thread_create();
    printf("我是主函数哦,我正在等待线程完成任务阿,呵呵/n");
    thread_wait();

    return 0;
}

    (1)用户级线程
    用户级线程主要解决的是上下文切换的问题,它的调度算法和调度过程全部由用户自行选择决定,在运行时不需要特定的内核支持。在这里,操作系统往往会提供一个用户空间的线程库,该线程库提供了线程的创建、调度、撤销等功能,而内核仍然仅对进程进行管理。如果一个进程中的某一个线程调用了一个阻塞的系统调用,那么该进程包括该进程中的其他所有线程也同时被阻塞。这种用户级线程的主要缺点是在一个进程中的多个线程的调度中无法发挥多处理器的优势。

__thread

__threadgcc内置的用于多线程编程的基础设施。用__thread修饰的变量,每个线程都拥有一份实体,相互独立,互不干扰。举个例子:

#include<iostream>  
#include<pthread.h>  
#include<unistd.h>  
using namespace std;
__thread int i = 1;
void* thread1(void* arg);
void* thread2(void* arg);
int main()
{
  pthread_t pthread1;
  pthread_t pthread2;
  pthread_create(&pthread1, NULL, thread1, NULL);
  pthread_create(&pthread2, NULL, thread2, NULL);
  pthread_join(pthread1, NULL);
  pthread_join(pthread2, NULL);
  return 0;
}
void* thread1(void* arg)
{
  cout<<++i<<endl;//输出 2  
  return NULL;
}
void* thread2(void* arg)
{
  sleep(1); //等待thread1完成更新
  cout<<++i<<endl;//输出 2,而不是3
  return NULL;
}

需要注意的是:

1,__thread可以修饰全局变量、函数的静态变量,但是无法修饰函数的局部变量。

2,被__thread修饰的变量只能在编译期初始化,且只能通过常量表达式来初始化。

$gcc  -pthread
#include<pthread.h>
pthread_self()/pthread_equal()            //获得ThreadID
pthread_attr_init()/pthread_attr_setdetachstate()/…                //创建一个线程前设置     
pthread_create()                          //创建一个线程            
pthread_detach()pthread_setcancelstate()/pthread_setcanceltype()    //已有一个线程后设置      
pthread_kill()                            //向线程发送一个信号        
pthread_exit()                            //退出线程但不退出进程    
pthread_cancel()                          //终止另一个线程            
pthread_join()                            //等待一个线程            

下面我们先来编译、执行一下

    (2)核心级线程
    这种线程允许不同进程中的线程按照同一相对优先调度方法进行调度,这样就可以发挥多处理器的并发优势。
    现在大多数系统都采用用户级线程与核心级线程并存的方法。一个用户级线程可以对应一个或几个核心级线程,也就是“一对一”或“多对一”模型。这样既可满足多处理机系统的需要,也可以最大限度地减少调度开销。

Memory Barrier

为了优化,现代编译器和CPU可能会乱序执行指令。例如:

int a = 1;
int b = 2;
a = b + 3;
b = 10;

CPU乱序执行后,第4行语句和第5行语句的执行顺序可能变为先b=10然后再a=b+3

有些人可能会说,那结果不就不对了吗?b为10,a为13?可是正确结果应该是a为5啊。

哦,这里说的是语句的执行,对应的汇编指令不是简单的mov b,10和mov b,a+3。

生成的汇编代码可能是:

movl    b(%rip), %eax ; 将b的值暂存入%eax
movl    $10, b(%rip) ; b = 10
addl    $3, %eax ; %eax加3
movl    %eax, a(%rip) ; 将%eax也就是b+3的值写入a,即 a = b + 3

这并不奇怪,为了优化性能,有时候确实可以这么做。但是在多线程并行编程中,有时候乱序就会出问题。

一个最典型的例子是用锁保护临界区。如果临界区的代码被拉到加锁前或者释放锁之后执行,那么将导致不明确的结果,往往让人不开心的结果。

还有,比如随意将读数据和写数据乱序,那么本来是先读后写,变成先写后读就导致后面读到了脏的数据。因此,Memory Barrier就是用来防止乱序执行的。具体说来,Memory Barrier包括三种:

1,acquire barrieracquire barrier之后的指令不能也不会被拉到该acquire barrier之前执行。

2,release barrierrelease barrier之前的指令不能也不会被拉到该release barrier之后执行。

3,full barrier。以上两种的合集。

所以,很容易知道,加锁,也就是lock对应acquire barrier;释放锁,也就是unlock新澳门手机版mg游戏,对应release barrier。哦,那么full barrier呢?

##pthread_self()

引文:

线程创建的Linux实现

__sync_synchronize

__sync_synchronize就是一种full barrier

//返回调用线程的ID
pthread_t pthread_self(void);
falcon@falcon:~/program/c/code/ftp$ gcc -lpthread -o thread_example thread_example.c
falcon@falcon:~/program/c/code/ftp$ ./thread_example
我是主函数哦,我正在创建线程,呵呵
线程1被创建
线程2被创建
我是主函数哦,我正在等待线程完成任务阿,呵呵
thread1 : I'm thread 1
thread1 : number = 0
thread2 : I'm thread 2
thread2 : number = 1
thread1 : number = 2
thread2 : number = 3
thread1 : number = 4
thread2 : number = 5
thread1 : number = 6
thread1 : number = 7
thread2 : number = 8
thread1 : number = 9
thread2 : number = 10
thread1 :主函数在等我完成任务吗?
线程1已经结束
thread2 :主函数在等我完成任务吗?
线程2已经结束

    Linux的线程实现是在核外进行的,核内提供的是创建进程的接口do_fork()。内核提供了两个系统调用clone()和fork(),最终都用不同的参数调用do_fork()核内API。当然,要想实现线程,没有核心对多进程(其实是轻量级进程)共享数据段的支持是不行的,因此,do_fork()提供了很多参数,包括CLONE_VM(共享内存空间)、CLONE_FS(共享文件系统信息)、 CLONE_FILES(共享文件描述符表)、CLONE_SIGHAND(共享信号句柄表)和CLONE_PID(共享进程ID,仅对核内进程,即0号进程有效)。当使用fork系统调用时,内核调用do_fork()不使用任何共享属性,进程拥有独立的运行环境,而使用 pthread_create()来创建线程时,则最终设置了所有这些属性来调用__clone(),而这些参数又全部传给核内的do_fork(),从而创建的“进程”拥有共享的运行环境,只有栈是独立的,由__clone()传入。

##pthread_equal()

实例代码里头的注释应该比较清楚了吧,下面我把网路上介绍上面涉及到的几个函数和变量给引用过来。

    Linux线程在核内是以轻量级进程的形式存在的,拥有独立的进程表项,而所有的创建、同步、删除等操作都在核外pthread库中进行。pthread 库使用一个管理线程(__pthread_manager(),每个进程独立且唯一)来管理线程的创建和终止,为线程分配线程ID,发送线程相关的信号(比如Cancel),而主线程(pthread_create())的调用者则通过管道将请求信息传给管理线程。

//对比两个线程ID,相等返回非0,不等返回0
int pthread_equal(pthread_t t1, pthread_t t2);

引文:

主要函数说明

##pthread_attr_init()/ pthread_attr_destroy()

线程相关操作

1.线程的创建和退出

//pthread_attr_init()初始化一个线程属性对象attr,不指定attr就按默认参数进行初始化。
//pthread_attr_destroy()销毁一个线程属性对象,销毁的操作对使用这个attr的线程有影响
//成功返回0,失败返回error number
int pthread_attr_init(pthread_attr_t *attr);
int pthread_attr_destroy(pthread_attr_t *attr);

一 pthread_t

    pthread_create 线程创建函数
    int pthread_create (pthread_t * thread_id,__const pthread_attr_t * __attr,void *(*__start_routine) (void *),void *__restrict __arg);

##更改attr对象的函数

pthread_t在头文件/usr/include/bits/pthreadtypes.h中定义:
  typedef unsigned long int pthread_t;
  它是一个线程的标识符。

    线程创建函数第一个参数为指向线程标识符的指针,第二个参数用来设置线程属性,第三个参数是线程运行函数的起始地址,最后一个参数是运行函数的参数。这里,我们的函数thread 不需要参数,所以最后一个参数设为空指针。第二个参数我们也设为空指针,这样将生成默认属性的线程。当创建线程成功时,函数返回0,若不为0 则说明创建线程失败,常见的错误返回代码为EAGAIN 和EINVAL。前者表示系统限制创建新的线程,例如线程数目过多了;后者表示第二个参数代表的线程属性值非法。创建线程成功后,新创建的线程则运行参数三和参数四确定的函数,原来的线程则继续运行下一行代码。

//成功返回0,失败返回error number
typedef struct{
    int                 detachstate;        //线程的分离状态      
    int                 schedpolicy;        //线程调度策略
    struct sched_param  schedparam;         //线程的调度参数
    int                 inheritsched;       //线程的继承性
    int                 scope;              //线程的作用域
    size_t              guardsize;          //线程栈末尾的警戒缓冲区大小
    int                 stackaddr_set;      //线程的栈设置
    void *              stackaddr;          //线程栈的位置
    size_t              stacksize;          //线程栈的大小
}pthread_attr_t;

pthread_attr_setdetachstate(pthread_attr_t* attr, int detachstate)
pthread_attr_getdetachstate(const pthread_attr_t* attr, int* detachstate)
PTHREAD_CREATE_JOINABLE /   PTHREAD_CREAT_DETACHED
int detachstate;

pthread_attr_getdetachstate(&attr,&detachstate);
if(PTHREAD_CREATE_JOINABLE==detachstate)
    printf("1.PTHREAD_CREATE_JOINABLEn");      //default

pthread_attr_setschedpolicy(pthread_attr_t *attr, int policy);
pthread_attr_getschedpolicy(const pthread_attr_t *attr, int *policy);
SCHED_OTHER     /   SCHED_FIFO  /   SCHED_RR

pthread_attr_setsschedchedparam(pthread_attr_t *attr, const struct sched_param *param);
pthread_attr_getschedparam(const pthread_attr_t *attr, struct sched_param *param);
struct sched_param {
    int sched_priority;// Scheduling priority,int sched_get_priority_max/sched_get_priority_min (int policy)
};

pthread_attr_setinheritsched(pthread_attr_t *attr, int inheritsched);
pthread_attr_getinheritsched(const pthread_attr_t *attr, int *inheritsched);
PTHREAD_INHERIT_SCHED       /    PTHREAD_EXPLICIT_SCHED

pthread_attr_setscope(pthread_attr_t *attr, int scope);
pthread_attr_getscope(const pthread_attr_t *attr, int *scope);
PTHREAD_SCOPE_SYSTEM        /   PTHREAD_SCOPE_PROCESS

pthread_attr_setguardsize ( pthread_attr_t *attr, size_t guardsize );
pthread_attr_getguardsize ( const pthread_attr_t *attr, size_t *guardsize );
>0               /   0
(默认1 page,当然还可以指定任意值)

pthread_attr_setstack(pthread_attr_t *attr, void *stackaddr, size_t stacksize);
pthread_attr_getstack(const pthread_attr_t *attr, void **stackaddr, size_t *stacksize);

pthread_attr_setstacksize ( pthread_attr_t *attr, size_t size );
pthread_attr_getstacksize ( const pthread_attr_t *attr, size_t *size );

二 pthread_create

    pthread_join 函数,来等待一个线程的结束。
    函数原型为:int pthread_join (pthread_t __th, void **__thread_return)
    第一个参数为被等待的线程标识符,第二个参数为一个用户定义的指针,它可以用来存储被等待线程的返回值。这个函数是一个线程阻塞的函数,调用它的函数将一直等待到被等待的线程结束为止,当函数返回时,被等待线程的资源被收回。线程只能被一个线程等待终止,并且应处于joinable状态(非detached)。