linux的多线程

以下是阿鲤对Linux下线程的总结,希望对大家有所帮助;若有误请慷慨指出。 1:线程概念 2:线程控制 2:线程安全 4:死锁 注:以下的代码实现均为centos7环境; 一:线程概念 1:线程的介绍:

以下是阿鲤对Linux下线程的总结,希望对大家有所帮助;若有误请慷慨指出。

1:线程概念

2:线程控制

2:线程安全

4:死锁

注:以下的代码实现均为centos7环境;


 

一:线程概念

1:线程的介绍:

     在传统操作系统中对程序的描述方式分为pcb和tcb即进程和线程;而在Linux下其进程和线程均是通过pcb进行描述的;pcb:是一个文件描述信息,其使用虚拟地址空间对内存进行访问;在这里就有了进程和线程的区别。一个进程拥有一个虚拟地址空间,而多个线程拥有一个虚拟地址空间;可以这么理解,多个线程(一个线程组)构成一个进程;所以说Linux下没有真正的线程。Linux下的线程是轻量级进程。图解如下:

所以进程是系统资源分配的基本单位,而线程是cpu调度的基本单位

2:线程的独有与共享:

独有:栈(函数调用),寄存器(程序信息),信号屏蔽字(pcb中的阻塞信号),errno(错误信息),标识符(存储在共享区,在创建中使用tid返回首地址);

共享:虚拟地址空间(代码段,数据段),文件描述符表(可以减少文件打开次数),信号处理方式(信号是针对进程的),工作路径,用户ID,组ID

3:多进程与多线程多任务处理比较

多线程特点:

     1:线程间通信很方便(包括进程间通信+全局变量等);

     2:线程的创建与销毁成本低;

     3:线程间的调度成本低;

     4:指向粒度更加细腻

多进程特点:

     1:具有独立性,因此更加的稳定,健壮;

共同特点:

     1:并行压缩cpu处理/IO等待时间

所以对主功能程序安全稳定性要求高的最好使用多进程,剩下的使用多线程。

二:线程控制

注:Linux下线程控制的接口都是库函数(操作系统没有向用户提供一个轻量级进程接口,因此大佬们就对进程控制的接口进行了封装,从而封装出线程控制库函数)

1:线程创建

接口:

int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);

     *thread:用于获取线程ID,通过这个ID可以找到线程描述信息,进而访问pcb(线程的首地址,存储在虚拟地址的共享区);

     attr:线程熟悉,通常NULL;

     start_routine:线程入口函数,创建的线程就是为了运行这个函数,函数运行完毕,线程退出;

     arg:通过线程入口函数,传给线程的参数(通信)

     return:0-成功, 非0值-失败

eg:

#include<stdio.h>    
#include<stdlib.h>    
#include<pthread.h>    
#include<unistd.h>                                                                                                                                                                           
    
void *func(void *arg)    
{    
    while(1)    
    {    
        printf("The main thread passed me a parameter:%d\n",*((int*)(arg)));    
        sleep(1);    
    }    
    return NULL;    
}    
    
int main()    
{    
    int tmp = 888;    
    pthread_t tid;    
    int ret =  pthread_create(&tid, NULL, func, (void*)&tmp);    
    if(ret != 0)    
    {    
        printf("thread create error: %d\n", ret);    
        return -1;    
    }    
    while(1)    
    {    
        printf("I am main thread\n");    
        sleep(1);    
    }    
    return 0;    
}  

注意:因为是库函数,所以需要使用-l进行库的链接;

线程查看:

如上图使用ps -efL | head -n 1 && ps -efL | grep create是查看线程的命令(其中L选型为轻量级进程);其中LWP是线程ID,PID是进程ID;所以我们可以看出来进程ID是主线程ID。

2:线程终止

终止方式

普通线程入口函数中的return(mian函数中的return退出的是进程);

void pthread_exit(void *retval);退出一个线程,谁调用谁退出   retval-线程返回值;

