多进程、多线程

17 篇文章 0 订阅
9 篇文章 0 订阅

线程池

线程池简介:
多线程技术主要解决处理器单元内多个线程执行的问题,它可以显著减少处理器单元的闲置时间,增加处理器单元的吞吐能力。
假设一个服务器完成一项任务所需时间为:T1 创建线程时间,T2 在线程中执行任务的时间,T3 销毁线程时间。
如果:T1 + T3 远大于 T2,则可以采用线程池,以提高服务器性能。
一个线程池包括以下四个基本组成部分:
1、线程池管理器(ThreadPool):用于创建并管理线程池,包括 创建线程池,销毁线程池,添加新任务;
2、工作线程(PoolWorker):线程池中线程,在没有任务时处于等待状态,可以循环的执行任务;
3、任务接口(Task):每个任务必须实现的接口,以供工作线程调度任务的执行,它主要规定了任务的入口,任务执行完后的收尾工作,任务的执行状态等;
4、任务队列(taskQueue):用于存放没有处理的任务。提供一种缓冲机制。

线程池技术正是关注如何缩短或调整T1,T3时间的技术,从而提高服务器程序性能的。它把T1,T3分别安排在服务器程序的启动和结束的时间段或者一些空闲的时间段,这样在服务器程序处理客户请求时,不会有T1,T3的开销了。
线程池不仅调整T1,T3产生的时间段,而且它还显著减少了创建线程的数目,看一个例子:
假设一个服务器一天要处理50000个请求,并且每个请求需要一个单独的线程完成。在线程池中,线程数一般是固定的,所以产生线程总数不会超过线程池中线程的数目,而如果服务器不利用线程池来处理这些请求则线程总数为50000。一般线程池大小是远小于50000。所以利用线程池的服务器程序不会为了创建50000而在处理请求时浪费时间,从而提高效率。
REF: Linux下C线程池的实现

多线程中调用fork

APUE P337
一、fork()函数
在操作系统的基本概念中进程是程序的一次执行,且是拥有资源的最小单位和调度单位(在引入线程的操作系统中,线程是最小的调度单位)。在Linux系统中创建进程有两种方式:一是由操作系统创建,二是由父进程创建进程(通常为子进程)。系统调用函数fork()是创建一个新进程的唯一方式,当然vfork()也可以创建进程,但是实际上其还是调用了fork()函数。fork()函数是Linux系统中一个比较特殊的函数,其一次调用会有两个返回值,下面是fork()函数的声明:

#include <unistd.h>

// On success, The PID of the process is returned in the parent, and 0 is returned in the child. On failure,
// -1 is returned in the parent, no child process is created, and errno is set appropriately.
pid_t fork (void);

当程序调用fork()函数并返回成功之后,程序就将变成两个进程,调用fork()者为父进程,后来生成者为子进程。这两个进程将执行相同的程序文本,但却各自拥有不同的栈段、数据段以及堆栈拷贝。子进程的栈、数据以及栈段开始时是父进程内存相应各部分的完全拷贝,因此它们互不影响。从性能方面考虑,父进程到子进程的数据拷贝并不是创建时就拷贝了的,而是采用了写时拷贝(copy-on -write)技术来处理。调用fork()之后,父进程与子进程的执行顺序是我们无法确定的(即调度进程使用CPU),意识到这一点极为重要,因为在一些设计不好的程序中会导致资源竞争,从而出现不可预知的问题。下图为写时拷贝技术处理前后的示意图:
这里写图片描述

在Linux系统中,常常存在许多对文件的操作,fork()的执行将会对文件操作带来一些小麻烦。由于子进程会将父进程的大多数数据拷贝一份,这样在文件操作中就意味着子进程会获得父进程所有文件描述符的副本,这些副本的创建方式类似于dup()函数调用,因此父、子进程中对应的文件描述符均指向相同的打开的文件句柄,而且打开的文件句柄包含着当前文件的偏移量以及文件状态标志,所以在父子进程中处理文件时要考虑这种情况,以避免文件内容出现混乱或者别的问题。下图为执行fork()调用后文件描述符的相关处理及其变化:
这里写图片描述

