4 Linux系统编程之线程--学习笔记

1.基础知识与线程的创建、退出和等待

  1. 进程的特点:进程是资源分配的基本单位,拥有自己独立的资源,拥有自己独立的数据段、代码段、堆栈段,所以cpu切换进程的时候,需要保存较多上下文。为了解决cpu保存进程上下文所付出的较大开销,所以就引入了线程。

  2. 进程间通信方式:管道、共享内存、信号量、消息队列、信号

  3. 线程:是cpu调度的基本单位,一个进程里面至少拥有一个线程。如果在一个进程里面创建多个线程,那么这些线程共享该地址空间。线程只拥有少量的栈空间。

  4. 线程是cpu调度的最小单位、与同进程中线程共享进程的地址空间、只拥有少量的占空间、执行速 度快、同进程内切换速度快。不利于资源的管理和保护。

  5. 线程的缺点:不利于资源的管理和保护,而进程更利于资源的管理和保护的。

  6. 线程的种类

    1. 用户级线程:用户自己创建的线程

    2. 核心级线程:cpu实际运行的线程

  7. 进程:创建fork() 退出exit() 等待wait()

  8. 线程:创建pthread_create() 退出pthread_exit() 等待pthread_join()

  9. 线程库NPTL,查看方式man 7 pthreads

  10. 不要使用进程的退出方式退出线程。

  11. 线程创建

    int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                              void *(*start_routine) (void *), void *arg);
    //成功返回0,失败返回错误码
    //参数1:线程id
    //参数2:线程的属性
    //参数3:线程的处理函数
    //参数4:线程处理函数的参数
    
  12. 两个线程中打印三句话的原因(两句 i am child thread): 由于usleep时间太短了,导致子线程刚把数据输出到屏幕上,还没有来得及清空缓冲区,主线程就从usleep苏醒过来,执行return,发现缓冲区没有被清空,就刷新缓冲。

    #include <head.h>
    void* threadFunc(void* p)
    {
        printf("i am child thread\n");
        return NULL;
    }
    int main(int argc, char **argv)
    {
        pthread_t thid;
        int ret = 0;
        ret = pthread_create(&thid, NULL, threadFunc, NULL);
        THREAD_ERROR_CHECK(ret, "pthread_create");
    
        printf("i an mian thread\n");
        usleep(1);
        return 0;
    }
    
  13. 编程中尽量不要把自己申请的资源交给别人使用,会导致资源泄露,更严重的会导致死锁。

  14. 一个进程当中只有一个主线程,其它都是子线程,主线线程可以等待子线程并且回收子线程的资源,其它子线程级别相同,可以互相等待,但是它们都不能等待主线程。

  15.  #include <head.h>
     void* threadFunc(void* p)
     {
         printf("i am child thread, val = %ld\n", (long)p);
         //return NULL;
         pthread_exit(NULL);
     }
     
     int main(int argc, char **argv)
     {
         pthread_t thid, thid1;
         int ret = 0;
     
         long val = 2;
         ret = pthread_create(&thid, NULL, threadFunc, (void*)val);
         THREAD_ERROR_CHECK(ret, "pthread_create");
     
         val += 2;
         ret = pthread_create(&thid1, NULL, threadFunc, (void*)val);
         THREAD_ERROR_CHECK(ret, "pthread_create");
     
         printf("i an mian thread\n");
         /* usleep(1); */
         sleep(1);
     
         return 0;
     }
     //这种方法采用的值传递,pthread_create第四个参数需要的8个字节,所以我们只需要传递8个字节就可以了。
    
  16. return 是C语言阵营的退出方式,void pthread_exit(void *retval)它是NPTL线程的线程退出方式。

  17. 线程的等待函数 int pthread_join(pthread_t thread, void **retval)

    1. 成功返回0, 失败返回错误码

    2. 参数1:等待退出的线程id

    3. 参数2:接收子线程的退出

    4. 可以接收指针类型的退出状态

      #include <head.h>
      void* threadFunc(void* p)
      {
          printf("i am child thread\n");
          char *p1 = (char*)malloc(20);
          strcpy(p1, "success\n");
      
          //子线程的退出,并传递出一块堆空间
          pthread_exit(p1);
      }
      int main(int argc, char **argv)
      {
          pthread_t thid;
          int ret = 0;
          ret = pthread_create(&thid, NULL, threadFunc, NULL);
          THREAD_ERROR_CHECK(ret, "pthread_create");
      
          printf("i an mian thread\n");
      
          char *retVal = NULL;
          //等待子线程的退出,并回收子线程的资源
          ret = pthread_join(thid, (void**)&retVal);
          THREAD_ERROR_CHECK(ret, "pthread_join");
      
          printf("retVal = %s\n", retVal);
          free(retVal);
          retVal = NULL;
      
          return 0;
      }
      
    5. 还可以接收long类型的变量

      #include <head.h>
      void* threadFunc(void* p)
      {
          printf("i am child thread\n");
      
          long ret = 2;
          pthread_exit((void*)ret);
      }
      int main(int argc, char **argv)
      {
          pthread_t thid;
          int ret = 0;
          ret = pthread_create(&thid, NULL, threadFunc, NULL);
          THREAD_ERROR_CHECK(ret, "pthread_create");
      
          printf("i an mian thread\n");
      
          long retVal = 0;
          //等待子线程的退出,并回收子线程的资源
          ret = pthread_join(thid, (void**)&retVal);
          THREAD_ERROR_CHECK(ret, "pthread_join");
      
          printf("retVal = %ld\n", retVal);
      
          return 0;
      }
      
  18. 线程的退出 void pthread_exit(void *retval)

  19. 查看线程相关信息的命令:ps -elLf

  20. 查看进程相关信息的命令:ps -elf