int pthread_cancel(pthread_t thread);取消一个指定的线程;tid:指定的线程id;

注意:

线程退出也不会完全释放资源,需要被其他线程等待;

使用int pthread_cancel(pthread_t thread);取消自己是一个违规操作;

主线程退出,其他线程正常运行,这也不是主流做法

主线程退出,并不影响整个进程的运行,只有所有线程退出,进程才会退出。

3:线程等待

等待一个线程的退出,获取退出线程的返回值,回收这个线程所占用的资源

接口:

int pthread_join(pthread_t thread, void **retval);

     thread:等待线程的id

     retval:用于获取线程退出的返回值

     return:0-成功, 非0值-失败

eg:

  #include<stdio.h>    
  #include<stdlib.h>    
  #include<pthread.h>    
  #include<unistd.h>    
      
  void *func(void *arg)    
  {    
      sleep(3);    
      char *buf = "BelongAl";                                                                                                                                                                
      pthread_exit(buf);    
      return NULL;    
  }    
      
  int main()    
  {    
      pthread_t tid;    
      int tmp;    
      
      int ret =  pthread_create(&tid, NULL, func, (void*)&tmp);    
      if(ret != 0)    
      {    
          printf("thread create error: %d\n", ret);    
          return -1;    
      }    
      
      void *retval = NULL;    
      pthread_join(tid, &retval);    
      printf("%s\n",(char*)retval);    
      return 0;    
  }

注:不是所有的线程都能被等待,一个线程被创建,默认情况下有一个属性joinable;处于joinable属性的线程退出后,不会被自动释放,需要等待;

4:线程分离

将线程的属性从joinable设置为detach;处于detach属性的线程退出后会自动释放资源,不需要被等待。

接口:

int pthread_detach(pthread_t thread);

     thread:指定线程id

     return:0-成功, 非0值-失败

注:

等待一个被分离的线程,则pthread_detach会返回错误:这不是一个joinable线程(因为在获取返回值时将获取不到,detach属性的线程退出后已经自动的释放了资源)。

线程的分离可以在任意地方,可以在线程入口函数中让线程分离自己,也可以让创建线程在创建之后直接分离。

三:线程安全

1:概念:在多个执行流中对同一个临界资源进行操作访问,而不会造成数据二义;

2:方法

互斥:通过保证同一时间只有一个执行流可以对临界资源进行访问,来保证数据访问的安全性。

同步:通过一些条件判断来实现多个执行流对临界资源访问的合理性(有资源则访问,无资源则等待直到有资源被唤醒)。

3:互斥的实现:互斥锁

1:第一个线程访问时,判断可以访问,因此将状态置为不可访问(计数器置为0),然后去访问资源

2:其他线程访问的时候,发现不可访问,就陷入等待(将线程置为可中断休眠状态)

3:第一个线程访问临界资源完毕后,将状态置为可以访问(计数器置为1),唤醒等待的线程(将线程置为运行状态),被唤醒的进程开始竞争这个资源

计数器原子性操作原理:我们可以发现,每一个线程都可以对计数器进行修改,那么只有计数器修改操作是原子性的,才可以保证线程安全;那么计数器是怎样实现原子性呢?请看下图

互斥锁操作接口:

int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr):锁的初始化      

int pthread_mutex_lock(pthread_mutex_t *mutex);加锁(阻塞)

int pthread_mutex_trylock(pthread_mutex_t *mutex);尝试加锁(非阻塞)

int pthread_mutex_unlock(pthread_mutex_t *mutex);解锁

 int pthread_mutex_destroy(pthread_mutex_t *mutex);销毁锁

注:pthread_mutex_t:互斥锁变量类型;attr:互斥锁属性

