Linux---线程总结笔记

很多东西记住之后也很容易忘记,整理的资料

进程和线程的区别

Linux线程详解(概念、原理、实现方法、优缺点)这位大佬讲的非常明白。接下来,我根据其他的资料将一些重点自己总结一下。感觉下图很好理解,图源还是来自上述链接。
在这里插入图片描述

进程

  • 进程有独立的地址空间
  • Linux为每个进程创建task_struct
  • 每个进程都参与内核调度,互不影响

但是进程在切换时系统开销大,这个时候就需要用到线程。

线程

使用线程可以大大提高任务切换的效率,避免了额外的TLB & cache的刷新。因为同一进程中的线程共享相同地址空间

一个进程中的多个线程共享资源包括

  • 可执行的指令
  • 静态数据
  • 进程中打开的文件描述符
  • 当前工作目录
  • 用户ID
  • 用户组ID

每个线程私有的资源包括

  • 线程ID (TID)
  • PC(程序计数器)和相关寄存器
  • 堆栈
  • 错误号 (errno)
  • 优先级
  • 执行状态和属性

Linux线程库

(包含的头文件: #include <pthread.h>)

  • 创建线程
    int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(routine)(void), void *arg)

  • 回收线程(三种方式)
    1、pthread_join函数
    是阻塞函数,如果回收的线程没有结束,则一直等待。该函数的缺点是对于一个线程回收比较方便,对于多个线程来说,前面的线程没有结束释放,会一直阻塞,导致后面的线程无法释放。
    2、pthread_detach线程分离函数
    独立使用,线程是默认线程时,创建线程时第二个参数是NULL(默认)。
    3、创建线程时候设置为分离属性
    独立使用
    pthread_attr_t attr;
    pthread_attr_init(&attr);
    pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED);

  • 结束线程
    void pthread_exit(void *retval) // 结束当前线程。会在结束前执行清理工作、释放资源等操作

  • 取消线程
    线程的取消是指在多线程环境中,一个线程(称为取消线程)请求取消另一个线程(称为目标线程)的执行。取消线程会发送取消请求给目标线程,要求其停止正在执行的任务。取消线程时需要设置取消点,还可设置取消类型。

    取消请求可以是线程粗略的终止方式,可能会导致资源泄漏或者程序状态不一致
    因此需要清理线程

  • 清理线程
    void pthread_cleanup_push(void (*routine) (void *), void *arg)
    void pthread_cleanup_pop(int execute) // 注意:这对函数必须成对使用
    routine函数被执行的条件:
    1、 被pthread_cancel取消掉。
    2、 执行pthread_exit
    3、 非0参数执行pthread_cleanup_pop()
    注意:!!!!!!
    1、必须成对使用,即使pthread_cleanup_pop不会被执行到也必须写上,否则编译错误。
    2、pthread_cleanup_pop()被执行且参数为0,pthread_cleanup_push回调函数routine不会被执行.
    3、pthread_cleanup_push 和pthread_cleanup_pop可以写多对,routine执行顺序正好相反//先进后出
    4、线程内的return 可以结束线程,也可以给pthread_join返回值,但不能触发pthread_cleanup_push里面的回调函数,所以我们结束线程尽量使用pthread_exit退出线程。

  • 信号量
    也叫信号灯,用于同步信号,我后面会梳理相关内容。

  • 互斥锁
    我之前写过相关概念及用法,移步 C-线程之互斥锁、动态锁、静态锁这篇。

在实际编程中需要注意:

  1. 线程创建需要时间,如果主进程马上退出,那线程不能得到执行 //所以通常加 sleep(),sleep毫无疑问会导致效率变低。
  2. 需要在编译时候加 -lpthread,不然会报错error: ld returned 1 exit status

其他常用命令:
pthread_t pthread_self(void) 查看自己的TID
ps eLf|grep xxx 查看系统关于关键字xxx的快照
ps ef|grep xxx (和上面一行命令的区别是:少了参数L之后看不到具体的线程,因为L表示显示进程的线程信息)

线程间参数传递(难点)

  1. 通过地址传递参数,注意类型的转换 //注意使用sleep(1)也可以解决一些问题,但是这样效率不高,这个时候就要用到值传递。
  2. 值传递,这时候编译器会告警,需要程序员自己保证数据长度正确。
    //定义的是int型,但是把它当做地址传。

代码展示:(注意看我解释的部分)

#include <pthread.h>
#include <stdio.h>
#include <unistd.h>