2.线程的取消

  1. 线程可以自己调线程退出函数退出,也可以被其它线程杀掉,被其它线程杀掉的方式就叫线程的取消。

  2. 被杀掉的线程可以选择自己如何处理这个信号,忽略、立刻终止、运行至取消点后终止。

  3. 什么是取消点?

    取消其实就是一些特殊函数:一般阻塞函数都是取消点,非阻塞性函数也可能是取消点。

  4. 线程取消函数 int pthread_cancel(pthread_t thread)

    1. 成功返回0, 失败返回错误码
    2. 参数:想要cancel线程的id
  5. ps -elLf看到的线程id是核心级线程的id,pthread_create()传出的参数thid是用户级线程的id

  6. 获取线程id的函数pthread_t pthread_self(void)

    永远不会失败,成功返回线程id

  7. 线程取消函数: 另外一个线程收到cancel信号默认行为是运行完取消点才结束,可以通过pthread_setcancelstate()函数进行设置修改默认行为。

  8. 一般不使用cancel进行线程取消,因为会造成内存的泄露,如子线程内在堆上申请了内存,取消点在释放内存的前面,当主线程杀子线程时,内存不会释放,造成内存泄露。

3.线程终止清理函数

  1. pthread_cleanup_push(), pthread_cleanup_pop(); 这是一个函数对,管理是一个栈结构,调用push的时候就是把清理函数入栈,调用pop的时候,清理函数出栈。

  2. void pthread_cleanup_push(void (*routine)(void *), void *arg);

    1. 参数1:清理函数
    2. 参数2:清理函数的参数
  3. void pthread_cleanup_pop(int execute);

    1. 如果填非0值,代表清理函数出栈,后进先出
    2. 如果填0,表示清理函数不出栈
  4. 使用注意事项:

    1. 必须成对使用
    2. 必须在代码的同一层次使用
  5. 在清理函数对中间的代码如果意外退出, 执行清理函数才是正确方式。

  6. 线程清理函数得到相应的时机:三个时机

    1. 线程被cancel掉
    2. 显式的弹栈pop(1)
    3. pthread_exit() //只要pthread_clean_pop()写在pthread_exit()的后面,即使是pthread_cleanup_pop(0)也会调用,当线程pthread_exit()退出的时候,清理函数栈中还有函数,且标志位为1,还未被清理函数pthread_clearnup_pop(0)置为0,故清理函数弹栈执行。
  7. 线程清理函数不会相应的时机:两个

    1. pop(0)
    2. return ;
  8. cancel代码当中尽量不要使用。

  9. push放在紧邻需要清理的资源下面

  10. pop尽量往后放。

  11. 对于 man 不了线程的相关函数需要增加 man 信息 , apt-get install manpages-posix-dev

  12. #include <head.h>
    void cleanFunc(void* p) 
    {
        printf("cleanFunc\n");
        free(p);
        p = NULL;
    }
    void *threadFunc(void *p)
    {
        char *p1 = (char*)malloc(20);
        strcpy(p1, "hello");
        //线程清理函数入栈
        pthread_cleanup_push(cleanFunc, p1);
    
        printf("p1 = %s\n", p1);
    
        free(p1);
        p1 = NULL;
        printf("free success\n");
        
       	pthread_cleanup_pop(0);
        //线程清理函数出栈
        pthread_exit(NULL);
    }
    
    int main(int argc, char **argv)
    {
        int ret = 0;
        pthread_t thid;
    
        ret = pthread_create(&thid, NULL, threadFunc, NULL);
        THREAD_ERROR_CHECK(ret, "pthread_create");
    
        //线程的取消
        ret = pthread_cancel(thid);
        THREAD_ERROR_CHECK(ret, "pthread_cancel");
        printf("i am main thread\n");
    
        pthread_join(thid, NULL);
        return 0;
    }
    