eg:

  #include<stdio.h>    
  #include<stdlib.h>    
  #include<unistd.h>    
  #include<pthread.h>    
      
  int tickets = 100;//表示有100张票    
  pthread_mutex_t mutex;    
      
  void *thr_tout(void *arg)    
  {    
      while(1)    
      {    
          pthread_mutex_lock(&mutex);//阻塞加锁    
          if(tickets > 0)    
          {    
              printf("tout:%p - get a ticket:%d\n",pthread_self(), tickets);    
              tickets--;    
              pthread_mutex_unlock(&mutex);//解锁    
          }    
          else    
          {    
              pthread_mutex_unlock(&mutex);                                                                                                                                                  
              pthread_exit(NULL);//线程退出    
          }    
      }    
      return NULL;    
  }    
      
  int main()    
  {    
      int i = 0, ret;    
      pthread_t tid[4];    
      //互斥锁初始化    
      //pthread_mutex_init() 或 pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER    
      pthread_mutex_init(&mutex, NULL);    
      
      for(; i < 4; i++)    
      {    
          ret = pthread_create(&tid[i], NULL, thr_tout, NULL); 
          if(ret != 0)
          {
              printf("thread cerat error\n");
              return -1;
          }
      }
  
      for(i = 0; i < 4; i++)//线程等待
      {
          pthread_join(tid[i], NULL);
      }
  
      pthread_mutex_destroy(&mutex);
  
      return 0;
  }                            

4:同步的实现:条件变量

有资源的时候可以获取,没有资源的时候则需要让线程等待,等待被唤醒(其他线程产生一个资源的时候)

条件变量:向用户提供了两个接口,使一个线程等待接口和唤醒一个线程接口+等待队列;条件变量只是提供了等待与唤醒的功能,但是什么时候等待,什么时候唤醒,需要用户自己来做判断

操作接口:

pthread_cond_t :条件变量类型

int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);//初始化

     cond:条件变量

     attr:条件变量属性

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;//初始化

int pthread_cond_destroy(pthread_cond_t *cond);//销毁

int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);//使当前执行流等待,加入等待队列

int pthread_cond_signal(pthread_cond_t *cond);//唤醒至少一个等待队列中的执行流

int pthread_cond_broadcast(pthread_cond_t *cond);//唤醒所有等待队列中的执行流

eg:吃面做面模型

  #include<stdio.h>                                                                                                                                                                          
  #include<stdlib.h>    
  #include<unistd.h>    
  #include<pthread.h>    
      
  //定义两个条件变量是防止在唤醒时唤醒错误    
  pthread_cond_t gourment_cond;//定义一个美食家变量    
  pthread_cond_t chief_cond;//定义一个厨师变量    
      
  int have_delicacy = 0;//刚开始并没有美食    
  pthread_mutex_t mutex;//定义一个互斥锁    
      
  void *gourment(void *arg)//美食家函数    
  {    
      while(1)    
      {    
          pthread_mutex_lock(&mutex);//加锁    
          while(have_delicacy == 0)/*如果没有美食则需要等待,但是在等待前需要先解锁//循环的原因为 pthread_cond_signal函数是唤醒至少一个,为了防止争抢混乱所采用的。例如在厨师做了美食之后,唤醒了一个或多个美食家若不采用循环,只有一个美食家会进入而其他美食家会陷入锁的阻塞,在这个美食家吃完之后会解锁然后再唤醒厨师,但是再唤醒厨师之前,那些阻塞再锁前的美食家可能会先行加锁但是因为不是循环等待,则会导致休眠之后解锁直接去吃美食,导致逻辑混乱 */   
          {    
              pthread_cond_wait(&gourment_cond, &mutex);//等待函数包括:解锁-》休眠-》加锁    
          }    
          printf("really delicious\n");    
          have_delicacy--;    
          pthread_mutex_unlock(&mutex);  //解锁  
          pthread_cond_signal(&chief_cond);  //唤醒厨师  
      }    
      return NULL;    
  }    
      
  void *chief(void *arg)//厨师长函数    
  {    
      while(1)    
      {    
          pthread_mutex_lock(&mutex);//加锁    
          while(have_delicacy == 1)//如过做好了没有人吃则陷入等待    
          {    
              pthread_cond_wait(&chief_cond, &mutex);    
          } 
          printf("I made a bowl of Buddha jumping over the wall\n");
          have_delicacy++;
          //做出没事之后唤醒等待的美食家
          pthread_mutex_unlock(&mutex);//解锁
          pthread_cond_signal(&gourment_cond);//唤醒美食家
      }
      return NULL;
  }
  
  int main()
  {
      pthread_t gourment_tid, chief_tid;
      int ret;
  
      pthread_cond_init(&chief_cond, NULL);//初始化厨师条件变量
      pthread_cond_init(&gourment_cond, NULL);//初始化美食家条件变量
      pthread_mutex_init(&mutex,NULL);//初始化互斥锁
  
      int i = 0;
      for(i = 0; i < 4; i++)
      {
          ret = pthread_create(&gourment_tid, NULL, gourment, NULL);//创建美食家线程
          if(ret != 0)
          {
              printf("pthread create error\n");
              return -1;
          }
      }
   
      for(i = 0; i < 4; i++)    
      {    
          ret = pthread_create(&chief_tid, NULL, chief, NULL);//创建厨师线程    
          if(ret != 0)    
          {    
              printf("pthread create error\n");    
              return -1;    
          }    
      }    
      
      pthread_join(gourment_tid, NULL);
      pthread_join(chief_tid, NULL);
  
      pthread_cond_destroy(&chief_cond);//销毁厨师条件变量
      pthread_cond_destroy(&gourment_cond);//销毁美食家条件变量
      pthread_mutex_destroy(&mutex);//销毁互斥锁
  
      return 0;
  }          

