并发程序设计

一、进程的创建和回收

1.进程概念

1.进程的概念:

        程序:存放在磁盘上的指令和数据的有序集合(文件)

                   静态的

        进程:执行一个程序所分配的资源的总称

                   进程是程序的一次执行过程

                   动态的,包括创建、调度、执行和消亡        

        

2.进程的内容:

程序在RAM里,进程在ROM(磁盘)里 

       
        BSS段:存放程序中未初始化的全局变量
        数据段:已初始化的全局变量
        代码段:程序执行代码(机器码) 。这部分区域的大小在程序运行前就已经确定,在                          代码段中,也有可能包含一些只读的常数变量,例如字符串常量等。
        堆(heap):存放进程运行中被动态分配的内存段 。malloc等函数分配内存
        栈(stack):局部变量(但不包括static声明的变量,static意味着在数据段中存放变                                    量),函数参数,函数的返回值。可以把堆栈看成一个寄存、交换临时数据                            的内存区。
        进程控制块(pcb):进程标志PID,进程用户, 进程状态、优先级,文件描述符表\

        

        

3.进程的类型

        交互进程(最常用):在shell下启动。以在前台运行,也可以在后台运行

        批处理进程:和在终端无关,被提交到一个作业队列中以便顺序执行

        守护进程:和终端无关,一直在后台运行

        

4.进程的状态:

        运行态:进程正在运行,或者准备运行

        等待态:进程在等待一个事件的发生或某种系统资源

                        可中断

                        不可中断

        停止态:进程被中止,收到信号后可继续运行

        死亡态:已终止的进程,但pcb没有被释放

      

2.进程常用命令

ps:查看系统进程快照        ps 命令详细参数:
                                                        -e:显示所有进程
                                                        -l:长格式显示更加详细的信息
                                                        -f 全部列出,通常和其他选项联用

top:查看进程动态信息        shift + >:后翻页        shift + <:前翻页        top -p PID查看某个进程

/proc:查看进程详细信息   

ps -elf|grep 加进程名,可单独查看该名称的进程

表头含义
F

进程标志,说明进程的权限,常见的标志有两个:

  • 1:进程可以被复制,但是不能被执行;
  • 4:进程使用超级用户权限;
S

进程状态。进程状态。常见的状态有以下几种:

-D:不可被唤醒的睡眠状态,通常用于 I/O 情况。
-R:该进程正在运行。
-S:该进程处于睡眠状态,可被唤醒。
-T:停止状态,可能是在后台暂停或进程处于除错状态。
-W:内存交互状态(从 2.6 内核开始无效)。
-X:死掉的进程(应该不会出现)。
-Z:僵尸进程。进程已经中止,但是部分程序还在内存当中。
-<:高优先级(以下状态在 BSD 格式中出现)。
-N:低优先级。
-L:被锁入内存。
-s:包含子进程。
-l:多线程(小写 L)。
-+:位于后台。

UID运行此进程的用户的 ID
PID进程的 ID;
PPID父进程的 ID;
C该进程的 CPU 使用率,单位是百分比;
PRI进程的优先级,数值越小,该进程的优先级越高,越早被 CPU 执行;
NI进程的优先级,数值越小,该进程越早被执行
ADDR该进程在内存的哪个位置
SZ该进程占用多大内存
WCHAN该进程是否运行。"-"代表正在运行
TTY该进程由哪个终端产生
TIME该进程占用 CPU 的运算时间,注意不是系统时间
CMD产生此进程的命令名

nice   按用户指定的优先级运行进程
     nice [-n NI值] 命令
NI 范围是 -20~19。数值越大优先级越低  (越nice优先级越低)
普通用户调整 NI 值的范围是 0~19,而且只能调整自己的进程。
普通用户只能调高 NI 值,而不能降低。如原本 NI 值为 0,则只能调整为大于 0。
只有 root 用户才能设定进程 NI 值为负值,而且可以调整任何用户的进程。
renice   改变正在运行进程的优先级
    renice [优先级] PID

        

jobs  查看后台进程

bg  将挂起的进程在后台运行

fg  把后台运行的进程放到前台运行

ctrl + z 把运行的前台进程转为后台并停止

./文件名 &   表示把文件后台运行

3.创建子        

1.子进程概念:

        子进程为由另外一个进程(对应称之为父进程)所创建的进程        

2.子进程创建——fork

#include  <unistd.h>
 pid_t  fork(void);  (pid_t 就是一个数字)

fork的本质就是复制一个进程

创建新的进程,失败时返回-1
成功时父进程返回子进程的进程号,子进程返回0
通过fork的返回值区分父进程和子进程

要点:1 虽然全部拷贝了父进程,但子进程只执行fork之后的代码
           2. 父子进程执行顺序是操作系统决定的

        

fork_t.c


#include<stdio.h>
#include<unistd.h>
 
int main(int argc, char **argv[])
{
 
	pid_t pid;
	printf("before fork\n");
	pid = fork();
 
	printf("pid=%d\n",(int)pid);//虽然子进程么有执行上面的代码,但是系统会给它一个pid(就是 0)
	printf("after fork\n");
	return 0;
}

   打印结果

