某某茶叶有限公司欢迎您!
金沙棋牌在线 > 服务器&运维 > Linux多线程编程

Linux多线程编程

时间:2020-03-22 23:21

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

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

线程分类

引言
条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:一个线程等待条件变量的条件成立而挂起(此时不再占用cpu);另一个线程使条件成立(给出条件成立信号)。为了防止竞争,条件变量的使用总是和一个互斥锁结合在一起。

  • class="wp_keywordlink">Volatile
  • __thread
  • Memory Barrier
  • __sync_synchronize

下面是我们的代码:

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

函数原型

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

  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修饰的变量只能在编译期初始化,且只能通过常量表达式来初始化。

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

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

#include <pthread.h>

/* 定义两个条件变量 */
pthread_cond_t cond_pro, cond_con;

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对应release barrier。哦,那么full barrier呢?

引文:

线程创建的Linux实现

  1. 初始化和销毁条件变量

__sync_synchronize

__sync_synchronize就是一种full barrier

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()传入。

#include <pthread.h>

int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);int pthread_cond_destroy(pthread_cond_t *cond); /* 初始化条件变量 */
pthread_cond_init(&cond_pro, NULL);
pthread_cond_init(&cond_con, NULL);
/* 销毁条件变量 */
pthread_cond_destroy(&cond_pro);
pthread_cond_destroy(&cond_pro);

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

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

  1. 等待和激发条件

引文:

主要函数说明

#include <pthread.h>

int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);

int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);
/* 等待条件 */
/* 注意:pthread_cond_wait为阻塞函数。解开锁,再等待。等条件满足时,需要抢到锁,才可以被唤醒*/  
pthread_cond_wait(&cond_pro,&mutex); 

/* 激发条件 */
/* 所有因为不满足条件的线程都会阻塞在条件变量cond_pro中的一个队列中 */
/* 以广播方式,通知所有被阻塞的所有线程 */
pthread_cond_broadcast(&cond_pro);
/* 以signal方式,只通知排在最前面的线程 */
pthread_cond_signal(&cond_pro);

线程相关操作

1.线程的创建和退出

代码

一 pthread_t

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

/*************************************************************************
  > File Name: my_con.c
  > Author: KrisChou
  > Mail:zhoujx0219@163.com 
  > Created Time: Tue 26 Aug 2014 10:24:29 AM CST
 ************************************************************************/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
#define CELL 10
#define FLORE 0



int i = 0; /* 所有线程共享的全局变量,此处假定至多递增至10,最小减到0 */

pthread_mutex_t mutex;       /* 定义互斥锁 */
pthread_cond_t cond_pro, cond_con; /* 定义两个条件变量 */

/* 生产者线程 */
void* pro_handler(void *arg)
{
  pthread_detach(pthread_self());  /* 由系统回收线程资源,而非主线程回收资源 ,此类情况主线程是个服务器,永久不会退出 */

  while(1)
  {
    pthread_mutex_lock(&mutex);
    while(i >= CELL)
    {
      pthread_cond_wait(&cond_pro,&mutex); 
      /* continue是轮询,此处是阻塞 */
      /* 把锁放开再等 ,第一个参数是结构体指针,其中有成员存放被阻塞的函数 */
      /*不占cpu*/
      /* 不满足条件时才会等 ,需要别人告诉它,才能唤醒它*//* 当它返回时,锁也要回来了*/
    }
    i++;
    if(i == 1)
    {
      /* 由空到不空,唤醒消费者 */
      pthread_cond_signal(&cond_con);  /*不会立马signal被阻塞的消费者线程,因为其还要等锁抢回来*/
    }
    printf("add i: %d n", i);
    pthread_mutex_unlock(&mutex);
    sleep(rand() % 5 + 1);
  }
}

/* 消费者线程 */
void* con_handler(void *arg)
{
  pthread_detach(pthread_self());
  while(1)
  {
    pthread_mutex_lock(&mutex);
    while(i <= FLORE)
    {
      pthread_cond_wait(&cond_cno,&mutex);
    }
    i--;
    if(i == 9) /* 由满到不满,要告诉生产者,以便将其唤醒 *//*此处,直接signal也可以,我们是为了更加精确*/
    {
      pthread_cond_signal(&cond_pro);
    }
    printf("con i: %d n", i);
    pthread_mutex_unlock(&mutex);
    sleep(rand() % 5 + 1);
  }
}

int main(int argc, char *argv[]) // exe +num -num
{
  srand(getpid());
  int con_cnt, pro_cnt;
  pro_cnt = atoi(argv[1]);
  con_cnt = atoi(argv[2]);
  pthread_mutex_init(&mutex,NULL);
  pthread_cond_init(&cond_pro,NULL);
  pthread_cond_init(&cond_con,NULL);
  pthread_t *arr = (pthread_t*)calloc(con_cnt + pro_cnt , sizeof(pthread_t));
  int index = 0;
  while(pro_cnt > 0)
  {
    pthread_create(arr + index, NULL, pro_handler, NULL);
    index++;
    pro_cnt--;
  }
  while(con_cnt > 0)
  {
    pthread_create(arr + index, NULL, con_handler, NULL);
    index++;
    con_cnt--;
  }
  while(1);
  pthread_mutex_destroy(&mutex);
  pthread_cond_destroy(&cond_pro);
  pthread_cond_destroy(&cond_con);
  return 0;
}

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

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

注意
无论是在生产者线程,还是在消费者线程中。标记黄色部分的判断条件必须用while。以生产者线程举例,当i>=CELL时,也就是i满时,此时执行pthread_cond_wait(&cond_cno,&mutex); 该生产者线程被挂起。必须等到消费者线程pthread_cond_signal(&cond_pro); 将其唤醒。但是消费者将其signal还不够,被挂其的生产者线程必须重新拿到锁,才可以被激活。但是,由于在消费者signal的同时,生产者并不能立即抢到锁,所以此时可能i值又改变变为大于等于10了。因此必须用while。不然可能导致i>10。

二 pthread_create

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

您可能感兴趣的文章:

  • linux下c语言的多线程编程
  • linux下的CC++多进程多线程编程实例详解
  • 详解Linux多线程编程(不限Linux)
  • linux多线程编程(五)
  • linux多线程编程(四)
  • Linux下的多线程编程(三)
  • Linux多线程编程(一)
  • Linux多线程编程快速入门