Linux 是多任务操作系统,可以同时运行多个进程,来完成多项工作。在Linux编程中,为了满足项目高并发的性能需求,采用多进程和多线程进行编程,下面将具体介绍Linux下的多进程与多线程编程。
1.多进程
进程就是处于活动状态的程序,占用一定的内存空间。进程可以把自己复制一份,从而创造出一个新的进程。新的进程称为 子进程,原来的进程称为 父进程。
进程可以复制自己。这意味着启动一个程序,可能会产生多个进程。这样的程序能同时进行多项工作。多进程编程就是要设计一个这样的程序。
1.1进程的创建
在进程中创建子进程是通过 fork() 函数进行创建的。fork() 不需要任何参数,返回值是 pid_t 型。pid_t 型实际上就是 int 型。这是专门用来保存进程 PID(进程的编号)的类型。如果子进程创建成功,fork() 函数将返回子进程的 PID,否则返回 -1。
前面说过,创建子进程相当于把自己复制一份。也就是说,创建出来的子进程和父进程几乎是一模一样的,并且都将接着执行 fork() 函数后面的代码。
不同的是,对于 fork() 函数的返回值,在子进程中将得到 0。因此,如果在 fork() 函数之后用一个 if 语句对 fork() 函数的返回值进行判断,子进程和父进程将进入不同的分支。函数如下:
pid_t cpid;
cpid = fork();
if (cpid == -1) {
printf("Create process failed!\n");
exit(1);
}
if (cpid == 0) {
printf("Hello from Child!\n");
} else {
printf("Hello from Parent!\n");
}
1.2进程状态
进程从创建到运行结束,经历的全部过程,称为进程的生命周期。在生命周期的不同阶段,进程会呈现不同的状态。下表列出了进程可能出现的所有状态。
状态 | 含义 |
---|---|
创建状态 | 正在被创建 |
就绪 | 刚刚创建好,还没运行过 |
内核状态 | 运行中 |
用户状态 | 暂停中 |
睡眠 | 已经轮到这个进程上场了,但是它的某些需求得不到满足,只能继续等待 |
唤醒 | 正在睡眠的进程,正在被唤醒 |
被抢占 | 运行到一半,CPU 被另一个进程抢占 |
僵死状态 | 进程已经结束,但记录还在 |
1.3进程的调度
实际上 CPU 只能同时处理一个进程的工作。也就是说,进程并不是真的同时都在运行。
Linux 是一个 分时 操作系统。在同一时刻,只有一个进程得到 CPU 的处理,但很快就会变成另一个进程,如此往复。虽然每个进程一次只占用 CPU 很短的一段时间,但是每个进程总是很快就能再一次占用 CPU,所以这些进程看起来就好像一直都在运行一样。
CPU 在工作时就好像人体的循环系统一样。心脏的跳动维持血液的流动;晶振的振荡维持 CPU 的运行。心脏一旦停止跳动,血液也就停止流动;晶振一旦停止振荡,CPU 也就停止工作。
晶振在单位时间内的振荡次数称为 时钟频率,两次振荡之间的时间间隔称为 时钟周期。进程占用 CPU 的时间应该以时钟周期为单位来计量。
1.4多进程优缺点
优点:
a,顺序程序的特点:具有封闭性和可再现性。
b,程序的并发执行和资源共享。多道程序设计出现后,实现了程序的并发执行和资源共享,提高了系统的效率和系统的资源利用率。
c,每个进程互相独立,不影响主程序的稳定性,子进程崩溃没关系; 通过增加CPU,就可以容易扩充性能; 可以尽量减少线程加锁/解锁的影响,极大提高性能,就算是线程运行的模块算法效率低也没关系; 每个子进程都有2GB地址空间和相关资源,总体能够达到的性能上限非常大 。
缺点:
a,操作系统调度切换多个线程要比切换调度进程在速度上快的多。而且进程间内存无法共享,通讯也比较麻烦。
b,线程之间由于共享进程内存空间,所以交换数据非常方便;在创建或撤消进程时,由于系统都要为之分配和回收资源,导致系统的开销明显大于创建或撤消线程时的开销。
2.多线程
进程进一步细分,就是线程。每一个进程都至少有一个线程,这个线程称为 主线程。主线程就是运行主函数 main() 的线程。创建线程相当于调用一个函数,只不过原来的线程会立即执行后续的代码而不等待这个函数返回。这使得被调函数中的代码和后续的代码是并行执行的。因此,可以简单地认为多线程就是同时运行多个函数。
2.1线程创建
线程通过调用 pthread_create() 函数创建。函数原型如下:
int pthread_create(pthread_t * id, pthread_attr_t * attr, void * (* start_routine)(void *), void * arg);
其中:
第一个参数要求一个 pthread_t 变量的地址。这个变量用来保存线程的标识符
第二个参数要求一个 pthread_attr_t 结构的地址。这个结构用于设定线程的一些属性,一般设为 0
第三个参数要求一个函数。创建的线程将调用这个函数。这个函数称为 线程函数。线程函数必须以一个 void 指针为参数,返回值也必须是一个 void 指针
第四个参数是一个 void 指针,它将会作为线程函数的参数。如果不需要传参,设为 0
如果线程创建成功,pthread_create() 函数将返回 0,否则返回要给错误代码。这些错误代码是线程库定义的一些常量,但没有一个是 -1。
2.2线程结束
线程调用 pthread_exit() 函数可结束自己,这个函数相当于结束进程的 exit(),唯一的参数是一个 void 指针,用来指向返回值。函数原型如下:
void pthread_exit(void * retval);
2.3等待线程结束
可以调用 pthread_join() 函数来等待另一个线程结束,其函数原型如下:
int pthread_join(pthread_t id, void ** retval);
其中:
第一个参数要求一个线程的标识符
第二个参数要求一个 void 指针的地址。这个指针将被指向线程的返回值。如果不需要得到线程的返回值,可设为 0。
如果顺利,pthread_join() 函数将返回 0,否则返回一个错误代码。
2.4线程脱离同步
pthread_detach() 函数用来使一个线程与其他线程脱离同步。脱离同步是指其他线程不能用 pthread_join() 函数来等待这个线程结束。这个线程将在退出时自行释放所占的资源。函数原型如下:
int pthread_detach(pthread_t id);
pthread_detach() 函数唯一的参数就是需要脱离同步的线程的标识符。如果顺利,将返回 0,否则返回一个错误代码。
2.5.多线程与多线程的优缺点
优点:
a,它是一种非常”节俭”的多任务操作方式。在Linux系统下,启动一个新的进程必须分配给它独立的地址空间,建立众多的数据表来维护它的代码段、堆栈段和数据段,这是一种”昂贵”的多任务工作方式。而运行于一个进程中的多个线程,它们彼此之间使用相同的地址空间,共享大部分数据,启动一个线程所花费的空间远远小于启动一个进程所花费的空间,而且,线程间彼此切换所需的时间也远远小于进程间切换所需要的时间。当然,在具体的系统上,这个数据可能会有较大的区别。
b,线程间方便的通信机制,由于同一进程下的线程之间共享数据空间,所以一个线程的数据可以直接为其它线程所用,这不仅快捷,而且方便。
c,使多CPU系统更加有效。操作系统会保证当线程数不大于CPU数目时,不同的线程运行于不同的CPU上。
d,改善程序结构。一个既长又复杂的进程可以考虑分为多个线程,成为几个独立或半独立的运行部分,这样的程序会利于理解和修改。
缺点:
a,每个线程与主程序共用地址空间,受限于2GB地址空间。
b,线程之间的同步和加锁控制比较麻烦; 一个线程的崩溃可能影响到整个程序的稳定性; 到达一定的线程数程度后,即使再增加CPU也无法提高性能,例如Windows Server 2003,大约是1500个左右的线程数就快到极限了(线程堆栈设定为1M),如果设定线程堆栈为2M,还达不到1500个线程总数; 线程能够提高的总性能有限,而且线程多了之后,线程本身的调度也是一个麻烦事儿,需要消耗较多的CPU。
c,调度时, 要保存线程状态,频繁调度, 需要占用大量的机时; 程序设计上容易出错(线程同步问题)。