3.父子进程的关系

        子进程继承了父进程的内容
        父子进程有独立的地址空间,互不影响
        若父进程先结束
                 子进程成为孤儿进程,被init进程收养
                 子进程变成后台进程

        若子进程先结束
                 父进程如果没有及时回收,子进程变成僵尸进程

        


#include<stdio.h>
#include<unistd.h>
 
int main(int argc, char **argv[])
{
 
	pid_t pid;
	printf("before fork\n");
	pid = fork();
 
	if(pid>0){
		printf("this is father prosess\n");
	}else if(pid == 0){
		printf("this is son prosess\n");
	}else if(pid < 0){
		perror("fork:");
		return 0;
	}
 
 
	printf("pid=%d\n",(int)pid);//虽然子进程么有执行上面的代码,但是系统会给它一个pid(就是 0)
	printf("after fork\n");
	return 0;
}

 结果

               

 

使用ps -elf|grep    可查看谁是父子进程(PPID是父进程PID)。下图中10079的PPID是10078。杀死父进程(此时是10078)后,子进程的PPID变为init的PID,被init收养了。此时只会执行子进程,CTRL+c无法结束。因为子进程变为了后台进程。可使用  kill  命令杀死。

杀死子进程后,子进程仍然存在,但是变成了僵尸进程(defunct),此时需要父进程把它火化,(关掉父进程即可,可使用CTRL+c结束)
 

        

     

4.子进程进阶

1、一个父进程生成五个子进程

       

  fork_t2.c:


#include <stdio.h>
#include <unistd.h>
 
int main(){
    pid_t pid;
    int i;    
    for(i=0;i<5;i++){
        pid = fork();
        if(pid<0){
            perror("fork");
            return 0;
        }else if(pid==0){
            printf("child process\n");
            sleep(5);
            break;//如果不结束,子进程也会去执行for循环,(子进程只执行fork后内容是有前提的,此时因为本身就是循环,不是按位置执行,是按顺序执行)会创建孙进程
        }else{
            printf("Father process\n");
            sleep(5);
        }
    }
 
    sleep(100);
 
 
}

如果不结束,子进程也会去执行for循环,(子进程只执行fork后内容是有前提的,此时因为本身就是循环,不是按位置执行,是按顺序执行)会创建孙进程        

结果:        

        

 2、实现一个进程链,父进程->子进程->孙进程->重孙进程->重重孙进程 

        

 day1zuoye.c:

#include <stdio.h>
#include <unistd.h>
 
int main(){
    pid_t pid;
    int i;    
    for(i=0;i<5;i++){
        pid = fork();
        if(pid<0){
            perror("fork");
			return 0;
        }else if(pid==0){
            printf("child process\n");
            sleep(5);
        }else{
            printf("Father process\n");
            sleep(5);
			break;//父进程不生了
        }
    }
 
    sleep(100);
 
 
}

结果:

5.进程的结束

#include <stdlib.h> 
 #include  <unistd.h>
 void  exit(int  status);
 void  _exit(int  status);

结束当前的进程并将status返回
exit结束进程时会刷新(流)缓冲区、、而_exit()不会刷新缓冲区

return 和exit的区别
只有main函数结束时会隐式地调用exit函数,普通函数return是返回上一级。        

        

6.进程的回收

子进程结束时由父进程回收

孤儿进程由init进程回收

若没有及时回收会出现僵尸进程

        

1.进程的回收——wait

        

#include <sys/wait.h>
  pid_t wait(int *status); 
 成功时返回回收的子进程的进程号;失败时返回EOF
 若子进程没有结束,父进程一直阻塞
 若有多个子进程,哪个先结束就先回收
 status 指定保存子进程返回值和结束方式的地址(不仅仅是返回值)
 status为NULL表示直接释放子进程PCB,不接收返回值

                        

        

wait_t.c:


#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
 
int main(int argc, const char *argv[])
{
	pid_t pid;
	pid_t rpid;
	pid = fork();
	int status;
 
	if(pid < 0){
		perror("fork:");
		return 0;
	}
	else if(pid == 0){
		sleep(3);
		printf("child will exit\n");
		exit(2);
 
	}else if(pid>0){
		//rpid = wait(&status);
		//printf("get child satus=%d\n",status);
		//printf("get child satus=%x\n",status);//16进制打印
		//printf("get child satus=%d\n",WEXITSTATUS(status));//只取返回值
	}
	while(1){//让父进程不结束
		sleep(1);
	}
}

 结果:wait可以回收僵尸进程

         

        

2、进程回收 - waitpid

#include  <unistd.h>
  pid_t waitpid(pid_t pid, int *status, int option);

 成功时返回回收的子进程的pid或0;失败时返回EOF
 pid可用于指定回收哪个子进程或任意子进程
 status指定用于保存子进程返回值和结束方式的地址
 option指定回收方式,0 或 WNOHANG

