Liunx 之高性能编程:多线程 & 多进程 & 进程池 & 线程池 & 线程思想

1、多进程 & 多线程

思想:

   父进程(或主线程)负责监控,并接收客户连接(accept)
   fork创建子进程(或函数线程),与一个客户端交互(recv , send)

注意:

多进程:
    1.父子进程之间共享文件描述符,所以父进程不需要将接收连接的文件描述符传递给子进程;
    2.父进程要关闭连接的文件描述符;
   原因:
    1.父进程不关闭文件描述符,则后续创建的子进程会将所有的文件描述符继承下来;
    2.父进程不关闭文件描述符,则后续的连接的文件描述符不断增大,连接的客户端
      的数量就受一个进程最多打开的文件的限制;
   缺陷:
     1.创建一个进程,为一个客户端交互完成后,线程也随之结束,会造成服务器系统负担;
     2.如果客户端很多,则服务器创建的子进程也会很多,并且大部分子进程都会阻塞在recv操作;
      

多线程:      
      1.主线程接收连接,连接的文件描述符必须通过 创建函数线程时值 传递给函数线程;
      2.主线程 不能 关闭文件描述符;

多线程特点:
     1.创建线程资源消耗相对较小;
     2.线程之间数据共享更容易;
     3.线程结束释放资源比较少;

与多进程编程相比:
     1.创建多进程会消耗大量的系统资源;
     2.如果子进程在很短的时间内结束,系统的负担会加重;

2、线程池 &进程池

池:初始时,申请比刚开始要使用的资源 大的多 的资源空间,
    接下来使用时,直接从池中获取资源;

进程池

原因:
   1.系统能够创建的进程或则一个进程中能够创建的线程都是有限的;
   2.为一个客户端连接创建一个进程或线程,客户端断开则销毁 是不划算的;
     所以,我们用线程池来改善问题。

思想:

  服务器启动则创建n(固定值,有限个)个子进程或函数线程,
  将这n个子进程或函数线程用池管理起来,服务器终止时销毁, 
  当有客户端连接时,从线程池中分配一个子进程或线程为其服务,
  服务完成以后以后,服务器就将子进程或线程又放回池中,
  继续等待分配下一个客户端;

线程池的实现:

1.主线程 执行先创建 3 条线程;
2.主线程 等待客户连接,3 条函数线程因为 信号量的 P 操作阻塞运行;
3.主线程接收到客户端连接后,通过 信号量 的 V 操作 通知一个函数线程和客户端通讯;
    
    全局数组作为 等待函数线程处理的 文件描述符 的等待队列

线程池 & 线程池 相比多线程的优势

1.创建的进程或者线程是有限的,服务器的系统代价比较小,一般不会达到系统限制的值;
2.服务器不需要频繁的创建、销毁进程或线程,只在服务器启动时创建,结束时销毁;
3.创建的进程或者线程不是为一个客户端服务,可以串行为多个客户端服务;
4.客户端连接上以后,不需要再去创建进程或线程,
   只需要分配进程池或线程池中的进程或线程,对客户端的速度就能快一些;