二、线程
与进程类似,线程(thread)是允许应用程序并发执行多个任务的一种机制。一个进程中可以包含多个线程,同一个程序中的所有线程均会独立执行,且共享同一份全局内存区域,其中包括初始化数据段(initialized data),未初始化数据段(uninitialized data),以及堆内存段(heap segment)。在多处理器环境下,多个线程可以同时执行,如果线程数超过了CPU的个数,那么每个线程的执行顺序将是无法确定的,因此对于一些全局共享数据据需要使用同步机制来确保其的正确性。
在系统中,线程也是稀缺资源,一个进程能同时创建多少个线程这取决于地址空间的大小和内核参数,一台机器可以同时并发运行多少个线程也受限于CPU的数目。在进行程序设计时,我们应该精心规划线程的个数,特别是根据机器CPU的数目来设置工作线程的数目,并为关键任务保留足够的计算资源。如果你设计的程序在背地里启动了额外的线程来执行任务,那这也属于资源规划漏算的情况,从而影响关键任务的执行,最终导致无法达到预期的性能。很多程序中都存在全局对象,这些全局对象的初始化工作都是在进入main()函数之前进行的,为了能保证全局对象的安全初始化(按顺序的),因此在程序进入main()函数之前应该避免线程的创建,从而杜绝未知错误的发生。

三、fork()与多线程
在程序中fork()与多线程的协作性很差,这是POSIX系列操作系统的历史包袱。因为长期以来程序都是单线程的,fork()运转正常。当20世纪90年代初期引入线程之后,fork()的适用范围就大为缩小了。
多线程执行的情况下调用fork()函数,仅会将发起调用的线程复制到子进程中。(子进程中该线程的ID与父进程中发起fork()调用的线程ID是一样的,因此,线程ID相同的情况有时我们需要做特殊的处理。)也就是说不能同时创建出于父进程一样多线程的子进程。其他线程均在子进程中立即停止并消失,并且不会为这些线程调用清理函数以及针对线程局部存储变量的析构函数。这将导致下列一些问题:
1. 虽然只将发起fork()调用的线程复制到子进程中,但全局变量的状态以及所有的pthreads对象(如互斥量、条件变量等)都会在子进程中得以保留,这就造成一个危险的局面。
例如:一个线程在fork()被调用前锁定了某个互斥量,且对某个全局变量的更新也做到了一半,此时fork()被调用,所有数据及状态被拷贝到子进程中,那么子进程中对该互斥量就无法解锁(因为其并非该互斥量的属主),如果再试图锁定该互斥量就会导致死锁,这是多线程编程中最不愿意看到的情况。同时,全局变量的状态也可能处于不一致的状态,因为对其更新的操作只做到了一半对应的线程就消失了。fork()函数被调用之后,子进程就相当于处于signal handler之中,此时就不能调用线程安全的函数(用锁机制实现安全的函数),除非函数是可重入的,而只能调用异步信号安全(async-signal-safe)的函数。
fork()之后,子进程不能调用:
malloc(3)。因为malloc()在访问全局状态时会加锁。
任何可能分配或释放内存的函数,包括new、map::insert()、snprintf() ……
任何pthreads函数。你不能用pthread_cond_signal()去通知父进程,只能通过读写pipe(2)来同步。
printf()系列函数,因为其他线程可能恰好持有stdout/stderr的锁。
除了man 7 signal中明确列出的“signal安全”函数之外的任何函数。
2. 因为并未执行清理函数和针对线程局部存储数据的析构函数,所以多线程情况下可能会导致子进程的内存泄露。另外,子进程中的线程可能无法访问(父进程中)由其他线程所创建的线程局部存储变量,因为(子进程)没有任何相应的引用指针。

由于这些问题,推荐在**多线程程序中调用fork()**的唯一情况是:**其后立即调用exec()函数执行另一个程序**,彻底隔断子进程与父进程的关系。由新的进程覆盖掉原有的内存,使得子进程中的所有pthreads对象消失。
对于那些必须执行fork(),而其后又无exec()紧随其后的程序来说,pthreads API提供了一种机制:fork()处理函数。利用函数pthread_atfork()来创建fork()处理函数。pthread_atfork()声明如下:
#include <pthread.h>

// Upon successful completion, pthread_atfork() shall return a value of zero; otherwise, an error number shall be returned to indicate the error.
// @prepare 新进程产生之前被调用
// @parent  新进程产生之后在父进程被调用
// @child    新进程产生之后,在子进程被调用
int pthread_atfork (void (*prepare) (void), void (*parent) (void), void (*child) (void));

该函数的作用就是往进程中注册三个函数,以便在不同的阶段调用,有了这三个参数,我们就可以在对应的函数中加入对应的处理功能。同时需要注意的是,每次调用pthread_atfork()函数会将prepare添加到一个函数列表中,创建子进程之前会(按与注册次序相反的顺序)自动执行该函数列表中函数。parent与child也会被添加到一个函数列表中,在fork()返回前,分别在父子进程中自动执行(按注册的顺序)。具体事例可参考:http://blog.chinaunix.net/uid-26885237-id-3210394.html