参数:
pid
pid>0时,只等待进程ID等于pid的子进程,不管其它已经有多少子进程运行结束退出了,只要指定的子进程还没有结束,waitpid就会一直等下去。
pid=-1时,等待任何一个子进程退出,没有任何限制,此时waitpid和wait的作用一模一样。
pid=0时,等待同一个进程组中的任何子进程,如果子进程已经加入了别的进程组,waitpid不会对它做任何理睬。
pid<-1时,等待一个指定进程组中的任何子进程,这个进程组的ID等于pid的绝对值。

options

options提供了一些额外的选项来控制waitpid,目前在Linux中只支持WNOHANG和WUNTRACED两个选项,这是两个常数,可以用"|"运算符把它们连接起来使用

WNOHANG    :若由pid指定的子进程未发生状态改变(没有结束),则waitpid()不阻塞,立即返回0
WUNTRACED:    返回终止子进程信息和因信号停止的子进程信息

 wait(wait_stat) 等价于waitpid(-1,wait_stat,0) 

wait_t.c:
 

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
 
int main(int argc, const char *argv[])
{
	pid_t pid;
	pid_t rpid;
	pid = fork();
	int status;
 
	if(pid < 0){
		perror("fork:");
		return 0;
	}
	else if(pid == 0){
		sleep(3);
		printf("child will exit\n");
		exit(2);
 
	}else if(pid>0){
		//rpid = wait(&status);
		//waitpid(pid,&status,0);//(只有一个子进程的情况下,pid位置的参数-1和pid是等价的)
		sleep(4);
		waitpid(-1,&status,WNOHANG);//这种模式,要保证它运行在要结束的进程之后,所以加了sleep(4)
		//printf("get child satus=%d\n",status);
		//printf("get child satus=%x\n",status);//16进制打印
		printf("get child satus=%d\n",WEXITSTATUS(status));//只取返回值
	}
	while(1){//让父进程不结束
		sleep(1);
	}
}

结果:        

        

二、exec函数族

1.exec函数族的执行过程

 进程调用exec函数族执行某个程序
进程当前内容被指定的程序替换
实现让父子进程执行不同的程序
         父进程创建子进程
         子进程调用exec函数族
         父进程不受影响        

        

背景:fork创建进程之后,子进程和父进程执行相同的代码,但是在实际开发当中,我们希望父子进程执行不同的代码。
作用:执行指定的程序

        

1.进程- execl/execlp

        

#include <unistd.h>

int execl(const char *path,const char *arg,...);   //执行程序的路径,执行程序的参数
int execlp(const char *file,const char *arg,...);  //环境变量中(具体查Linux课程),...内容为NULL或(char *)0

成功时执行指定的程序;失败时返回EOF
path 执行的程序名称,包含路径
arg***传递给执行的程序的参数列表
file 执行的程序的名称,在PATH中查找

        

2.进程创建——execl(p)示例

执行ls命令,显示/etc目录下所有文件的详细信息        

if (execl("/bin/ls", "ls" , "-a", "-l" , "/etc" ,NULL ) < 0){

    perror("execl");

}

if (execlp("ls", "ls", "-a", "-l", "/etc", NULL) < 0){
    perror("execlp");
}

 exec_t.c


#include<stdio.h>
#include<unistd.h>
int main(int argc, const char *argv[])
{
	//if(execl("/bin/ls","ls","-a","-l","./",NULL)<0){
	if(execlp("ls","ls","-a","-l","./",NULL)<0){
		perror("exec:");
	}
}

  结果: 

              

2.进程——execv/execvp         

#include <unistd.h>

int execv(const char* path,char *const argv[]);
int execvp(const char *file,char *const argv[]);


成功时执行指定的程序;失败时返回EOF
arg...封装成指针数组的形式

 代码实现:

执行ls命令,显示/etc目录下所有文件的详细信息
char *arg[] = {"ls","-a","-l","/etc",NULL};

if (execv("/bin/ls",agv) < 0){
    perror("execv");
}

if (execvp("ls",agv)<0){
    perror("execvp");
}

execv_t.c

#include <unistd.h>
#include <stdio.h>

int main(int argc, const char *argv[])
{
	char *arg[] = {"ls","-a","-l",NULL};
	if (execv("/bin/ls",arg) < 0){
		perror("execv");
	}
	return 0;
}

       

3.进程——system

#include <stdlib.h>

int system(const char *command);


成功时返回命令command的返回值;失败时返回EOF
当前进程等待command执行结束后才继续执行

 system_t.c

执行ls命令,显示/etc目录下所有文件的详细信息
#include <stdlib.h>

int main(int argc, char *argv[])
{
    system("ls -a -l ./");
}

执行结果同上 

注意:

两个函数区别execlp不需要写文件名全路径,在PATH查找
    最后一个参数必须用空指针(NULL)作结束
执行完excep进程当前内容被指定的程序替换,但进程号不变
第0个参数(ls)必须要写,虽然它没有使用

        

3.exec函数族特点exec2_t.c:

#include <stdio.h>
#include <unistd.h>

int main(int argc, const char *argv[])
{
	printf("before exec\n");
	if(execlp("ls","ls","-a","-l","./",NULL) < 0){
		perror("execl");
	}

	printf("after exec\n");
	return 0;
}