void *testThread(void *arg){
    printf("This is a thread test,pid=%d,tid=%lu\n",getpid(),pthread_self());
   // return NULL;
    printf("This is %d thread.\n", (int)arg);  //这里,我们知道该值是int型,所以直接转换成int型就行了,不用强制转换
   // printf("This is %d thread.\n", *(int*)arg); // 当使用地址传递时,代码*(int*)arg将参数强转为int型后通过*取值
   // pthread_exit(NULL);
    while(1){
        sleep(1);
    }
    printf("after pthread exit\n");
}
int main(){
    pthread_t tid[5];
    int ret;
    int arg = 5;
    int i;
    /*该句将int型的i强制转换成了void型,testThread函数需要的是void型指针。 
    这里我们使用值传递,把这个值当成地址去传。该方法需要注意数据长度,程序员根据实际情况自己调整去避免数据丢失。*/
    for(i=0;i<5;i++){   
        ret = pthread_create(&tid[i],NULL,testThread,(void *)i);
//        ret = pthread_create(&tid[i],NULL,testThread,(void *)&i);  
//        sleep(1);   
/*  对于 sleep(1) 的解释
    当使用ret = pthread_create(&tid[i],NULL,testThread,(void *)&i); 我们看到传递的参数是i的地址,如果不加sleep的话,
    线程创建太快了,来不及去++,导致最终程序的print的结果会是一样的。
    
    在这里,地址传递导致效率太低了!!!!!!!!因此我们用值传递,也就是没有注释的那行代码。
*/
        printf("This is main thread,tid=%lu\n",tid[i]);    
    }
    while(1){
        sleep(1);
    }
}

运行上述代码编译器会警告,因为使用了值传递。我们此时可以忽略该警告,程序执行要求达到我们的目的就行了。

条件变量

应用场景:生产者消费者问题。
消费者不知道什么时候会有资源,所以需要等待生产者生产资源,为了在等待的时候不耗费CPU资源,因此我们引入条件变量。条件变量使得消费者在等待生产者生产时让线程休眠。

