Linux并发程序设计(2)——线程

       在某个程序运行的同时系统就会创建一个进程,并且系统会给进程分配独立的地址空间,而且系统会把进程的详细信息保存在task_struct结构体中。
       由于每个进程都要参与内核调度互相不影响,那么会导致进程在切换时系统开销比较大(进程的代码数据存放下内存中,CPU要访问进程就需要读取内存。一个高速设备访问一个低速设备,为了匹配就需要一个成本高容量小的cache高速缓存设备,这样就会导致进程频繁切换时系统会频繁刷新cache、TLB导致开销比较大)。因此很多操作系统引入了轻量级进程LWP(线程),同一进程中的线程共享相同的地址空间。
往期推荐 :
       Linux并发程序设计(1)——进程
       我还是曾今那个少年(一个普通男孩的十年)——纪念博客访问量突破十万

线程特点

  • 线程指的是共享相同地址空间的多个任务
  • 使用线程能大幅提高任务切换效率
  • 避免额外的TLB和cache的刷新
  • 一个进程中的多个线程共享以下资源
        1. 可执行命令
        2. 静态数据
        3. 进程中打开的文件描述符
        4. 当前工作目录
        5. 用户ID
        6. 用户组ID
  • pthread线程库提供了如下基本操作
       1. 创建线程
       2. 回收线程
       3. 结束线程
  • 同步和互斥机制
       1. 信号量
       2. 互斥锁

线程相关函数

创建线程——pthread_create

#include <stdio.h>
int pthread_create(pthread_t *thread,const
		pthread_attr_t *attr,void *(*routine)(void*),void *arg);
  • 成功时返回0,失败时返回错误码
  • thread线程对象
  • attr线程属性,NULL代表默认属性
  • routine是线程创建后执行的函数
  • arg传递给routine的参数(不需要参数时为NULL)

线程回收——pthread_join

#include<pthread.h>
int pthread_join(pthread_t thread,void **retval);
  • 执行成功返回0,失败返回错误码
  • thread要回收线程对象
  • 回收线程没有结束,调用线程阻塞直到thread结束
  • *retval接收线程thread的返回值

结束线程——pthread_exit

#include <pthread.h>
void pthread_exit(void *retval);//返回的地址不能是局部变量的地址
  • 结束当前线程
  • retval可被其他线程通过pthread_join获取
  • 线程结束,线程栈私有资源被释放

注意结束线程不能用exit/_exit,这样会使当前进程中创建的线程全部结束.

示例:
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<pthread.h>
#include<stdlib.h>
#include<time.h>

char message[32]="hello world";//定义一个全局变量的数组,存放在静态存储区
void *thread_func(void *arg);//声明线程开启后执行的函数

int main()
{
    pthread_t a_thread;//线程对象变量
    void *result;
    if(pthread_create(&a_thread,NULL,thread_func,NULL)!=0){
        printf("fail to pthread_create");
        exit(-1);
    }
    printf("start\n");
    pthread_join(a_thread,&result);//等待a_thread结束
    printf("result is %s\n",result);
    printf("message is %s\n",message);
    return 0;
}

void *thread_func(void *arg)
{
    sleep(1);//睡眠一秒
    strcpy(message,"marked by thread");
    pthread_exit("thank you for waiting for me");//退出当前线程,返回结果,
}

注意:
       线程的相关函数都是通过相关的第三方库来实现的因此在使用gcc编译时要使用-l链接选项来链接pthread,结束线程也可以使用return。在进程中父进程结束子进程依旧可以继续运行,但是在线程中父进程(main函数)结束,子线程也随之结束

线程的通信

Alt
       由于线程共享同一进程的地址空间,这个地址空间的代码和全局数据每个线程都可以访问。这样就使得线程间通信很容易,可以通过全局变量来交换数据实现通信。为了避免多个线程访问全局变量时出现线程调度、时间片用完等情况造成数据的破坏,那么我们就需要同步和互斥机制来保证数据的完整。

同步

       同步(synchronization)指的是多个任务按照约定的先后次序互相配合完成一件事1968年,Edsgar Dijkstra基于信号量提出了一种同步机制,由信号量来决定线程是继续运行还是阻塞等待。

       信号量代表一类资源,其值表示系统中该资源的数量,同时它也是一个受保的变量,只能通过三种操作来访问(初始化、P操作——申请资源、V操作——释放资源)。

P/V操作
  • P(S)含义如下:
if(信号量的值大于0){申请资源的任务继续运行;
					信号量的值减一;}
else{申请资源的任务阻塞;}
  • V(S)含义如下:
信号量的值加一;
if(有任务在等待资源){唤醒等待业务,让其继续运行}
posix信号量
  • posix中定义了两类信号量
    • 无名信号量(基于内存的信号量)——只适用于进程
    • 有名信号量——线程进程都适用
  • pthread库常用信号量操作函数
  1. 信号量初始化函数
#include<semaphore.h>
int sem_init(sem_t *sem,int pshared,unsigned int value);
  • 成功时返回0,失败返回EOF
  • sem指向要初始化的信号量的对象
  • pshared 0 线程间 1 进程间
  • val信号量初值
  1. P操作函数
#include<semaphore.h>
int sem_wait(sem_t *sem);//P操作
  • 成功时返回0,失败返回EOF
  • sem指向要操作的信号量对象
  1. V操作函数
#include<semaphore.h>
int sem_post(sem_t *sem);//V操作
  • 成功时返回0,失败返回EOF
  • sem指向要操作的信号量对象

注意:P操作是在任务需要访问资源之前,如果访问完资源需要通过V操作告诉系统。