结果:

  因为在执行execlp时ls替代了程序,所以下面的进程不会执行      

        

如果想不被替换,可造一个子进程来执行execl

 test.c

#include <stdio.h>

int main(int argc,char *argv[])
{
    printf("hello world %s,%s\n",argv[1],argv[2]);
}

exec2_t.c:


#include <stdio.h>
#include <unistd.h>
int main(int argc, const char *argv[])
{
	pid_t pid;
	printf("before exec\n");
	pid = fork();
	if(pid == 0){
		if(execl("./test","ls","-a","-l","./",NULL)<0){
			perror("execl:");
		}
	}
	printf("after execl\n");

结果:        

                

三、守护进程

1.概念

        守护进程(Daemon Process)又叫精灵进程(Daemon Process),是Linux三种进程类型之一,是Linux中的后台服务进程,是一个生存期较长的进程,通常独立于控制终端并且周期性地执行某种任务或等待处理某些发生地事件。

        进程组(Process Group):进程集合,每个进程组有一个组长(Leader),其进程ID就是该进程组ID。

        会话(Session):进程组集合,每个会话有一个组长,其进程ID就是该会话组ID。

        控制终端(Controlling Terminal):每个会话可以有一个单独的控制终端,与控制终端连接的Leader就是控制进程(Controlling Process)。

        PS:控制终端就是shell   会话和控制终端没有严格分开

        

2.守护进程特点:

        始终在后台运行,独立于任何终端,周期性的执行某种任务或等待处理特定事件。
它是个特殊的孤儿进程,这种进程脱离终端,为什么要脱离终端呢?之所以脱离于终端是为了避免进程被任何终端所产生的信息所打断,其在执行过程中的信息也不在任何终端上显示。由于在 Linux 中,每一个系统与用户进行交流的界面称为终端,每一个从此终端开始运行的进程都会依附于这个终端,这个终端就称为这些进程的控制终端,当控制终端被关闭时,相应的进程都会自动关闭。

举例:

http服务的守护进程叫httpd,mysql服务的守护进程叫mysqld。

更简便地创建守护进程:nohup命令

nohup xxx &

        

3.创建守护进程

setsid函数:
pid_t setsid(void);
成功:返回调用进程的会话ID;失败:-1,设置errno。
调用了setsid函数的进程,既是新的会长,也是新的组长
getsid函数
pid_t getsid(pid_t pid)
成功:返回调用进程的会话ID;失败:-1,设置errno

1.pid为0表示察看当前进程session ID
2.ps ajx命令查看系统中的进程。参数a表示不仅列当前用户的进程,也列出所有其他用户的进程,参数x表示不仅列有控制终端的进程,也列出所有无控制终端的进程,参数j表示列出与作业控制相关的信息。
3.组长进程不能成为新会话首进程,新会话首进程必定会成为组长进程。

getpid:pid_t getpid(void);       获取进程id
getpgid:pid_t getpgid(pid_t pid);   获取进程组id

       

step1、创建子进程,父进程退出
     if (fork() > 0)  {
        exit(0);
     }

子进程变成孤儿进程,被init进程收养
子进程在后台运行

step2、子进程创建新会话
     if (setsid() < 0)  {
        exit(-1);
     }

子进程成为新的会话组长
  子进程脱离原先的终端

step3、更改当前工作目录    
     chdir(“/”);
     chdir(“/tmp”);

 守护进程一直在后台运行,其工作目录不能被卸载
  重新设定当前工作目录cwd

step4、重设文件权限掩码   
     if (umask(0) < 0)  {
        exit(-1);
     }

 文件权限掩码设置为0
  只影响当前进程

step5、关闭打开的文件描述符
     int  i;
     for(i=0; i<3; i++) {
        close(i);
     }
 关闭所有从父进程继承的打开文件
  已脱离终端,stdin / stdout / stderr无法再使用

第一步

方法一:

mydaemon.c


#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
 
int main(int argc, const char *argv[])
{
	pid_t pid;
	pid = fork();
	if(pid<0){
		perror("fork:");
		return 0;
	}else if(pid == 0){
		printf("I am a deamon\n");
		sleep(100);
	}else{
		exit(0);//父进程退出
	}
 
 
}

结果:

方法二(不提倡):使用  nohup 命令(nohup  xxxx  &)


#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
 
int main(int argc, const char *argv[])
{
	pid_t pid;
	pid = fork();
	if(pid<0){
		perror("fork:");
		return 0;
	}else if(pid == 0){
		printf("I am a deamon\n");
		sleep(100);
	}else{
		//exit(0);//父进程退出
		sleep(100);
	}
 
 
}

结果:

        

 第二步:

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
 
int main(int argc, const char *argv[])
{
	pid_t pid;
	pid = fork();
	if(pid<0){
		perror("fork:");
		return 0;
	}else if(pid>0){
		exit(0);//父进程退出
	}
	printf("I am a demon\n");
	printf("sid=%d,pid=%d,pgid=%d\n",getsid(getpid()),getpid(),getpgid(getpid()));
 
	if(setsid()<0){
		perror("setsid:");
		exit(0);
	}
 
	printf("after sid=%d,pid=%d,pgid=%d\n",getsid(getpid()),getpid(),getpgid(getpid()));
 
	sleep(100);
}

结果:        

第三、四、五步:        


#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/stat.h>
 
int main(int argc, const char *argv[])
{
	pid_t pid;
	pid = fork();
	if(pid<0){
		perror("fork:");
		return 0;
	}else if(pid>0){
		exit(0);//父进程退出
	}
	printf("I am a demon\n");
	printf("sid=%d,pid=%d,pgid=%d\n",getsid(getpid()),getpid(),getpgid(getpid()));
 
	if(setsid()<0){//子进程成为新的会话组长
		perror("setsid:");
		exit(0);
	}
 
	printf("after sid=%d,pid=%d,pgid=%d\n",getsid(getpid()),getpid(),getpgid(getpid()));
 
	chdir("/");//重设当前目录为“/”目录
 
	if(umask(0)<0){//文件权限掩码设置为0
		perror("umask:");
		exit(0);
	}
 
	close(0);
	close(1);
	close(2);//关闭文件描述符,第一第二第三
 
	printf("after close\n");
	sleep(100);
}

结果:关闭文件描述符后,不能打印在屏幕上了,但是可以打印到文件里        

 四、GDB调试多进程程序

set follow-fork-mode child     设置GDB调试子进程
set follow-fork-mode parent   设置GDB调试父进程
set detach-on-fork  on/off    设置GDB跟踪调试单个进程或多个
on: 只调试父进程或子进程的其中一个,(根据follow-fork-mode来决定),这是默认的模式
off:父子进程都在gdb的控制之下,其中一个进程正常调试(根据follow-fork-mode来决定),另一个进程会被设置为暂停状态。
info inferiors    显示GDB调试的进程
inferiors  进程序号(1,2,3....)  切换GDB调试的进程
  

五、线程的创建和参数传递

1.线程基本特点

进程:

        进程有独立的地址空间

        linux为每个进程创建task_struct

        每个进程都参与内核调度,互不影响

线程:

        进程在切换时系统开销大

        很多操作系统引入了轻量级进程LWP

        同一进程中的线程共享相同地址空间

        Linux不区分进程、线程

线程特点:

        通常线程指的是共享相同地址空间的多个任务

        使用多线程的好处:

                大大提高了任务切换的效率

                避免了额外的TLB & cache的刷新

        

线程共享资源:

        一个进程中的多个线程共享以下资源

                可执行的指令

                静态数据

                进程中打开的文件描述符

                当前工作目录

                用户ID

                用户组ID

线程私有资源:

        线程ID(TID)

        PC(程序计数器)和相关寄存器

        堆栈

        错误号(errno)

        优先级

        执行状态和属性

        

2.使用pthread库函数创建线程

1.pthread线程库中提供了如下基本操作        //Linux本身内核不能实现线程,而是通过线程库

        创建线程

        回收线程

        结束线程

同步和互斥机制

        信号量

        互斥锁        

        

#include  <pthread.h>
 int  pthread_create(pthread_t *thread, const  pthread_attr_t *attr, void *(*routine)(void *), void *arg);

pthread_t *thread是一个数字的指针(pthread_t就是一个数字),pthread_attr_t *attr是线程的结构体

 成功返回0,失败时返回错误码
 thread 线程对象
 attr 线程属性,NULL代表默认属性
 routine 线程执行的函数
 arg 传递给routine的参数 ,参数是void * ,注意传递参数格式,

2.线程的运行特点

        1.主进程的退出,他创建的线程也会退出

        2.线程创建需要时间,如果主进程马上退出,那线程不能得到执行

        

3.线程结束--pthread_exit

#include <pthread.h>
void pthread_exit(void *retval)


结束当前线程
retval可被其他线程通过pthread_join获取
线程私有资源被释放

4.获取线程的id

        ①通过pthread_create函数的第一个参数;

        ②通过在线程里面调用pthread_self函数                

        

createP_t.c:


#include<stdio.h>
#include<pthread.h>
#include<unistd.h>
 
int *testThread(char *arg){
	printf("This is a thread test,pid=%d,tid=%lu\n",getpid(),pthread_self());//调用函数线程内查看
	//return NULL;
	pthread_exit(NULL);//退出线程(与return NULL类似,建议使用本函数,有清理线程的作用)
	printf("after pthread exit\n");
}
 
 
int main()
{
	pthread_t tid;
	int ret;
	ret = pthread_create(&tid,NULL,(void*)testThread,NULL);
	printf("This is main thread,tid = %lu\n",tid);//通过获取creat时的第一个参数在主线程查看
	sleep(1);
 
 
}

结果

3.线程的参数传递(重点难点)

pthread_create(pthread_t *thread, const  pthread_attr_t *attr, void *(*routine)(void *), void *arg);
 最后一个参数

        

编译错误:
createP_t.c:8:34: warning: dereferencing ‘void *’ pointer
     printf("input arg=%d\n",(int)*arg);
                                  ^
createP_t.c:8:5: error: invalid use of void expression
     printf("input arg=%d\n",(int)*arg);
错误原因是void *类型指针不能直接用*取值(*arg),因为编译不知道数据类型。
解决方法:转换为指定的指针类型后再用*取值  比如:*(int *)arg

        

第一种方式:通过地址传递


#include<stdio.h>
#include<pthread.h>
#include<unistd.h>
 
void *testThread(void *arg){//这里的arg变成了(void*)型指针
	printf("This is a thread test,pid=%d,tid=%lu\n",getpid(),pthread_self());//调用函数线程内查看
	//return NULL;
	printf("input arg=%d\n",*(int*)arg);//(void*)型指针无法直接取值,需要给一个准确的类型
	pthread_exit(NULL);//退出线程(与return NULL类似,建议使用本函数,有清理线程的作用)
	printf("after pthread exit\n");
}
 
 
int main()
{
	pthread_t tid;
	int ret;
	int arg = 5;//这里arg是整型
	ret = pthread_create(&tid,NULL,(void*)testThread,(void*)&arg);
	printf("This is main thread,tid = %lu\n",tid);//通过获取creat时的第一个参数在主线程查看
	sleep(1);
 
 
}

结果:

        

 第二种方式:值传递


#include<stdio.h>
#include<pthread.h>
#include<unistd.h>
 
void *testThread(void *arg){
	printf("This is a thread test,pid=%d,tid=%lu\n",getpid(),pthread_self());//调用函数线程内查看
	//return NULL;
	//printf("input arg=%d\n",arg);//直接用%d打印地址,会告警(%p打印会加0x前缀,但不会告警)
	printf("input arg=%d\n",(int)arg);//把指针类型直接转化为整型(不会报错),本来5是地址这里直接变成整型
	pthread_exit(NULL);//退出线程(与return NULL类似,建议使用本函数,有清理线程的作用)
	printf("after pthread exit\n");
}
 
 
int main()
{
	pthread_t tid;
	int ret;
	int arg = 5;//这里arg是整型
	ret = pthread_create(&tid,NULL,(void*)testThread,(void*)arg);//整型直接转化为(void*)型指针,5就是地址
	printf("This is main thread,tid = %lu\n",tid);//通过获取creat时的第一个参数在主线程查看
	sleep(1);
 
 
}

结果:

        

注意:1.    通过地址传递参数,注意类型的转换
2.    值传递,这时候编译器会告警,需要程序员自己保证数据长度正确(int和指针长度都是4,所以才能能运行) 

        

创建多线程:

mthread_t.c:

#include<stdio.h>
#include<pthread.h>
#include<unistd.h>
 
void *testThread(void *arg){
	printf("This is a thread test,pid=%d,tid=%lu\n",getpid(),pthread_self());
	//return NULL;
	//printf("This is %d thread\n",*(int*)arg);//地址传递
	printf("This is %d thread\n",(int)arg);//值传递
	pthread_exit(NULL);
	printf("after pthread exit\n");
}
 
 
int main()
{
	pthread_t tid[5];
	int ret;
	int arg = 5;
	int i;
	for(i=0;i<5;i++){//创建多线程
    //ret = pthread_create(&tid[i],NULL,(void*)testThread,(void*)&i);
	ret = pthread_create(&tid[i],NULL,(void*)testThread,(void*)i);
	printf("This is main thread,tid = %lu\n",tid[i]);
	//sleep(1);//不加此函数,for循环里i都到5执行完了,上面子线程可能才能取到i的值
	
	}
 
	sleep(1);
 
 
}

1、不加sleep(1)的地址传递:(有问题)

2、加sleep(1)的地址传递(效率低)

3、值传递

运行错误:

*** stack smashing detected ***: ./mthread_t terminated
已放弃 (核心已转储)

原因:栈被破坏了(数组越界)(本来ret = pthread_create(&tid[i],NULL,(void*)testThread,(void*)i);是   ret = pthread_create(&tid[5],NULL,(void*)testThread,(void*)i);)
 

六、线程的回收及内存演示

1.pthread_join与pthread_exit

#include <pthread.h>

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

对于一个默认属性的线程A来说,线程占用的资源并不会因为执行结束而得到释放

//成功返回0,失败时返回错误码
//thread要回收的线程对象
//调用线程阻塞知道thread结束
//*retval接收线程thread的返回值

注意:pthread_join是阻塞函数,如果回收的线程没有结束,就会一直等待       

#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
void *func(void *arg){
    pthread_detach(pthread_self());
    printf("This is child thread\n");
    sleep(25);
    pthread_exit("thread return");
 
}
 
 
int main(){
    pthread_t tid[100];
    void *retv;
    int i;
    for(i=0;i<100;i++){
        pthread_create(&tid[i],NULL,func,NULL);
       // pthread_detach(tid);
    }
     
    while(1){    
        sleep(1);
    } 
 
}

结果:5秒后全部收回  

2.线程分离pthread_detach

用法:线程主动脱离主线程,线程结束后不会产生僵尸线程了。

int pthread_detach(pthread_t thread)

//成功:0;失败:错误号

 线程分离两种方式       

        1.使用pthread_detach

        2.创建线程时候设置为分离属性

pdatt.c:


#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
void *func(void *arg){
    printf("This is child thread\n");
    sleep(5);
    pthread_exit("thread return");
 
}
 
 
int main(){
    pthread_t tid[5];
    void *retv;
    int i;
    pthread_attr_t attr;
    pthread_attr_init(&attr);
    pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED);
 
    for(i=0;i<5;i++){
        pthread_create(&tid[i],&attr,func,NULL);
       // pthread_detach(tid);
    }
    
    while(1){    
        sleep(1);
    } 
 
}