4:同步的实现:信号量(POSIX标准)(也可实现互斥)

本质:计数器+等待队列+向外提供的使执行流阻塞/唤醒的功能接口。通过对资源进行计数,统计当前资源数量,通过自身的计数,就可以进行条件判断,是否能够进行操作,若不能获取资源,则阻塞当前执行流。在程序初始化阶段,根据实际资源数量初始化信号量计数器,在每次获取资源之前先获取信号量(先去判断计数器是否小于0,若大于0,则计数-1,直接返回,获取数据,否则阻塞当前执行流;其他执行流生产一个资源后,先判断计数器是否<0,若小于0,则唤醒一个执行流,然后计数器+1)

接口:

sem_t sem   : 信号量类型

 int sem_init(sem_t *sem, int pshared, unsigned int value);

     sem:信号量ID

     pshared:这个参数决定了当前信号量适用于进程间还是线程间;0-线程间/非0-进程间

     value:实际资源数量,用于初始化信号量计数器初值

int sem_wait(sem_t *sem);//阻塞等待

int sem_trywait(sem_t *sem);//尝试等待

int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);//等待一段时间

int sem_post(sem_t *sem);//唤醒操作

int sem_destroy(sem_t *sem);//销毁

eg:生产者与消费者模型

#include<iostream>
#include<semaphore.h>
#include<vector>

#define MAX_QUEUE 5

class ringQueue
{
    std::vector<int> m_array;
    int m_capacity;
    int m_pos_write;//写指针
    int m_pos_read;//读指针
    sem_t m_consumer_sem;//数据资源计数器,消费者计数器
    sem_t m_product_sem;//空闲空间计数器,生产者计数器
    sem_t m_sem_lock;//锁

public:
    ringQueue(int capacity = MAX_QUEUE):
        m_capacity(capacity),
        m_pos_read(0),
        m_pos_write(0),
        m_array(MAX_QUEUE)
    {
        sem_init(&m_consumer_sem, 0, 0);//数据资源初始化
        sem_init(&m_product_sem, 0, MAX_QUEUE);//空闲时间初始化
        sem_init(&m_sem_lock, 0, 1);//锁的初始化
    }