示例:
       两个线程同步读写缓冲区(生产者/消费者问题)
#include<stdio.h>
#include<pthread.h>
#include<semaphore.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
char buf[32];//全局缓冲区
sem_t sem;
void *function(void *arg);//线程执行函数
int main()
{
    pthread_t a_thread;
    if(sem_init(&sem,0,0)<0){//信号量初始化
        perror("sem_init");
        exit(-1);
    }
    if(pthread_create(&a_thread,NULL,function,NULL)!=0){
        printf("fail to pthread_create"); 
        exit(-1);
    }
    do{
        fgets(buf,32,stdin);
        sem_post(&sem);//V操作,写缓冲区
    }while(strncmp(buf,"quit",4)!=0);//看输入是否为quit,不是则继续
    return 0;
}
void *function(void *arg)
{
    while(1)
    {
        sem_wait(&sem);//P操作,检查缓冲区是否有数据
        printf("you enter %d characters\n",strlen(buf));//操作成功完成打印结果 
    }
}

       以上程序并不是严格意义上的同步,因为只有P操作(读缓冲区)在操作前检查缓冲区是否有数据,但是V操作(写操作)并没有检查操作前是否有缓冲数据。 因此以上代码可以进行为以下代码。

#include<stdio.h>
#include<pthread.h>
#include<semaphore.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
char buf[32];//全局缓冲区
sem_t sem_r,sem_w;//sem_r可读缓冲物的个数,sem_w可写缓冲区的个数
void *function(void *arg);//线程执行函数
int main()
{
    pthread_t a_thread;
    if(sem_init(&sem_r,0,0)<0){//信号量初始化,一开始可读缓冲区个数为0
        perror("sem_init");
        exit(-1);
    }
    if(sem_init(&sem_w,0,1)<0){//信号量初始化,一开始可写,故可写缓冲区个数为1
        perror("sem_init");
        exit(-1);
    }
    if(pthread_create(&a_thread,NULL,function,NULL)!=0){
        printf("fail to pthread_create"); 
        exit(-1);
    }
    do{
    	sem_wait(&sem_w);//P操作,判断是否有可以写的线程
        fgets(buf,32,stdin);
        sem_post(&sem_r);//V操作,写缓冲区
    }while(strncmp(buf,"quit",4)!=0);//看输入是否为quit,不是则继续
    return 0;
}
void *function(void *arg)
{
    while(1)
    {
        sem_wait(&sem_r);//P操作,检查缓冲区是否有数据
        printf("you enter %d characters\n",strlen(buf));//操作成功完成打印结果 
        sem_post(&sem_w);//缓冲区可写,就通知下一次可写操作
    }
}

互斥

临界资源

  • 一次只允许一个任务(进程、线程)访问的共享资源

临界区

  • 访问临界区的代码

互斥机制

  • mutex互斥锁——只能有一个任务可以获得
  • 任务访问临界区前申请锁,访问完后释放锁

互斥锁初始化——pthread_mutex_init

#include<pthread.h>
int pthread_mutex_init(pthread_mutex_t *mutex,const pthread_mutex_t* attr);
  • 成功时返回0,失败时返回错误码
  • mutex指向初始化的互斥锁对象
  • attr互斥锁属性,NULL表示缺省属性(一般情况就是NULL)

申请锁——pthread_mutex_lock

#include<pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);
  • 成功时返回0,失败时返回错误码
  • mutex指向初始化的互斥锁对象
  • 如果无法获得锁,任务阻塞

释放锁——pthread_mutex_unclock

#include<pthread.h>
int pthread_mutex_unclock(pthread_mutex_t *mutex);
  • 成功时返回0,失败时返回错误码
  • mutex指向要初始化的互斥锁对象
  • 执行完临界区要及时释放锁
示例:
#include<stdio.h>
#include<pthread.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>

unsigned int count,value1,value2;
pthread_mutex_t lock;
void *funtion(void *arg);
int main()
{
    pthread_t a_thread;
    if(pthread_mutex_init(&lock,NULL)!=0){//锁初始化,默认属性,只需要初始化一次
        printf("fail to pthread_mutex_init\n");
        exit(-1);
    }
    if(pthread_create(&a_thread,NULL,funtion,NULL)!=0){//创建线程
        printf("fail to pthread_create");
        exit(-1);
    }
    while(1){
        count++;
    #ifdef _LOCK_//条件编译
        pthread_mutex_lock(&lock);
    #endif
        value1=count;
        value2=count;
    #ifdef _LOCK_
        pthread_mutex_unlock(&lock);
    #endif
    }
    return 0;
}

void *funtion(void *arg)
{
    while(1){
    #ifdef _LOCK_
        pthread_mutex_lock(&lock);
    #endif 
        if(value1!=value2){
            printf("value1=%u,value2=%u\n",value1,value2);
            usleep(100000);
        }
    #ifdef _LOCK_
        pthread_mutex_unlock(&lock);
    #endif 
    }
    return NULL;
}

       以上文件存放在Syn.c中,通过命令gcc -o syn Syn.c -lpthread -D_LOCK_来运行代码可以发现保护了临界区的数据。

       不积小流无以成江河,不积跬步无以至千里。而我想要成为万里羊,就必须坚持学习来获取更多知识,用知识来改变命运,用博客见证成长,用行动证明我在努力。
       如果我的博客对你有帮助、如果你喜欢我的博客内容,记得“点赞” “评论” “收藏”一键三连哦!听说点赞的人运气不会太差,每一天都会元气满满呦!如果实在要白嫖的话,那祝你开心每一天,欢迎常来我博客看看。
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/weixin_44895651/article/details/107876842