结果:        

        

线程回收的实际效果:


#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
void *func(void *arg){
    printf("This is child thread\n");
    sleep(25);
    pthread_exit("thread return");
 
}
 
 
int main(){
    pthread_t tid[100];
    void *retv;
    int i;
    for(i=0;i<100;i++){
        pthread_create(&tid[i],NULL,func,NULL);
    }
    for(i=0;i<100;i++){
        pthread_join(tid[i],&retv);
        printf("thread ret=%s\n",(char*)retv);
    }
    while(1){    
        sleep(1);
    } 
 
}

结果:

25s后线程结束打印thread ret=thread return

 25秒前:

25s后:内存明显减小,说明线程被回收

        

pdetach_t.c: 


#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
void *func(void *arg){
    pthread_detach(pthread_self());
    printf("This is child thread\n");
    sleep(25);
    pthread_exit("thread return");
 
}
 
 
int main(){
    pthread_t tid[100];
    void *retv;
    int i;
    for(i=0;i<100;i++){
        pthread_create(&tid[i],NULL,func,NULL);
       // pthread_detach(tid);
    }
    
    while(1){    
        sleep(1);
    } 
 
}

 结果同上

七、线程的取消和清理

1.pthread_cancel与线程的取消

int pthread_cancel(pthread_t thread) 杀死一个线程
void pthread_testcancel(void);