    ~ringQueue()
    {
        sem_destroy(&m_product_sem);
        sem_destroy(&m_consumer_sem);
        sem_destroy(&m_sem_lock);
    }

    bool push(int &data)
    {
        //没有空间空间则直接阻塞,并且空闲空间计数-1;
        sem_wait(&m_product_sem);

        sem_wait(&m_sem_lock);//加锁,保护入队操作
        m_array[m_pos_write++] = data;
        if(m_pos_write == m_capacity)
        {
            m_pos_write = 0;
        }
        sem_post(&m_sem_lock);//解锁,

        sem_post(&m_consumer_sem);//资源计数器+1,唤醒消费者
        return true;
    }

    bool pop(int *data)
    {
        //通过资源计数器判断是否否能获取资源,资源计数器-1
        sem_wait(&m_consumer_sem);

        sem_wait(&m_sem_lock);//加锁
        *data = m_array[m_pos_read++];
        if(m_pos_read == m_capacity)
        {
            m_pos_read = 0;
        }
        sem_post(&m_sem_lock);//解锁

        sem_post(&m_product_sem);//空闲计数器+1,唤醒生产者
        return true; 
    }

};

void *product(void* arg)
{
    ringQueue *rq = (ringQueue*)arg;
    int i = 0;
    while(1)
    {
        rq->push(i);
        std::cout << "productor: " << pthread_self() << "put data: " << i++ << std::endl; 
    }
    return NULL;
}

void *consumer(void *arg)
{
    int data;
    ringQueue *rq = (ringQueue*)arg;
    while(1)
    {
        rq->pop(&data);
        std::cout << "consumer: " << pthread_self() << "get data: " << data << std::endl; 
    }
    return NULL;
}

#define MAX_THR 4

int main()
{
    int ret, i;
    pthread_t ptid[MAX_THR], ctid[MAX_THR];

    ringQueue rq;

    for(i = 0; i < MAX_THR; i++)
    {
        ret = pthread_create(&ptid[i], NULL, product, (void*)&rq);
        if(ret != 0)
        {
            std::cout << "thread create error" << std::endl; 
            return -1;
        }
    }

    for(i = 0; i < MAX_THR; i++)
    {
        ret = pthread_create(&ctid[i], NULL, consumer, (void*)&rq);
        if(ret != 0)
        {
            std::cout << "thread create error" << std::endl; 
            return -1;
        }
    }

    for(i = 0; i < MAX_THR; i++)
    {
        pthread_join(ptid[i], NULL);
        pthread_join(ctid[i], NULL);
    }

    return 0;
}

四:死锁

1:概念:多个执行流在对多个锁资源进行争抢操作,但是因为推进不当,而导致互相等待,流程无法继续推进的情况。

2:死锁产生的四个必要条件

     1,互斥条件:一个锁只有一个人能加,我加了锁,别人就不能加了

     2,不可剥夺条件:我加的锁,别人不能替我释放

     3,请求与保持条件:我加了A锁,然后去请求B锁,但是请求不到B锁,我也不释放A锁

     4,环路等待条件:eg:有甲乙两人,AB两锁;甲持有A锁,乙持有B锁,;甲请求B锁,乙请求A锁;在满足条件三的情况下,形成了环路等待

3:死锁的预防

即破坏产生死锁的四个必要条件

     1:锁资源按序一次性分配

     2:加锁的时候可以使用非阻塞加锁,若无法加锁,则将手中的其他锁释放掉

4:死锁的避免

     1:银行家算法:定义三张表:现在有多少钱(现在都有那些锁);现在那些人已经借了钱(当前那些执行流已经获取了锁);当前还有那些人需要借多少钱(当前那些执行流想要那些锁);

           若给一个执行流分配指定的锁有可能会造成环路等待(非安全状态),则不予分配,并且回溯释放当前执行流已有的资源。

     2:死锁监测算法

 

知秋君
上一篇 2024-08-12 09:12
下一篇 2024-08-12 08:48

相关推荐