4.线程互斥

  1. 对资源的独占式访问

  2. 线程锁pthread_mutex_t mutex称为互斥锁或者互斥体。访问共享资源的时候,需要加锁对资源进行访问,访问完成之后解锁。如果已经有锁锁住该资源,那么就阻塞,直到该资源被释放。

  3. 锁的初始化

    1. 静态方式

       pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
      
    2. 动态的方式

      int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr) 
      //成功返回0, 失败返回错误码 
      //参数1:需要初始化的锁变量 
      //参数2:锁的属性,通常为NULL
      
  4. 锁的使用

    1. 加锁

      int pthread_mutex_lock(pthread_mutex_t *mutex); 
      //成功返回0, 失败返回错误码 
      //参数:需要加锁的变量 
      //阻塞型函数
      
    2. 解锁

      int pthread_mutex_unlock(pthread_mutex_t *mutex); 
      
    3. 锁的销毁

      int pthread_mutex_destroy(pthread_mutex_t *mutex);
      //成功返回0,失败返回错误码
      
    4. 测试加锁

      int pthread_mutex_trylock(pthread_mutex_t *mutex);
      //它是非阻塞性函数,如果一把锁正在使用,那么使用trylock不会加锁加锁成功,也不会阻塞,会返回错误码。如果这把锁没有被使用,那么trylock的行为和lock一样。 
      
  5. 锁的注意事项

    1. 不能对一把正在使用的锁进行销毁操作
    2. 加锁一次:锁的引用值+1;解锁时,引用计数-1。锁的引用计数为0的时候,锁才能被销毁。所以,对一把锁加锁1次,连续解锁两次,destroy不会成功。 对一把锁加锁一次、解锁两次,再加锁一次,再destroy会执行成功。
    3. pthread_mutex_lock是一个阻塞性函数。连续加锁会阻塞(哪怕先解锁,引用计数变成-1,再两次加锁)。
    4. 自死锁:自己连续加锁两次 ; abba锁
  6. 互斥锁的属性

    1. 普通锁、快速锁,pthread_mutex_init(&mutex, NULL);

    2. 嵌套锁:可以对一把锁连续加锁多次

      pthread_mutexattr_t attr;
      pthread_mutexattr_init(&attr);
      
      //线程锁属性类型设置函数
      int pthread_mutexattr_settype(pthread_mutexattr_t *attr, int type);
      //参数1:设置锁的属性变量
      //参数2:需要设置的类型
      //嵌套锁type填:PTHREAD_MUTEX_RECURSIVE
      
    3. 检错锁:它可以检测最简单的自死锁, 还可以检测多余的解锁。

      pthread_mutexattr_t attr;
      pthread_mutexattr_init(&attr);
      
      //线程锁属性类型设置函数
      int pthread_mutexattr_settype(pthread_mutexattr_t *attr, int type);
      //type填PTHREAD_MUTEX_ERRORCHECK设置检错锁
      
  7. 对于不同属性的锁,加锁和解锁的各自表现

    1. 加锁:
      1. 对于普通锁:不允许连续加锁
      2. 对于嵌套锁:可以连续加锁多次
      3. 对于检错锁:连续加锁、连续解锁会返回错误码
    2. 解锁:
      1. 对于普通锁:解锁者可以是同进程内任意线程
      2. 对于嵌套锁:帮助文档要求必须由加锁者解锁,但是同进程的任意线程也可以解锁
      3. 对于检错锁:解锁者必须由加锁者解锁
  8. 死锁的概念:指多个进程因竞争资源而造成的一种僵局。若无外力的干预,这些进程将无法向前推进

  9. 死锁产生的原因:1.系统资源的竞争 2.进程的推进顺序不合理

  10. 死锁产生的必要条件 1.互斥条件 2.不可剥夺条件 3. 循环等待 4.请求与保持

  11. 死锁的避免:破坏四个必要条件之一即可。