int pthread_setcancelstate(int state,int *oldstate);  //设置取消使能或禁止
PTHREAD_CANCEL_ENABLE
PTHREAD_CANCEL_DISABLE

int pthread_setcanceltype(int type,int *oldtype)
PTHREAD_CANCEL_DEFRRED
PTHREAD_CANCEL_ASYNCHRONOUS

注意:线程的取消要有取消点才可以,不是说取消就取消,线程的取消点主要是阻塞的系统调用   

        如果没有取消点,手动设置一个 void pthread_testcancel(void)

        

2.线程的清理(没有听懂)

        

void pthread_cleanup_push(void (*routine)(void *),void *arg)

void pthread_cleanup_pop(int execute)

**这两个函数必须成对使用

  

必要性: 当线程非正常终止,需要清理一些资源。

void pthread_cleanup_push(void (*routine) (void *), void *arg)   //(函数指针,表示函数的参数)他其实不是函数,是个宏
void pthread_cleanup_pop(int execute)

显然pthread_cleanup_push() 是挂接 清理函数的,它的返回值类型为 void,有两个入口参数,第一个参数是清理函数函数指针,第二个参数是传给清理函数的 typeless pointer 。

另一个兄弟 pthread_cleanup_pop() 是来触发清理函数的,是按照相反的顺序来触发清理函数的。而如果它的入口参数 execute 为0值,则对应的清理函数并没有真正的执行。