四、总结
fork()函数的调用会导致在子进程中除调用线程外的其它线程全都终止执行并消失,因此在多线程的情况下会导致死锁和内存泄露的情况。在进行多线程编程的时候尽量避免fork()的调用,同时在程序在进入main函数之前应避免创建线程,因为这会影响到全局对象的安全初始化。线程不应该被强行终止,因为这样它就没有机会调用清理函数来做相应的操作,同时也就没有机会来释放已被锁住的锁,如果另一线程对未被解锁的锁进行加锁,那么将会立即发生死锁,从而导致程序无法正常运行。
出自:http://blog.csdn.net/cywosp/article/details/27316803

//usage:gcc pthread_atfork.c -lpthread
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>

static  int i = 1;
void prepare(void)
{ 
     printf("prepare the i is %d\n",i);
}
void parent(void)
{
   i++;
   printf("parent i is %d\n",i);

}
void child(void)
{
   i++;
   printf("child the i is %d\n",i);
}

void * fun_thread(void *arg)
{
    printf("thread start....\n");
    i++;
    printf("thread i is %d\n",i);
    pause();

    return 0;
}

int main()
{
    pid_t pid;
    printf("begin the i is %d\n",i);
    if(pthread_atfork(prepare, parent, child)!=0)
    {
        printf("pthread_atfork error\n");
    }

    pthread_t thread_t;
    int err;
    err = pthread_create(&thread_t,NULL,fun_thread,NULL);
    if(err)
    {
        perror("thread create:");        
    }
    else
        perror("thread create:");

    sleep(2);
    printf("parent about to fork:\n");
    if((pid=fork())<0)
    {
        perror("fork :");
    }
    else if(pid ==0)
    {   
        //sleep(1);
        printf("child\n");
    }
    else
    { 
        //sleep(1);
        printf("parent\n");
        wait(pid);
        exit(0);
    }
}
begin the i is 1
thread create:: Success
thread start....
thread i is 2
parent about to fork:
prepare the i is 2
parent i is 3
parent
child the i is 3
child

这里写图片描述

五、线程创建

void * fun_thread(void *arg)
{
    printf("thread start....\n");

    return 0;
}

int main()
{
    pid_t pid;
    pthread_t thread_t;
    int err;
    err = pthread_create(&thread_t,NULL,fun_thread,NULL);
    if(err)
    {
        perror("thread create:");        
    }
    else
        perror("thread create:");


    printf("main thread before sleep i=%d\n",i);
    sleep(3);   //此处不加sleep,是不会运行到新创建的子线程的,因为主线程已退出。
}

线程间的通信、同步

参考:APUE P298
互斥锁、条件变量、读写锁和线程信号(非常详细的解释了线程间的同步问题)

避免死锁

递归锁 APUE P320
即如果一个函数既有可能在已加锁的情况下使用,
也有可能在未加锁的情况下使用,往往将这个函数拆成两个版本—加锁版本和不加锁版本(添加nolock后缀)。
例如将foo()函数拆成两个函数。

    // 不加锁版本  
    void foo_nolock()  
    {  
        // do something  
    }  
    // 加锁版本  
    void fun()  
    {  
        mutex.lock();  
       foo_nolock();  
       mutex.unlock();  
   }  

为了接口的将来的扩展性,可以将bar()函数用同样方法拆成bar_withou_lock()函数和bar()函数。
在Douglas C. Schmidt(ACE框架的主要编写者)的“Strategized Locking, Thread-safe Interface, and Scoped Locking”论文中,
提出了一个基于C 的线程安全接口模式(Thread-safe interface pattern),与AUPE的方法有异曲同工之妙。
即在设计接口的时候,每个函数也被拆成两个函数,没有使用锁的函数是private或者protected类型,
使用锁的的函数是public类型。接口如下:

    class T  
    {  
    public:  
        foo(); //加锁  
        bar(); //加锁  
    private:  
        foo_nolock();  
        bar_nolock();  
    }  

作为对外接口的public函数只能调用无锁的私有变量函数,而不能互相调用。

读写锁

读写锁的实现往往是比互斥锁要复杂的,因此开销通常也大于互斥锁。
如果一个线程先获得写锁,又获得读锁,则结果是无法预测的。

进程的创建

参考:进程创建fork

内核精彩文章

Linux内核之旅

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值