5.线程的同步

  1. 什么是同步:两个或两个以上随时间变化的量,在变化的过程当中保持一定的相对关系。

  2. 线程实现同步的方式是条件变量。

  3. pthread_cond_t cond

  4. 条件变量的初始化:

    1. 动态方式

      int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr)
      //参数1:待初始化的条件变量
      //参数2:条件变量的属性,一般都填NULL
      
    2. 静态方式

      pthread_cond_t cond = PTHREAD_COND_INITIALIZER
      
  5. 条件变量的销毁:只有在条件变量没有被使用的时候,才能被销毁

    int pthread_cond_destroy(pthread_cond_t *cond) 
    
  6. 条件变量的等待:

    1. 无条件等待

      int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
      //参数1:条件变量 
      //参数2:互斥锁 
      
    2. 计时等待:等待一段时间,如果条件没有成立,就返回

      int pthread_cond_timedwait(pthread_cond_t *restrict cond,
                 pthread_mutex_t *restrict mutex,
                 const struct timespec *restrict abstime);
      
  7. 函数的上下半部:指的是函数的行为发生了改变

    1. 对比买酒-卖酒模型(先在外面手动加锁进入商店):pthread_cond_wait的上半部:商店里没有酒,在登记表登记,解锁出商店,回家睡觉

      下半部: 商店老板通知顾客商店有酒了,顾客睡醒,加锁进入商店买酒,删除登记本上的信息(然后在外面手动解锁)。

    2. 对比代码:pthread_cond_wait的上半部:1.条件变量上排队,2.解锁 3.睡眠;

      下半部:1.被唤醒 2.加锁(如果此时该锁没有被使用,那么就加锁成功, 如果此时这个锁正在被使用的,会阻塞,直到该锁被释放,加锁) 3.pthread_cond_wait函数返回。

  8. 条件变量的激发:

    1. 每次激发一个条件变量

      int pthread_cond_signal(pthread_cond_t *cond) 
      
    2. 激发所有条件变量

      int pthread_cond_broadcast(pthread_cond_t *cond) 
      
  9. 宏定义的时候do-while(0)的意义是:防止该宏定义写在判断语句的后面,而该判断语句没有加括号。

  10. 买票卖票例子

    #include <funcl.h>
    #define N 10
    #define Round 3
    typedef struct data_s{
        int num;
        int round_s;
        pthread_cond_t cond;
        pthread_cond_t cond1;
        pthread_mutex_t mutex;
    }data_t;
    void *setTickets(void *p){
        data_t *data = (data_t *)p;
        for(int i=0;i<Round;i++){
            pthread_mutex_lock(&data->mutex);
            pthread_cond_wait(&data->cond,&data->mutex);
            data->num = N;
            data->round_s--;
            printf("%d time setTicks\n",i+1);
            pthread_cond_broadcast(&data->cond1);
            pthread_mutex_unlock(&data->mutex);
        }
        pthread_exit(NULL);
    }
    void *sale_Win1(void *p){
        data_t *data = (data_t *)p;
        while(1){
            if(data->num == 0 && data->round_s == 0){
                printf("Win1 no tickets,quit!\n");
                pthread_exit(NULL);
            }
            pthread_mutex_lock(&data->mutex);
            if(data->num > 0){
                --data->num;
                printf("Win1 sales a ticket\n");
            }
            else{
                pthread_cond_signal(&data->cond);
                pthread_cond_wait(&data->cond1,&data->mutex);
            }
            pthread_mutex_unlock(&data->mutex);
        }
        pthread_exit(NULL);
    }
    void *sale_Win2(void *p){
        data_t *data = (data_t *)p;
        while(1){
            if(data->num == 0 && data->round_s == 0){
                printf("Win2 no tickets,quit!\n");
                pthread_exit(NULL);
            }
            pthread_mutex_lock(&data->mutex);
            if(data->num > 0){
                --data->num;
                printf("Win2 sales a ticket\n");
            }
            else{
                pthread_cond_signal(&data->cond);
                pthread_cond_wait(&data->cond1,&data->mutex);
            }
            pthread_mutex_unlock(&data->mutex);
        }
    }
    int main(int argc,char*argv[])
    {
        int ret = 0;
        pthread_t thid,thid1,thid2;
        data_t data;
        //初始化
        data.num = 0;
        data.round_s = Round;
        ret = pthread_mutex_init(&data.mutex,NULL);
        THREAD_ERROR_CHECK(ret,"pthread_mutex_init");
        ret = pthread_cond_init(&data.cond,NULL);
        THREAD_ERROR_CHECK(ret,"pthread_cond_init");
        ret = pthread_cond_init(&data.cond1,NULL);
        THREAD_ERROR_CHECK(ret,"pthread_cond_init 1");
    
        //线程创建
        ret = pthread_create(&thid,NULL,setTickets,(void *)&data);
        THREAD_ERROR_CHECK(ret,"pthread_create 0");
        ret = pthread_create(&thid1,NULL,sale_Win1,(void *)&data);
        THREAD_ERROR_CHECK(ret,"pthread_create 1");
        ret = pthread_create(&thid2,NULL,sale_Win2,(void *)&data);
        THREAD_ERROR_CHECK(ret,"pthread_create 2");
        //线程接收
        ret = pthread_join(thid1,NULL);
        THREAD_ERROR_CHECK(ret,"pthread_join 1");
        ret = pthread_join(thid2,NULL);
        THREAD_ERROR_CHECK(ret,"pthread_join 2");
        ret = pthread_join(thid,NULL);
        THREAD_ERROR_CHECK(ret,"pthread_join 0");
    
        pthread_mutex_destroy(&data.mutex);
        pthread_cond_destroy(&data.cond);
        return 0;
    }
    