routine 函数被执行的条件:(或)
1.    被pthread_cancel取消掉。
2.    执行pthread_exit 
3.    非0参数执行pthread_cleanup_pop()

注意:
1.    必须成对使用,即使pthread_cleanup_pop不会被执行到也必须写上,否则编译错误。2.pthread_cleanup_pop()被执行且参数为0,pthread_cleanup_push回调函数routine不会被执行.
3 pthread_cleanup_push 和pthread_cleanup_pop可以写多对,routine执行顺序正好相反
4.    线程内的return 可以结束线程,也可以给pthread_join返回值,但不能触发pthread_cleanup_push里面的回调函数,所以我们结束线程尽量使用pthread_exit退出线程。但是return的返回值和pthread_exit的返回值都可以被pthread_join捕捉到。
 

      

八、互斥锁/读写锁的概念及使用、死锁的避免

1.互斥锁的概念和使用

临界资源

        一次只允许一个任务(进程、线程)访问的共享资源(比如打印机),比如写文件,只能由一个线程写,同时写会乱码。比如外设打印机,打印的时候只能由一个程序使用。外设基本上都是不能共享的资源。

临界区

        访问临界资源的代码

互斥机制

        mutex互斥锁

        任务访问临界资源前申请锁,访问完后释放锁 

        

创建锁

        两种方法创建互斥锁,静态方式和动态方式(互斥锁初始化-pthread_mutex_init)

     动态方式:   

#include <pthread.h>

int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_murexattr_t *attr);

//成功时返回0,失败时返回错误码
//mutex指向要初始化的互斥锁对象
//attr互斥属性,NULL表示缺省属性
//man函数出现 No manual entry for pthread_mutex_xxx解决

        静态方式:

        