3、线程编程

     1.线程的概念
     (程序:磁盘上存储的二进制可执行文件;)
     (进程的概念:运行(加载到内存)中的程序;一组有序指令+数据+资源的集合;)
      线程:进程内部的一条执行序列(执行流),一个进程可以包含多条线程,
        至少会有一条线程(main函数所代表执行序列,主线程)
        通过函数库创建线程 ---> 函数线程,一个进程中的所有线程都是并发执行的;
        
     2.线程与进程的区别
          1、线程是执行的最小单位、CPU调度的最小单位;进程是资源分配的最小单位;
          2、一个进程中可以包含多条线程,进程是独立的执行个体;线程是进程内部的执行序列;
          3、进程切换效率低,代价大;线程切换效率高,代价小(指令小)

   {   协程是一种用户态的轻量级线程,即协程是由用户程序自己控制调度的;
       优点如下:
        1.协程的切换开销更小,属于程序级别的切换,操作系统完全感知不到,因而更加轻量
        2.单线程内就可以实现并发的效果,最大限度地利用cpu
       缺点如下:
         1.协程的本质是单线程下,无法利用多核,可以是一个程序开启多个进程,
              每个进程内开启多个线程,每个线程内开启协程;
         2.协程指的是单个线程,因而一旦协程出现阻塞,将会阻塞整个线程;
       总结协程特点:
           1.必须在只有一个单线程里实现并发
           2.修改共享数据不需加锁
           3.用户程序里自己保存多个控制流的上下文栈   }

     3.线程的实现方式
           用户级、内核级、混合模式
          
           用户级:线程的创建、销毁、管理都在用户空间完成, 内核只会识别为一个进程,一条线程;
             特点:
                1、灵活性,操作系统不知道线程存在,在任何平台上都可以运行;不用修改操作系统,
                      容易实现;切换效率高,不需要陷入内核;
                2、内核实现简单,但编程复杂,线程的创建、调度、管理都需要用户程序自己完成;
                3、如果一个线程阻塞,整个进程都会阻塞;不能使用对称多处理器;
           
            内核级:线程的创建、销毁、管理由操作系统内核完成;
                   内核线程使得用户编程简单,但是每次切换都得陷入内核
                      (从用户态切换到内核态),所以效率较低;
          
          混合模式:即一部分以用户级线程创建,一部分由内核创建,是一个多对多的关系;
                            结合用户级和内核级的优点;

     4.线程库的使用 --> 线程创建
           线程库包含在头文件 pthread.h 中
           线程的创建函数:
        int pthread_create(pthread_t *id, pthread_attr_t *attr,
                           (void*)(*pthread_fun(void*)), void *arg);
                返回值:成功返回0,失败返回错误码;
                id : 用于获取系统创建的线程的ID值;
                attr: 线程的属性,默认属性 NULL;
                pthread_fun: 线程函数,指定新建线程的执行序列;
                arg: 传递给线程函数的参数;
         测试结论:
         1、pthread_create 创建一个新的线程,新线程执行的指令序列是pthread_fun指针指向的函数;
         2、进程中的线程 并发执行;
     
      5.一个进程中的所有线程间的数据共享:
                全局变量  .data   共享
                局部变量  .stack  不共享
                堆区变量  .heap   共享
              文件描述符    fd    共享
    
     6.创建线程时传参
     pthread_create();  创建线程是给线程函数传参的方式:
          1、值传递 :  值最大4个字节,void* 只有4个字节;
          2、地址传递:实现了线程间的栈区数据共享;
          主线程结束时:默认调用exit函数,结束的是进程;
          线程结束的函数:
              int pthread_exit(void *reval); //reval:设置线程的退出状态;
          等待线程结束函数/获取线程退出状态:
              int pthread_join(pthread_t id, void **getval); 
              //会阻塞运行;并获取线程退出的信息
               waitpid(pid_t pid,  );
           主动结束一个线程:int pthread_cancel(pthread_t id);
  
     7.线程同步
         实现线程同步的方法:信号量、互斥锁、读写锁、条件变量
         
         互斥锁:只能在线程之间使用的一种控制临界资源访问的机制;
                如果一个线程要访问临界资源,则必须先加锁,用完之后解锁;
         一个锁只有两种状态:加锁  解锁
                             0    1
             #include<pthread.h>
             pthread_mutex_t mutex; //全局变量
          int pthread_mutex_init(pthread_mutex_t *mutex_t, pthread_mutexattr_t *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); //销毁锁
         
       信号量:类似于计数器,记录临界资源的数量;
            #include<semaphore.h>
            sem_t sem;
            int sem_init(sem_t *sem, int shared, int val);
                shared:信号量是否可以在进程间共享,Linux不支持;
                val:设置的信号量的初始值;
             int sem_wait(sem_t *sem); //P操作  wait等待(sem-1)
             int sem_post(sem_t *sem); //V操作(sem+1)
             int sem_destroy(sem_t *sem); //销毁信号量
             
 条件变量(读写锁   自旋锁)
     条件变量:
         条件变量是线程可用的另一种同步机制,给多个线程提供了一个会合的场所
         条件变量与互斥量一起使用时,允许线程以无竞争的方式等待特定的条件发生
         条件变量是线程中的东西,就是等待某一条件的发生,和信号一样;
     条件变量的使用:
         条件变量要与互斥量一起使用,条件本身是由互斥量保护的,
         线程在改变条件状态之前必须首先锁住互斥量, 
         其他线程在获得互斥量之前不会察觉到这种改变,
         因为互斥量必须在锁定以后才能计算条件;
    条件变量的作用:
         使用条件变量可以以原子方式阻塞线程,直到某个特定条件为真为止。
         条件变量始终与互斥锁一起使用,对条件的测试是在互斥锁(互斥)的保护下进行的。
         如果条件为假,线程通常会基于条件变量阻塞,并以原子方式释放等待条件变化的互斥锁。
         如果另一个线程更改了条件,该线程可能会向相关的条件变量发出信号,
         从而使一个或多个等待的线程执行以下操作:1.唤醒  2.再次获取互斥锁  3.重新评估条件;

      
      读写锁:
          一个资源可以被多个线程同时读,或者被一个线程写,但是不能同时存在读和写线程;
      读写锁作用: 
          读写锁能够保证读取数据的 严格实时性,如果不需要这种 严格实时性,那么不需要加读写锁;
     
     自旋锁:
          是一种用于保护多线程共享资源的锁,与一般互斥锁(mutex)不同之处在于
          当自旋锁尝试获取锁时以忙等待(busy waiting)的形式不断地循环检查锁是否可用。
      自旋锁的作用:
          在多CPU的环境中,对持有锁较短的程序来说,
          使用自旋锁代替一般的互斥锁往往能够提高程序的性能。




         
     8.保证线程安全的库函数
           多线程环境下,操作共享的资源时,可能会发生一些问题 --> 执行的结果不确定;
           可重入函数  strtok_r
           cahr *strtok(char *buff, char *flag);
           char *strtok_r(char *buff, char *flag, char **rtptr);
          
       什么是线程安全?
          线程主要由控制流程和资源使用两部分构成,因此一个不得不面对的问题就是对共享资源的访问。
        为了确保资源得到正确的使用,开发人员在设计编写程序时需要考虑避免竞争条件和死锁,
        需要更多地考虑使用线程互斥变量;
           因此在编写线程安全函数时,要注意两点:
           1.减少对临界资源的依赖,尽量避免访问全局变量,静态变量或其它共享资源,
                如果必须要使用共享资源,所有使用到的地方必须要进行互斥锁 (Mutex) 保护;
           2.线程安全的函数所调用到的函数也应该是线程安全的,如果所调用的函数不是线程安全的,
                那么这些函数也必须被互斥锁 (Mutex) 保护;


           
     9.多线程下的fork使用及锁的继承问题
          1、如果多线程环境下,一个线程调用fork创建子进程,
             子进程仅仅会将调用fork的那个线程启动,其他线程并不会启动;
          2、多线程环境下调用fork创建的子进程继承父进程的锁;
             子进程会继承父进程的锁,包括其状态;
       
        注册函数:
            int pthread_atfork(void(*parpare)(void), void(*parent)(void), 
                               void(*child)(void));  //fork之前调用
            fork调用初始,先调用parpare函数,其作用是:给所有的锁加锁,
                如果有锁处于加锁状态,则fork函数会被阻塞,等待解锁;
            parent和child函数在fork执行完成之后,分别在父进程空间和子进程空间调用,
                         其作用是:对所有的锁解锁;
                         
       伪代码:
         mutex
         void pthread_fun(void *arg)
         {
           pthread_mutex_lock(&mutex);
           sleep(3);
           unlock();
         }
         int main()
         {
           .....
           sleep(1);  //保证函数线程能够加锁;
           pit_t pid = fork(); //fork执行时,mutex是函数线程加锁状态;
           if(pid == 0)
           {
              lock(&mutex);
              printf();
           }
           else
            {
               lock(&mutex);
               printf();
            }
       }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值