6.线程安全

  1. 线程安全:如果一个函数能够安全的同时被多个线程调用而得到正确的结果

  2.  //线程不安全的函数
     char *ctime(const time_t *timep);
     //ctime()函数返回的是静态全局区的地址,不同线程调用ctime函数会将结果相互覆盖
     
     //线程安全函数
     char *ctime_r(const time_t *timep, char *buf) 
     //参数2:保存当前调用ctimr_r函数所生成的时间字符串的内存区,不同线程调用时传入的地址不同,所以保存时间字符串的位置不同,所以线程安全
    
  3. 可重入函数 : 简单来说就是可以被中断的函数 (或者说,该函数被中断后重新载入(调用)该函数能够正确执行)

  4. 线程安全函数和可重入函数的关系: 可重入函数一定是线程安全函数,线程安全函数不一定时可重入函数。

  5. 线程的属性(pthread_create 的第二个参数 attr 是一个结构体指针,结构中的元素分别指定新线程的运行属性,通过该结构体的不同填法,可以设置不同的线程属性)

  6. 设置线程分离属性(表示新线程是否与进程中其他线程脱离同步,如果设置该属性则新线程不能用 pthread_join()来等待,且在退出时自行释放所占用的资源)

    1. 主线程设置子线程的分离属性

      pthread_attr_t attr; 
      pthread_attr_init(&attr); 
      int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate); 
      //参数2:设置为PTHREAD_CREATE_DETACHED是线程脱离同步,默认是 PTHREAD_CREATE_JOINABLE
      
    2. 子线程设置自己为分离属性:pthread_detach(pthread_self());

  7. 设置绑定属性的函数为 pthread_attr_setscope()

    设置分离属性的函数是 pthread_attr_setdetachstate()

    设置线程优先级的相关函数pthread_attr_getscehdparam()(获取线程优先级)和 pthread_attr_setschedparam()(设置线程优先级)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值