pthread mutex_t mutex = PTHREAD_MUTEX_INITIALIZER

锁的销毁:

        int pthread_mutex_destroy(pthread_mutex_t *mutex)

        在Linux中,互斥锁并不占用任何资源,因此LinuxThreads中pthread_mutex_destroy()除了检查锁状态以外(锁定状态则返回EBUSY)并没有其他动作。

        

申请锁

        

#include <pthread.h>

int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);


//成功返回0,失败返回错误码
//mutex指向要初始化的互斥锁对象
//pthread_mutex_lock如果无法获得锁,任务阻塞
//pthread_mutex_trylock如果无法获得锁,返回EBUSY而不是挂起等待

        

释放锁

        

#include <pthread.h>

int pthread_mutex_unlock(pthread_mutex_t *mutex);

//成功返回0,失败时返回错误码
//mutex指向要初始化的互斥锁对象

九、条件变量的使用及注意事项

1.条件变量基本使用

应用场景:生产者消费问题

必要性:为了实现等待某个资源,让线程休眠。提高运行效率

        

int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);   //等待资源

int pthread_cond_timedwait(pthread_cond_t *restrict cond,  pthread_mutex_t *restrict mutex,  const struct timespec *restrict abstime);   //等待资源,超时后自动退出

int pthread_cond_signal(pthread_cond_t *cond);      //放出单个信号
int pthread_cond_broadcast(pthread_cond_t *cond);    //广播形式放出信号

使用步骤:
初始化:
静态初始化
pthread_cond_t   cond = PTHREAD_COND_INITIALIZER;      //初始化条件变量
pthread_mutex_t  mutex = PTHREAD_MUTEX_INITIALIZER;  //初始化互斥量

                PS:上面条件变量和互斥量必须同时使用
或使用动态初始化
pthread_cond_init(&cond);

生产资源线程:
pthread_mutex_lock(&mutex);
开始产生资源
pthread_cond_sigal(&cond);    //通知一个消费线程
或者
pthread_cond_broadcast(&cond); //广播通知多个消费线程
pthread_mutex_unlock(&mutex);

消费者线程:

pthread_mutex_lock(&mutex);
while (如果没有资源){   //防止惊群效应
pthread_cond_wait(&cond, &mutex); 
}
有资源了,消费资源
pthread_mutex_unlock(&mutex);  
        

注意:
1 pthread_cond_wait(&cond, &mutex),在没有资源等待是是先unlock 休眠,等资源到了,再lock
所以pthread_cond_wait he pthread_mutex_lock 必须配对使用。
 
2  如果pthread_cond_signal或者pthread_cond_broadcast 早于 pthread_cond_wait ,则有可能会丢失信号。 
3 pthead_cond_broadcast 信号会被多个线程收到,这叫线程的惊群效应。所以需要加上判断条件while循环。


#include<stdio.h>
#include<pthread.h>
#include<unistd.h>
#include<stdlib.h>
 
pthread_cond_t hasTaxi = PTHREAD_COND_INITIALIZER;//初始化条件变量
pthread_mutex_t lock=PTHREAD_MUTEX_INITIALIZER;//初始化互斥量
 
struct taxi{//出租车结构体
	struct taxi *next;//指向下一个车的指针
	int num;//出租车编号
 
};
 
struct taxi *Head=NULL;//出租车链表
 
void *taxiarv(void *arg){//生产者线程
	printf("taxi arrived thread\n");
	pthread_detach(pthread_self());
	
	struct taxi *tx;
	int i=1;
	while(1){
 
		tx = malloc(sizeof(struct taxi));//车来了
		tx->num = i;
		printf("taxi %d comming\n",i);
		i++;
		pthread_mutex_lock(&lock);//互斥锁
 
		tx->next = Head;
		Head = tx;//Head永远指向最新到的车
		pthread_cond_signal(&hasTaxi);//发送信号有车了
 
 
		pthread_mutex_unlock(&lock);
		sleep(1);//就是为了释放cpu
	}
	pthread_exit(0);
}
 
void *takeTaxi(void *arg){//消费者线程
	printf("take taxi thread\n");
	pthread_detach(pthread_self());
	struct taxi *tx;
	while(1){
 
		pthread_mutex_lock(&lock);
		while(Head == NULL){//没有资源,继续等
			pthread_cond_wait(&hasTaxi,&lock);//前面必须有lock函数
		}
		tx =Head;
		Head = tx->next;//目前的状态是后来的车先走
 
		printf("Take taxi %d\n",tx->num);//哪个车走了
		free(tx);//我理解为释放车位
 
		pthread_mutex_unlock(&lock);
		
	}
 
	pthread_exit(0);
}
 
int main(int argc, const char *argv[])
{
	pthread_t tid1,tid2;
 
	pthread_create(&tid1,NULL,taxiarv,NULL);
	pthread_create(&tid2,NULL,takeTaxi,NULL);
	while(1){//不让主线程退出
		sleep(1);
	}
}

结果:

 

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值