进程: 子进程是父进程的复制品.子进程获得父进程数据空间、堆和栈的复制品。
线程:相对与进程而言,线程是一个更接近与执行体的概念,它可以与同进程的其他线程共享数据,但拥有自己的栈空间,拥有独立的执行序列。
根本区别:用多进程每个进程有自己的地址空间,线程则共享地址空间。所以其他区别都是由此而来的:
1、 速度:线程产生的速度快,线程间的通讯快、切换快等,因为他们在同一个地址空间内。
2、 资源利用率:线程的资源利用率比较好也是因为他们在同一个地址空间内。
3、 同步问题:线程使用 公共变量/内存 时需要使用同步机制还是因为他们在同一个地址空间内。
很想写点关于多进程和多线程的东西,我确实很爱他们。但是每每想动手写点关于他们的东西,却总是求全心理作祟,始终动不了手。
今天终于下了决心,写点东西,以后可以再修修补补也无妨。
一.为何需要多进程(或者多线程),为何需要并发?
这个问题或许本身都不是个问题。但是对于没有接触过多进程编程的朋友来说,他们确实无法感受到并发的魅力以及必要性。
我想,只要你不是整天都写那种int main()到底的代码的人,那么或多或少你会遇到代码响应不够用的情况,也应该有尝过并发编程的甜头。就像一个快餐点的服务员,既要在前台接待客户点餐,又要接电话送外卖,没有分身术肯定会忙得你焦头烂额的。幸运的是确实有这么一种技术,让你可以像孙悟空一样分身,灵魂出窍,乐哉乐哉地轻松应付一切状况,这就是多进程/线程技术。
并发技术,就是可以让你在同一时间同时执行多条任务的技术。你的代码将不仅仅是从上到下,从左到右这样规规矩矩的一条线执行。你可以一条线在main函数里跟你的客户交流,另一条线,你早就把你外卖送到了其他客户的手里。
所以,为何需要并发?因为我们需要更强大的功能,提供更多的服务,所以并发,必不可少。
二.多进程
什么是进程。最直观的就是一个个pid,官方的说法就:进程是程序在计算机上的一次执行活动。
说得简单点,下面这段代码执行的时候
[cpp] view plaincopyprint?
int main()
{
printf(”pid is %d/n”,getpid() );
return 0;
}
进入main函数,这就是一个进程,进程pid会打印出来,然后运行到return,该函数就退出,然后由于该函数是该进程的唯一的一次执行,所以return后,该进程也会退出。
看看多进程。linux下创建子进程的调用是fork();
[cpp] view plaincopyprint?
#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
void print_exit()
{
printf("the exit pid:%d/n",getpid() );
}
main ()
{
pid_t pid;
atexit( print_exit ); //注册该进程退出时的回调函数
pid=fork();
if (pid < 0)
printf("error in fork!");
else if (pid == 0)
printf("i am the child process, my process id is %d/n",getpid());
else
{
printf("i am the parent process, my process id is %d/n",getpid());
sleep(2);
wait();
}
}
i am the child process, my process id is 15806
the exit pid:15806
the exit pid:15806
i am the parent process, my process id is 15805
the exit pid:15805
the exit pid:15805
这是gcc测试下的运行结果。
关于fork函数,功能就是产生子进程,由于前面说过,进程就是执行的流程活动。
那么fork产生子进程的表现就是它会返回2次,一次返回0,顺序执行下面的代码。这是子进程。
一次返回子进程的pid,也顺序执行下面的代码,这是父进程。
(为何父进程需要获取子进程的pid呢?这个有很多原因,其中一个原因:看最后的wait,就知道父进程等待子进程的终结后,处理其task_struct结构,否则会产生僵尸进程,扯远了,有兴趣可以自己google)。
如果fork失败,会返回-1.
额外说下atexit( print_exit ); 需要的参数肯定是函数的调用地址。
进程通信方式这里的print_exit 是函数名还是函数指针呢?答案是函数指针,函数名永远都只是一串无用的字符串。
某本书上的规则:函数名在用于非函数调用的时候,都等效于函数指针。
说到子进程只是一个额外的流程,那他跟父进程的联系和区别是什么呢?
我很想建议你看看linux内核的注解(有兴趣可以看看,那里才有本质上的了解),总之,fork后,子进程会复制父进程的task_struct结构,并为子进程的堆栈分配物理页。理论上来说,子进程应该完整地复制父进程的堆,栈以及数据空间,但是2者共享正文段。
关于写时复制:由于一般 fork后面都接着exec,所以,现在的 fork都在用写时复制的技术,顾名思意,就是,数据段,堆,栈,一开始并不复制,由父,子进程共享,并将这些内存设置为只读。直到父,子进程一方尝试写这些区域,则内核才为需要修改的那片内存拷贝副本。这样做可以提高 fork的效率。
三.多线程
线程是可执行代码的可分派单元。这个名称来源于“执行的线索”的概念。在基于线程的多任务的环境中,所有进程有至少一个线程,但是它们可以具有多个任务。这意味着单个程序可以并发执行两个或者多个任务。
简而言之,线程就是把一个进程分为很多片,每一片都可以是一个独立的流程。这已经明显不同于多进程了,进程是一个拷贝的流程,而线程只是把一条河流截成很多条小溪。它没有拷贝这些额外的开销,但是仅仅是现存的一条河流,就被多线程技术几乎无开销地转成很多条小流程,它的伟大就在于它少之又少的系统开销。(当然伟大的后面又引发了重入性等种种问题,这个后面慢慢比较)。
还是先看linux提供的多线程的系统调用:
int pthread_create(pthread_t *restrict tidp, const pthread_attr_t *restrict attr, void *(*start_rtn)(void), void *restrict arg); |
Returns: 0 if OK, error number on failure |
第一个参数为指向线程标识符的指针。
第二个参数用来设置线程属性。
第三个参数是线程运行函数的起始地址。
最后一个参数是运行函数的参数。
第二个参数用来设置线程属性。
第三个参数是线程运行函数的起始地址。
最后一个参数是运行函数的参数。
[cpp] view plaincopyprint?
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>
void* task1(void*);
void* task2(void*);
void usr();
int p1,p2;
int main()
{
usr();
getchar();
return 1;
}
void usr()
{
pthread_t pid1, pid2;
pthread_attr_t attr;
void *p;
int ret=0;
pthread_attr_init(&attr); //初始化线程属性结构
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); //设置attr结构为分离
pthread_create(&pid1, &attr, task1, NULL); //创建线程,返回线程号给pid1,线程属性设置为attr的属性,线程函数入口为task1,参数为NULL
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);
pthread_create(&pid2, &attr, task2, NULL);
//前台工作
ret=pthread_join(pid2, &p); //等待pid2返回,返回值赋给p
printf("after pthread2:ret=%d,p=%d/n", ret,(int)p);
}
void* task1(void *arg1)
{
printf("task1/n");
//艰苦而无法预料的工作,设置为分离线程,任其自生自灭
pthread_exit( (void *)1);
}
void* task2(void *arg2)
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论