(下面两段话引用自https://www.cnblogs.com/yychuyu/p/13732262.html
条件变量本身不是锁,但它也可以造成线程阻塞,通常与互斥锁配合使用,给多线程提供一个会合的场所。

如直接使用mutex,除了生产者、消费者之间要竞争互斥量以外,消费者之间也需要竞争互斥量,但如果汇聚(链表)中没有数据,消费者之间竞争互斥锁是无意义的。有了条件变量机制以后,只有生产者完成生产,才会引起消费者之间的竞争。提高了程序效率。

函数

pthread_cond_init函数        //动态初始化条件变量
pthread_cond_destroy函数     //销毁
pthread_cond_wait函数        // 等待一个条件变量
pthread_cond_timedwait函数   // 限时等待一个条件变量
pthread_cond_signal函数      //通知一个消费线程
pthread_cond_broadcast函数   //广播通知多个消费线程

使用步骤(代码框架搭建)

步骤描述配合代码案例食用

1、初始化

类似上篇讲到的静态锁和动态锁,条件变量也是除了静态初始化,还有动态初始化。

  1. 静态初始化
    pthread_cond_t cond = PTHREAD_COND_INITIALIZER; //初始化条件变量
    pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; //初始化互斥量
    这俩通常一起使用
  2. 动态初始化
    pthread_cond_init(&cond);
2、生产者线程
1、pthread_mutex_lock(&mutex);
2、开始产生资源
3、pthread_cond_sigal(&cond);    //通知一个消费线程 (如有多个消费者,它们会争抢)
   //或者
   //pthread_cond_broadcast(&cond); //广播通知多个消费线程
4、pthread_mutex_unlock(&mutex);
3、消费者线程
1、pthread_mutex_lock(&mutex);
2、while (如果没有资源){   //防止惊群效应
       pthread_cond_wait(&cond, &mutex); 
   }
3、有资源了,消费资源
4、pthread_mutex_unlock(&mutex);  

在上述代码片中,我们需要注意:
第一:
步骤2非常重要,while (如果没有资源),加上这句话之后:

1、我们可以避免浪费出租车,也就是说当生产者生产了好几个出租车时,乘客(消费者线程)还没有到,例如当乘客到时已经生产第六辆了,那么加上while (如果没有资源)后,乘客会把前面生产的出租车也消费掉;相反,如果没有加,乘客就会直接开始消费最新生产的出租车,浪费掉之前生产的出租车。

因此,这里我们要注意的点是:如果pthread_cond_signal或者pthread_cond_broadcast 早于 pthread_cond_wait ,则有可能会丢失信号。

2、当消费者个数多于生产者个数时,使用广播机制时避免段错误,错误会出现在下述代码的tx->next = Head处,表明指向了NULL。 pthead_cond_broadcast 信号会被多个线程收到,这叫线程的惊群效应。所以需要加上判断条件while循环。

第二:
pthread_cond_wait(&cond, &mutex)函数会自动在没有资源等待时先unlock,休眠(sleep),等资源到了,再lock。

代码案例(新手可以好好看注释喔)

#include <pthread.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
//1、初始化
pthread_cond_t  hasTaxi=PTHREAD_COND_INITIALIZER;
pthread_mutex_t lock  = PTHREAD_MUTEX_INITIALIZER;

struct taxi{
    struct taxi *next;
    int num;
};

struct taxi *Head=NULL;

void *taxiarv(void *arg){
    printf("taxi arrived thread\n");
    pthread_detach(pthread_self()); //pthread_detach进行线程分离,三种回收线程方式之一
    struct taxi *tx;
    int i=1;
    while(1){
        tx = malloc(sizeof(struct taxi));
        tx->num = i++;
        printf("taxi %d comming\n",tx->num);
        pthread_mutex_lock(&lock);
        tx->next = Head; //Head永远指向链表的首部,最先加入的链表指向链表的末尾。
        Head = tx;
        pthread_cond_signal(&hasTaxi); //生产者给消费者发送信号,提醒资源到了;只能有一个消费者抢到该信号
        //pthread_cond_broadcast(&hasTaxi);
        pthread_mutex_unlock(&lock);
        // 注意!!!在生产者函数中加sleep;但在消费者函数中不加,因为有pthread_cond_wait()函数,会自动阻塞
        sleep(1); //每一秒钟,生产一辆出租车
    }
    pthread_exit(0);  //线程无异常退出
}

void *takeTaxi(void *arg){
    printf("take taxi thread\n");
    pthread_detach(pthread_self());//pthread_detach进行线程分离,三种回收线程方式之一
    struct taxi *tx;
    while(1){
        pthread_mutex_lock(&lock);
        // 有车时Head!=NULL,跳过等待,直接消费;无车时,Head==NULL,执行等待直到车来
        while(Head==NULL) //当不加这行时,当积攒很多车时pthread_cond_wait(&hasTaxi,&lock)进行等待
        {   //等待资源到来
            pthread_cond_wait(&hasTaxi,&lock); //自动阻塞,因此该函数中不用加sleep
        }
        tx = Head;
        Head=tx->next;
        printf("%d,Take taxi %d\n",(int)arg,tx->num);
        free(tx);
        pthread_mutex_unlock(&lock);
    }
    pthread_exit(0);
}

int main(){
    pthread_t tid1,tid2,tid3,tid4;

    pthread_create(&tid1,NULL,taxiarv,NULL);       // 创建生产者线程
//    sleep(5);  //让生产者多生产5秒,也就是让消费者晚来5秒
    pthread_create(&tid2,NULL,takeTaxi,(void*)1);  //创建3个消费者的线程
    pthread_create(&tid3,NULL,takeTaxi,(void*)2);
    pthread_create(&tid4,NULL,takeTaxi,(void*)3);
    //while(1)保证主线程不退出
    while(1) { 
        sleep(1);
    }
}

线程池

通俗的讲就是一个线程的池子,可以循环的完成任务的一组线程集合。
我们平时创建一个线程,完成某一个任务,等待线程的退出。但当需要创建大量的线程时,假设T1为创建线程时间,T2为在线程任务执行时间,T3为线程销毁时间,当 T1+T3 > T2,这时候就不划算了,使用线程池可以降低频繁创建和销毁线程所带来的开销,任务处理时间比较短的时候这个好处非常显著。
在这里插入图片描述

实现简单线程池思路及代码

1、创建线程池的基本结构:

任务队列链表
typedef struct Task;
线程池结构体
typedef struct ThreadPool;

2、线程池的初始化:

pool_init()
{
    创建一个线程池结构
    实现任务队列互斥锁和条件变量的初始化
    创建n个工作线程
}

3、 线程池添加任务

pool_add_task
{
    判断是否有空闲的工作线程
    给任务队列添加一个节点
    给工作线程发送信号newtask
}

4、实现工作线程

 workThread
{
   while(1){
       等待newtask任务信号
       从任务队列中删除节点
       执行任务
   }
}

5、 线程池的销毁

pool_destory
{
    删除任务队列链表所有节点,释放空间
    删除所有的互斥锁条件变量
    删除线程池,释放空间
}
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

#define POOL_NUM 10
typedef struct Task{
    void *(*func)(void *arg);
    void *arg;
    struct Task *next;
}Task;

typedef struct ThreadPool{
    pthread_mutex_t taskLock;
    pthread_cond_t newTask;
    pthread_t tid[POOL_NUM];
    Task *queue_head;
    int busywork;
}ThreadPool;

ThreadPool *pool;

void *workThread(void *arg){
    while(1){
        pthread_mutex_lock(&pool->taskLock);  //对任务队列中的头一个任务进行操作,是临界任务,防止被打扰,所以加锁
        pthread_cond_wait(&pool->newTask,&pool->taskLock);  //等待任务 (也就是等待pthread_cond_signal()函数释放新任务到达的信号) 等待的时候上锁

        Task *ptask = pool->queue_head;  //从任务队列的头中取出任务
        pool->queue_head = pool->queue_head->next;  // 指向头部的指针往后指         这里先往后指,真正用到了后面的空间的话后面去申请。

        pthread_mutex_unlock(&pool->taskLock);  //任务取完了,解锁

        ptask->func(ptask->arg); //执行任务
        pool->busywork--;  //执行完了,busywork--
    }
}

void *realwork(void *arg){
    printf("Finish work %d\n",(int)arg);
}

void pool_add_task(int arg){
    Task *newTask;
    
    pthread_mutex_lock(&pool->taskLock);
    while(pool->busywork>=POOL_NUM){  //pool->busywork 访问线程池的临界资源,前加锁
        pthread_mutex_unlock(&pool->taskLock);
        usleep(10000);
        pthread_mutex_lock(&pool->taskLock);
    }
    pthread_mutex_unlock(&pool->taskLock);
    
    //为新任务开辟内存空间,并将要执行的函数赋值给新任务
    newTask = malloc(sizeof(Task));
    newTask->func =  realwork;
    newTask->arg = arg;
    
    //上锁,将任务放到线程池中
    pthread_mutex_lock(&pool->taskLock);
    Task *member = pool->queue_head;
    if(member==NULL){
        pool->queue_head = newTask;
    }else{
       while(member->next!=NULL){
            member=member->next;
       }
       member->next = newTask;

    }
    pool->busywork++;
    //通知等待的线程,任务来啦
    pthread_cond_signal(&pool->newTask);  //条件变量被触发,等待的线程会被唤醒

    pthread_mutex_unlock(&pool->taskLock);


}


void pool_init(){
    pool = malloc(sizeof(ThreadPool));
    pthread_mutex_init(&pool->taskLock,NULL);
    pthread_cond_init(&pool->newTask,NULL);
    pool->queue_head = NULL;
    pool->busywork=0;

    for(int i=0;i<POOL_NUM;i++){
        pthread_create(&pool->tid[i],NULL,workThread,NULL);
    }
}

void pool_destory(){
    Task *head;
    while(pool->queue_head!=NULL){
        head = pool->queue_head;
        pool->queue_head = pool->queue_head->next;
        free(head);
    }

    pthread_mutex_destroy(&pool->taskLock);
    pthread_cond_destroy(&pool->newTask);
    free(pool);

}
int main(){
   pool_init();
   sleep(20);
   for(int i=1;i<=20;i++){
       pool_add_task(i);
   }
   sleep(5);
   pool_destory();
}

对于程序的理解:
首先在对线程初始化的时候就创建线程,线程的执行函数做的就是:等待任务到来,执行任务,告诉线程池任务执行了一个任务。
取任务的指针往后指。

pool_add_task函数。
为新任务开辟内存空间,并将要执行的函数赋值给新任务,将任务放到线程池中,通知等待的线程,任务来啦

以上是个人的一些学习总结和心得体会。参考了一些网络资源,文中有相应链接对应。后面还有学习心得会继续补充。新手,如有建议或写的不对的地方,欢迎讨论一下哦。欢迎交流,共同进步!

  • 8
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Linux是一种开源的操作系统内核,而CentOS则是一种基于Linux内核的发行版。下面是关于Linux和CentOS的一些笔记: 1. Linux操作系统: - Linux是一种开源操作系统内核,最初由Linus Torvalds于1991年创建。 - Linux操作系统具有高度的可定制性和灵活性,适用于各种设备,包括个人电脑、服务器、嵌入式系统等。 - Linux支持多用户、多任务和多线程,并提供了丰富的命令行工具和图形界面。 - 常见的Linux发行版包括Ubuntu、Debian、Fedora、CentOS等。 2. CentOS发行版: - CentOS是一种基于Red Hat Enterprise Linux(RHEL)源代码构建的免费发行版。 - CentOS提供了稳定、可靠且安全的操作系统环境,适用于服务器和企业级应用。 - CentOS与RHEL在软件包和功能方面基本保持一致,但不包含商业支持。 - CentOS使用yum作为软件包管理工具,可以方便地安装、更新和卸载软件包。 3. 常用命令和配置: - 在Linux和CentOS中,常用的命令有ls(列出文件和目录)、cd(切换目录)、mkdir(创建目录)、rm(删除文件或目录)等。 - 配置文件通常存放在/etc目录下,例如/etc/network/interfaces用于配置网络接口。 - Linux和CentOS支持使用vi或nano等文本编辑器来编辑配置文件。 - 通过使用sudo命令,可以以超级用户权限执行特权操作。 这些是关于Linux和CentOS的一些基本笔记。如果你有更具体的问题,我可以为你提供更详细的信息。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值