18.守护进程和线程
18.守护进程和线程
学习目标
-
说出守护进程的特点
-
独立完成守护进程的创建
-
独立实现多个线程的创建
-
独立实现线程的退出和资源回收
-
理解线程同步的思想
1.守护进程
1.1守护进程介绍
Daemon(精灵)进程,是Linux中的后台服务进程,通常独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。一般采用以d结尾的名字,如vsftpd
守护进程是个特殊的孤儿进程,这种进程脱离终端,为什么要脱离终端呢?之所以脱离于终端是为了避免进程被任何终端所产生的信息所打断,其在执行过程中的信息也不在任何终端上显示。由于在 Linux 中,每一个系统与用户进行交流的界面称为终端,每一个从此终端开始运行的进程都会依附于这个终端,这个终端就称为这些进程的控制终端,当控制终端被关闭时,相应的进程都会自动关闭。
Linux后台的一些系统服务进程,没有控制终端,不能直接和用户交互。不受用户登录、注销的影响,一直在运行着,他们都是守护进程。如:预读入缓输出机制的实现;ftp服务器;nfs服务器等。
- 总结守护进程的特点:
▶Linux后台服务进程
▶独立于控制终端
▶周期性的执行某种任务
▶不受用户登陆和注销的影响
▶一般采用以d结尾的名字
1.2进程组和会话
- 进程组
▶进程组是一个或者多个进程的集合,每个进程都属于一个进程组,引入进程组是为了简化对进程的管理。当父进程创建子进程的时候,默认子进程与父进程属于同一个进程组。
进程组ID第一个进程ID(组长进程)。如父进程创建了多个子进程,父进程和多个子进程同属于一个组,而由于父进程是进程组里的第一个进程,所以父进程就是这个组的组长,组长ID是父进程ID。
ps -ef | wc -l
查看当前进程组进程个数
ps - ajx
查看进程组会话组ID
▶可以使用kill -SIGKILL -进程组ID(负的)来将整个进程组内的进程全部杀死。
▶只要进程组中有一个进程存在,进程组就存在,与组长进程是否终止无关。
▶进程组生存期:从进程组创建到最后一个进程离开
- 会话
▶一个会话是一个或多个进程组的集合。
▶创建会话的进程不能是进程组组长
▶创建会话的进程成为一个进程组的组长进程,同时也成为会话的会长。
▶需要有root权限(ubuntu不需要)
▶新创建的会话丢弃原有的控制终端
▶建立新会话时,先调用fork,父进程终止,子进程调用setsid函数
- 可以使用ps -ajx来查看进程组ID和会话ID
▶可以fork出几个子进程,然后查看进程组ID和会话ID
- 进程组和会话的关系图
1. 3创建守护进程的模型
第1步:fork子进程,父进程退出
▶子进程继承了父进程的进程组ID,但具有一个新的进程ID,这样就保证了子进程不是一个进程组的组长ID,这对于下面要做的setsid函数的调用是必要的前提条件
创建守护进程时,需要保证子进程不是一个进程组的组长ID,这是因为守护进程需要与终端分离,如果子进程成为了进程组的组长ID,那么在守护进程启动后,如果用户在终端使用Ctrl+C等信号结束了进程,那么整个进程组都会收到该信号,从而导致守护进程也被结束。因此,为了保证守护进程的正常运行,需要确保子进程不是一个进程组的组长ID。
第2步:子进程调用setsid函数创建新会话
▶调用这个函数以后
▷该进程成为新会话的首进程,是会话的会长
▷成为一个新进程组的组长进程,是进程组组长
▷不受控制终端的影响
第3步:改变当前工作目录chdir 可以省略
▶如:a.out在U盘上,启动这个程序,这个程序的当前的工作目录就是这个u盘,如果u盘拔掉后进程的当前工作目录将消失,a.out将不能正常工作。
第4步:重设文件掩码 mode & ~umask 可以省略
▶子进程会继承父进程的掩码
▶增加子进程程序操作的灵活性
▶umask(0000);
文件掩码主要用于根据用户的需求屏蔽掉不希望赋予其他用户的特定权限。例如,在Linux系统中,文件权限掩码可以通过设置屏蔽掉某些用户对文件的特定权限,如读取和写入等。这样可以防止文件被未经授权的用户修改,从而保护文件的内容不被篡改,避免数据丢失。
第5步:关闭文件描述符 可以省略
▶守护进程不受控制终端的影响所以可以关闭,以释放资源
▶close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);
第6步:执行核心工作
▶守护进程的核心代码逻辑
1.4练习
编写一个守护进程,每隔2S钟获取一次系统时间,并将这个时间写入磁盘文件。
分析:首先要按照1.3介绍的守护进行的步骤创建一个守护进程
题目要求每隔2S钟,所以需要一个定时器,2S钟触发一次,需要调用setitimer函数创建一个定时器,并且要捕获SIGALRM信号,然后在SIGALRM信号处理函数里面完成获取系统时间,然后将时间写入文件。
用到的主要知识点:
▶创建守护进程的模型
▶setitimer函数的使用
▶sigaction函数
▶文件I/O操作
▶获取系统时间函数time,将time_t类型转换为字符串ctime函数
编写一个守护进程,每隔2S钟获取一次系统时间,并将这个时间写入磁盘文件。
分析:首先要按照1.3介绍的守护进行的步骤创建一个守护进程。
每隔2S钟: 使用setitimer函数设置时钟,该时钟发送的是SIGALRM信号,
信号操作: 注册信号处理函数,signal或者sigaction,还有一个信号处理函数
获取一次系统时间: time函数的使用,ctime函数的使用
写入磁盘文件: 文件操作函数: open write close
//创建守护进程
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
#include <time.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/time.h>
void myfunc(int signo)
{
//打开文件
int fd = open("mydemon.log", O_RDWR | O_CREAT, 0755);
if (fd < 0)
{
return;
}
//获取当前的系统时间
time_t t;
time(&t);
char* p = ctime(&t);
//将时间写入文件
write(fd, p, strlen(p));
close(fd);
return;
}
int main()
{
//父进程fork子进程,然后父进程退出
pid_t pid = fork();
if (pid < 0 || pid>0)
{
exit(1);
}
//子进程调用setsid函数创建会话
setsid();
//改变当前的工作目录
chdir("/home/itcast/log");
//改变文件掩码
umask(0000);
//关闭标准输入,输出和错误输出文件描述符
close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);
//核心操作
//注册信号处理函数
struct sigaction act;
act.sa_handler = myfunc;
act.sa_flags = 0;
sigemptyset(&act.sa_mask);
sigaction(SIGALRM, &act, NULL);
//设置时钟
struct itimerval tm;
tm.it_interval.tv_sec = 2;
tm.it_interval.tv_usec = 0;
tm.it_value.tv_sec = 3;
tm.it_value.tv_usec = 0;
setitimer(ITIMER_REAL, &tm, NULL);
printf("hello world\n");
while (1)
{
sleep(1);
}
}
int fd = open("mydemon.log", O_RDWR | O_CREAT | O_APPEND, 0755);
//核心操作
//注册信号处理函数
struct sigaction act;
act.sa_handler = myfunc;
act.sa_flags = 0;
sigemptyset(&act.sa_mask);
sigaction(SIGALRM, &act, NULL);
这段代码的主要目的是设置一个信号处理器函数来响应 SIGALRM
信号。让我们逐行分析它:
-
struct sigaction act;
: 这声明了一个sigaction
结构,它用于描述如何处理特定的信号。 -
act.sa_handler = myfunc;
: 这设置myfunc
函数作为SIGALRM
信号的处理器。当SIGALRM
信号被接收时,myfunc
函数会被调用。 -
act.sa_flags = 0;
: 这设置信号处理的标志为0,表示没有特定的行为被指定。sigaction
结构有一个sa_flags
字段,可以用来指定各种控制信号处理的标志。 -
sigemptyset(&act.sa_mask);
: 这调用sigemptyset
函数来初始化act.sa_mask
信号集,确保其不包含任何信号。当信号处理器运行时,sa_mask
中的信号会被阻塞,防止它们被接收。在这里,我们确保没有额外的信号被阻塞。 -
sigaction(SIGALRM, &act, NULL);
: 最后,sigaction
函数被调用来为SIGALRM
信号设置处理行为。它将使用上面定义的act
结构来确定如何响应该信号。第三个参数(这里是NULL
)可以是一个指向旧sigaction
结构的指针,它在调用之前是如何设置的,但在这里我们不关心旧的行为,所以传递NULL
。
简而言之,这段代码的效果是:当程序收到 SIGALRM
信号时,它会调用 myfunc
函数来处理该信号。
2.线程
2.1 什么是线程
▶轻量级的进程(LWP:light weight process),在Linux环境下线程的本质仍是进程。
▶进程:拥有独立的地址空间,拥有PCB,相当于独居。
▶线程:有PCB,但没有独立的地址空间,多个线程共享进程空间,相当于合租。
- 在Linux操作系统下:
线程:最小的执行单位
▶进程:最小分配资源单位,可看成是只有一个线程的进程。
- 线程的特点
▶类Unix系统中,早期是没有“线程”概念的,80年代才引入,借助进程机制实现出了线程的概念。因此在这类系统中,进程和线程关系密切。
▶线程是轻量级进程(light-weight process),也有PCB,创建线程使用的底层函数和进程一样,都是clone
▶从内核里看进程和线程是一样的,都有各自不同的PCB.
▶进程可以蜕变成线程
▶在linux下,线程最是小的执行单位;进程是最小的分配资源单位
▶察看指定线程的LWP号:ps –Lf pid
实际上,无论是创建进程的fork,还是创建线程的pthread_create,底层实现都是调用同一个内核函数 clone。
▶如果复制对方的地址空间,那么就产出一个“进程”;
▶如果共享对方的地址空间,就产生一个“线程”。
so:Linux内核是不区分进程和线程的, 只在用户层面上进行区分。
所以,线程所有操作函数 pthread_* 是库函数,而非系统调用。
线程创建过程
2.2线程共享资源
▶文件描述符表
▶每种信号的处理方式
▶当前工作目录
▶用户ID和组ID
▶内存地址空间 (.text/.data/.bss/heap/共享库)
2.3 线程非共享资源
▶线程id
▶处理器现场和栈指针(内核栈)
▶独立的栈空间(用户空间栈)
▶errno变量
▶信号屏蔽字
▶调度优先级
2.4 线程优、缺点
- 优点:
▶提高程序并发性
▶开销小
▶数据通信、共享数据方便
- 缺点:
▶库函数,不稳定
▶gdb调试、编写困难
▶对信号支持不好
优点相对突出,缺点均不是硬伤。Linux下由于实现方法导致进程、线程差别不是很大。
2.5 pthread_create函数
- 函数作用:
▶创建一个新线程
- 函数原型
▶
int pthread_create(pthread_t* thread,
const pthread_attr_t* attr,
void* (*start_routine) (void*),
void* arg);
- 返回值
▶成功,返回0
▶失败,返回错误号
- 函数参数:
▶pthread_t:传出参数,保存系统为我们分配好的线程ID
▷当前Linux中可理解为:typedef unsigned long int pthread_t。
▶attr:通常传NULL,表示使用线程默认属性。若想使用具体属性也可以修改该参数。
▶start_routine:函数指针,指向线程主函数(线程体),该函数运行结束,则线程结束。
▶arg:线程主函数执行期间所使用的参数。
- 注意点
▶由于pthread_create的错误码不保存在errno中,因此不能直接用perror()打印错误信息,可以先用strerror()把错误码转换成错误信息再打印。
▶如果任意一个线程调用了exit或_exit,则整个进程的所有线程都终止,由于从main函数return也相当于调用exit,为了防止新创建的线程还没有得到执行就终止,我们在main函数return之前延时1秒,这只是一种权宜之计,即使主线程等待1秒,内核也不一定会调度新创建的线程执行,下一节我们会看到更好的办法。
- 练习题:
1.编写程序创建一个线程。
2.编写程序创建一个线程,并给线程传递一个int参数
3.编写程序创建一个线程,并给线程传递一个结构体参数。
4.编写程序,主线程循环创建5个子线程,并让子线程判断自己是第几个子线程。
练习4分析:最后每个子线程打印出来的值并不是想象中的值,比如都是5,分析其原因:
在创建子线程的时候使用循环因子作为参数传递给子线程,这样主线程和多个子线程就会共享变量i(变量i在main函数中定义,在整个进程都一直有效)所以在子线程看来变量i是合法的栈内存空间。
那么为什么最后每个子线程打印出来的值都是5呢?
是由于主线程可能会在一个cpu时间片内连续创建了5个子线程,此时变量i的值变成了5,当主线程失去cpu的时间片后,子线程得到cpu的时间片,子线程访问的是变量i的内存空间的值,所以打印出来值为5。
创建子线程
例子1:
//创建子线程
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <pthread.h>
//线程执行函数
void *mythread(void *arg)
{
printf("child thread, pid==[%d], id==[%ld]\n", getpid(), pthread_self());
}
int main()
{
//int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
// void *(*start_routine) (void *), void *arg);
//创建子线程
pthread_t thread;
int ret = pthread_create(&thread, NULL, mythread, NULL);
if(ret!=0)
{
printf("pthread_create error, [%s]\n", strerror(ret));
return -1;
}
printf("main thread, pid==[%d], id==[%ld]\n", getpid(), pthread_self());
//目的是为了让子线程能够执行起来
sleep(1);
return 0;
}
这个程序的执行逻辑可以分解为以下步骤:
-
程序开始:
当你运行这个程序时,首先会进入
main()
函数。 -
线程创建:
在
main()
函数内部,使用pthread_create
函数创建一个新的子线程。- 这个子线程的执行体是
mythread
函数。 - 主线程与新创建的子线程将并行执行。
- 这个子线程的执行体是
-
检查线程创建状态:
pthread_create
返回的结果被存储在ret
中。如果线程创建失败,ret
会是一个非零值,此时程序会打印出错误信息。 -
主线程继续执行:
如果线程成功创建,主线程将继续执行并打印主线程的进程ID和线程ID。
-
子线程开始执行:
新创建的子线程将执行
mythread
函数,打印子线程的进程ID和线程ID。注意,子线程与主线程的进程ID是相同的,因为它们都处于同一个进程内,但线程ID是不同的。 -
程序暂停:
主线程中的
sleep(1)
使主线程暂停1秒。这是为了确保子线程有足够的时间执行和完成其任务。如果没有这个延迟,主线程可能会先于子线程结束,导致子线程可能不会打印其输出。 -
程序结束:
在
sleep(1)
之后,主线程继续执行并达到main()
的末尾,然后程序结束。
总结:这个程序的主要目的是演示如何在C语言中使用pthreads
库创建一个新的线程,并在两个线程(主线程和新创建的子线程)中并行执行。主线程和子线程都打印它们的进程ID和线程ID,以显示它们在同一个进程中但是是两个不同的线程。
线程创建的时候传递参数
例子2:
//创建子线程: 传递参数
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <pthread.h>
//线程执行函数
void* mythread(void* arg)
{
int n = *(int *)arg;
printf("n==[%d]\n", n);
printf("child thread, pid==[%d], id==[%ld]\n", getpid(), pthread_self());
}
int main()
{
int n = 99;
//int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
// void *(*start_routine) (void *), void *arg);
//创建子线程
pthread_t thread;
int ret = pthread_create(&thread, NULL, mythread, &n);
if (ret != 0)
{
printf("pthread_create error, [%s]\n", strerror(ret));
return -1;
}
printf("main thread, pid==[%d], id==[%ld]\n", getpid(), pthread_self());
//目的是为了让子线程能够执行起来
sleep(1);
return 0;
}
例子3:
//创建子线程: 传递参数
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <pthread.h>
struct Test
{
int data;
char name[64];
};
//线程执行函数
void* mythread(void* arg)
{
struct Test* p = (struct Test*)arg;
printf("[%d][%s]\n", p->data, p->name);
printf("child thread, pid==[%d], id==[%ld]\n", getpid(), pthread_self());
}
int main()
{
int n = 99;
struct Test t;
memset(&t, 0x00, sizeof(struct Test));
t.data = 88;
strcpy(t.name, "xiaowen");
//int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
// void *(*start_routine) (void *), void *arg);
//创建子线程
pthread_t thread;
int ret = pthread_create(&thread, NULL, mythread, &t);
if (ret != 0)
{
printf("pthread_create error, [%s]\n", strerror(ret));
return -1;
}
printf("main thread, pid==[%d], id==[%ld]\n", getpid(), pthread_self());
//目的是为了让子线程能够执行起来
sleep(1);
return 0;
}
线程执行函数什么时候被调用?
在给定的代码中,线程执行函数(名为 mythread
)是在主线程中调用 pthread_create
函数时启动的。
具体来说,当你调用 pthread_create(&thread, NULL, mythread, NULL);
,以下事件发生:
- 系统为新线程分配所需的资源。
- 新线程开始执行,并从指定的线程函数,即
mythread
,开始其执行。 mythread
函数开始运行,并执行其中的代码。在这种情况下,它将打印子线程的进程ID和线程ID。
需要注意的是,新创建的线程(在这种情况下是 mythread
)和主线程将同时运行,除非它们被特定的同步机制阻止。这意味着,mythread
函数可以在主线程继续执行其余代码之前,之后或几乎同时开始执行。因此,输出的顺序(主线程的打印和 mythread
的打印)可能会因运行而异,除非有同步机制来控制这种行为。
在这个特定的代码示例中,sleep(1)
在 main
函数中被用作一种简单的方法,以确保主线程不会立即退出,从而给新线程足够的时间来执行和打印其消息。
如果子线程开始运行,直接执行线程执行函数吗?
是的,当子线程(或更准确地说,新创建的线程)开始运行,它将直接执行作为 pthread_create
的第三个参数传递的线程执行函数。在你给出的代码示例中,这个函数是 mythread
。
一旦 pthread_create
被调用,并且线程成功创建,新线程将从指定的线程执行函数的开始处开始执行。当这个函数执行完毕并返回时,线程就结束了。
这意味着线程的生命周期是由其线程执行函数的开始到结束来定义的。一旦线程执行函数完成,线程就会自动退出。如果需要在函数结束后仍然保留线程或等待其他线程,你需要使用其他的pthread函数和同步机制,如 pthread_join
。
需要注意的是,线程的启动时间、执行速度和调度是由操作系统的线程调度器来决定的,因此新线程何时具体开始执行其线程执行函数可能会有一些延迟,这取决于多种因素,如系统的负载、其他进程和线程的活动等。
循环创建多个子线程
//循环创建子线程,并且打印是第几个子线程
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <pthread.h>
//线程执行函数
void* mythread(void* arg)
{
int i = *(int*)arg;
printf("[%d]:child thread, pid==[%d], id==[%ld]\n", i, getpid(), pthread_self());
}
int main()
{
//int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
// void *(*start_routine) (void *), void *arg);
//创建子线程
int ret;
int i = 0;
int n = 5;
pthread_t thread[5];
for (i = 0; i < n; i++)
{
ret = pthread_create(&thread[i], NULL, mythread, &i);
if (ret != 0)
{
printf("pthread_create error, [%s]\n", strerror(ret));
return -1;
}
}
printf("main thread, pid==[%d], id==[%ld]\n", getpid(), pthread_self());
//目的是为了让子线程能够执行起来
sleep(1);
return 0;
}
写个脚本来方便编译
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day8$ vi pmak
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day8$ chmod +x pmak
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day8$ ./pmak pthcreate1
gcc -o $1 $1.c -lpthread
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day8$ ls -ltr
总计 172
-rw-rw-r-- 1 cmt cmt 943 10月 21 20:52 mmap1.c
-rwxrwxr-x 1 cmt cmt 16296 10月 21 20:52 mmap1
-rwxrwxr-x 1 cmt cmt 16136 10月 21 21:42 mmap_write
-rw-rw-r-- 1 cmt cmt 55 10月 21 21:42 test.log
-rwxrwxr-x 1 cmt cmt 16272 10月 21 22:04 mmap_read
-rw-rw-r-- 1 cmt cmt 796 10月 21 22:06 mmap_read.c
-rw-rw-r-- 1 cmt cmt 727 10月 21 22:07 mmap_write.c
-rw-rw-r-- 1 cmt cmt 804 10月 22 10:35 setitimer.c
-rwxrwxr-x 1 cmt cmt 16136 10月 22 10:35 setitimer
-rw-rw-r-- 1 cmt cmt 739 10月 22 20:41 sigset.c
-rwxrwxr-x 1 cmt cmt 16192 10月 22 20:41 sigset
-rw-rw-r-- 1 cmt cmt 1310 10月 25 10:55 mydeamon.c
-rwxrwxr-x 1 cmt cmt 16544 10月 25 10:55 mydeamon
-rw-rw-r-- 1 cmt cmt 784 10月 25 19:57 pthread_create.c
-rwxrwxr-x 1 cmt cmt 16232 10月 25 19:58 a.out
-rwxrwxr-x 1 cmt cmt 25 10月 25 21:17 pmak
-rw-rw-r-- 1 cmt cmt 949 10月 25 21:19 pthcreate1.c
-rwxrwxr-x 1 cmt cmt 16280 10月 25 21:19 pthcreate1
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day8$ ./pthcreate1
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day8$ ./pthcreate1
main thread, pid==[16561],id==[139640113907520]
[5]:child thread,pid==[16561],id==[139640094840512]
[5]:child thread,pid==[16561],id==[139640086447808]
[5]:child thread,pid==[16561],id==[139640078055104]
[5]:child thread,pid==[16561],id==[139640103233216]
[5]:child thread,pid==[16561],id==[139640111625920]
//循环创建子线程,并且打印是第几个子线程
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>-
#include <unistd.h>
#include <pthread.h>
//线程执行函数
void* mythread(void* arg)
{
int i = *(int*)arg;
printf("[%d]:child thread, pid==[%d], id==[%ld]\n", i, getpid(), pthread_self());
}
int main()
{
//int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
// void *(*start_routine) (void *), void *arg);
//创建子线程
int ret;
int i = 0;
int n = 5;
pthread_t thread[5];
for (i = 0; i < n; i++)
{
ret = pthread_create(&thread[i], NULL, mythread, &i);
if (ret != 0)
{
printf("pthread_create error, [%s]\n", strerror(ret));
return -1;
}
sleep(1);
}
printf("main thread, pid==[%d], id==[%ld]\n", getpid(), pthread_self());
//目的是为了让子线程能够执行起来
sleep(1);
return 0;
}
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day8$ ./pmak pthcreate1
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day8$ ./pthcreate1
[0]:child thread,pid==[16615],id==[140263225816768]
[1]:child thread,pid==[16615],id==[140263217424064]
[2]:child thread,pid==[16615],id==[140263209031360]
[3]:child thread,pid==[16615],id==[140263200638656]
[4]:child thread,pid==[16615],id==[140263192245952]
main thread, pid==[16615],id==[140263228868416]
//循环创建子线程,并且打印是第几个子线程
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <pthread.h>
//线程执行函数
void* mythread(void* arg)
{
int i = *(int*)arg;
printf("[%d]:child thread, pid==[%d], id==[%ld]\n", i, getpid(), pthread_self());
}
int main()
{
//int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
// void *(*start_routine) (void *), void *arg);
//创建子线程
int ret;
int i = 0;
int n = 5;
int arr[5];
pthread_t thread[5];
for (i = 0; i < n; i++)
{
arr[i] = i;
ret = pthread_create(&thread[i], NULL, mythread, &arr[i]);
if (ret != 0)
{
printf("pthread_create error, [%s]\n", strerror(ret));
return -1;
}
}
printf("main thread, pid==[%d], id==[%ld]\n", getpid(), pthread_self());
//目的是为了让子线程能够执行起来
sleep(1);
return 0;
}
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day8$ ./pmak pthcreate1
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day8$ ./pthcreate1
main thread, pid==[16684],id==[139781395973952]
[2]:child thread,pid==[16684],id==[139781375776448]
[3]:child thread,pid==[16684],id==[139781367383744]
[4]:child thread,pid==[16684],id==[139781358991040]
[1]:child thread,pid==[16684],id==[139781384169152]
[0]:child thread,pid==[16684],id==[139781392561856]
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day8$ ./pthcreate1
main thread, pid==[16720],id==[139951972595520]
[4]:child thread,pid==[16720],id==[139951935043264]
[3]:child thread,pid==[16720],id==[139951943435968]
[2]:child thread,pid==[16720],id==[139951951828672]
[1]:child thread,pid==[16720],id==[139951960221376]
[0]:child thread,pid==[16720],id==[139951968614080]
2.6 pthread_exit函数
在线程中禁止调用exit函数,否则会导致整个进程退出,取而代之的是调用pthread_exit函数,这个函数是使一个线程退出,如果主线程调用pthread_exit函数也不会使整个进程退出,不影响其他线程的执行。
- 函数描述
▶将单个线程退出
- 函数原型
▶void pthread_exit(void *retval);
- 函数参数
▶retval表示线程退出状态,通常传NULL
另注意,pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了,栈空间就会被回收。
练习:编写程序测试pthread_exit函数使一个线程退出。
通过程序测试得知,pthread_exit函数只是使一个线程退出,假如子线程里面调用了exit函数,会使整个进程终止;如果主线程调用了pthread_exit函数,并不影响子线程,只是使主线程自己退出。
//创建子线程
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <sys/time.h>
#include <fcntl.h>
#include <pthread.h>
void *mythread(void *arg)
{
printf("child thread,pid==[%d],id==[%ld]\n", getpid(), pthread_self());
pthread_exit(NULL);
}
int main()
{
//int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
// void *(*start_routine) (void *), void *arg);
pthread_t thread;
int ret = pthread_create(&thread, NULL, mythread, NULL);
if(ret != 0)
{
printf("pthread_create error,[%s]\n", strerror(ret));
return -1;
}
printf("main thread, pid==[%d],id==[%ld]\n", getpid(), pthread_self());
sleep(10);
return 0;
}
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day8$ ./pmak pthread_exit
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day8$ ./pthread_exit
main thread, pid==[18730],id==[140596543588160]
child thread,pid==[18730],id==[140596540864192]
pmak
gcc -o $1 $1.c -lpthread
终端2
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day8$ ps -ef
cmt 18761 4438 0 10:32 pts/0 00:00:00 ./pthread_exit//主线程活着
cmt 18763 5994 0 10:32 pts/2 00:00:00 ps -ef
//循环创建子线程,并且打印是第几个子线程
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <pthread.h>
//线程执行函数
void* mythread(void* arg)
{
int i = *(int*)arg;
printf("[%d]:child thread, pid==[%d], id==[%ld]\n", i, getpid(), pthread_self());
sleep(100);
}
int main()
{
//int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
// void *(*start_routine) (void *), void *arg);
//创建子线程
int ret;
int i = 0;
int n = 5;
int arr[5];
pthread_t thread[5];
for (i = 0; i < n; i++)
{
arr[i] = i;
ret = pthread_create(&thread[i], NULL, mythread, &arr[i]);
if (ret != 0)
{
printf("pthread_create error, [%s]\n", strerror(ret));
return -1;
}
}
printf("main thread, pid==[%d], id==[%ld]\n", getpid(), pthread_self());
//目的是为了让子线程能够执行起来
sleep(100);
return 0;
}
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day8$ ./pmak pthread_exit
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day8$ ./pthread_exit
main thread, pid==[24806],id==[140648504678208]
[2]:child thread,pid==[24806],id==[140648485213888]
[3]:child thread,pid==[24806],id==[140648476821184]
[4]:child thread,pid==[24806],id==[140648468428480]
[1]:child thread,pid==[24806],id==[140648493606592]
[0]:child thread,pid==[24806],id==[140648501999296]
ps -ef
查看进程里有几个线程
cmt 24806 4438 0 21:06 pts/0 00:00:00 ./pthread_exit
cmt 24812 5994 50 21:07 pts/2 00:00:00 ps -ef
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day8$ ps -Lf 24806
UID PID PPID LWP C NLWP STIME TTY STAT TIME CMD
cmt 24806 4438 24806 0 6 21:06 pts/0 Sl+ 0:00 ./pthread_exit
cmt 24806 4438 24807 0 6 21:06 pts/0 Sl+ 0:00 ./pthread_exit
cmt 24806 4438 24808 0 6 21:06 pts/0 Sl+ 0:00 ./pthread_exit
cmt 24806 4438 24809 0 6 21:06 pts/0 Sl+ 0:00 ./pthread_exit
cmt 24806 4438 24810 0 6 21:06 pts/0 Sl+ 0:00 ./pthread_exit
cmt 24806 4438 24811 0 6 21:06 pts/0 Sl+ 0:00 ./pthread_exit
//循环创建子线程,并且打印是第几个子线程
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <pthread.h>
//线程执行函数
void* mythread(void* arg)
{
printf("child thread, pid==[%d], id==[%ld]\n", getpid(), pthread_self());
sleep(100);
}
int main()
{
//int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
// void *(*start_routine) (void *), void *arg);
//创建子线程
pthread_t thread;
int ret = pthread_create(&thread, NULL, mythread, NULL);
if (ret != 0)
{
printf("pthread_create error, [%s]\n", strerror(ret));
return -1;
}
printf("main thread, pid==[%d], id==[%ld]\n", getpid(), pthread_self());
//目的是为了让子线程能够执行起来
pthread_exit(NULL);
sleep(10);
return 0;
}
看看主线程退出之后,看看子线程有没有活着,如果子线程死了,意味着整个进程结束了,如果还活着说明不影响其他子线程
ps -ef
UID下面两个线程 PID为4600的两个已经变成僵尸线程
白色条一栏是主线程,已经退出,主线程变成僵尸进程
主线程退出并没有使整个进程退出
整个进程没有退出,因为还有子线程活着,只有当最后一个线程退出,整个进程空间才能被回收
2.7 pthread_join函数
-
函数描述:阻塞等待线程退出,获取线程退出状态。其作用,对应进程中的waitpid() 函数。
-
函数原型:
int pthread_join(pthread_t thread, void **retval);
-
函数返回值:
▶成功:0;
▶失败:错误号
- 函数参数:
▶thread:线程ID
▶retval:存储线程结束状态,整个指针和pthread_exit的参数是同一块内存地址。(传出参数)
练习:编写程序,使主线程获取子线程的退出状态。
一般先定义void *ptr; 然后pthread_join(threadid, &ptr);
//循环创建子线程,并且打印是第几个子线程
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <pthread.h>
int g_val = 9;
//线程执行函数
void* mythread(void* arg)
{
printf("child thread, pid==[%d], id==[%ld]\n", getpid(), pthread_self());
printf("%p\n", &g_val);
pthread_exit(&g_val);
}
int main()
{
//int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
// void *(*start_routine) (void *), void *arg);
//创建子线程
pthread_t thread;
ret = pthread_create(&thread, NULL, mythread, NULL);
if (ret != 0)
{
printf("pthread_create error, [%s]\n", strerror(ret));
return -1;
}
printf("main thread, pid==[%d], id==[%ld]\n", getpid(), pthread_self());
//回收子线程
void* p = NULL;
pthread_join(thread, &p);
int n = *(int*)p;
printf("child exit status:[%d], [%p]\n", n, p);
return 0;
}
以下是此代码的功能和执行流程的简要概述:
这段代码展示了如何在C语言中使用POSIX线程(也称为pthreads)创建一个新的线程、运行它,并在线程完成后回收它。下面是代码的详细解释:
-
初始化全局变量:
int g_val = 9;
:此全局变量的地址和值在稍后将被使用。
-
线程执行函数
mythread
:- 当新线程开始时,它会从这个函数开始执行。
- 打印子线程的进程ID和线程ID。
- 打印全局变量
g_val
的地址。 - 使用
pthread_exit
结束线程,并返回一个指向g_val
的指针。
-
main
函数:- 定义线程ID变量
thread
和整数变量ret
。 - 使用
pthread_create
函数创建一个新线程。此新线程从mythread
函数开始执行。 - 检查
pthread_create
的返回值以确定线程是否成功创建。如果返回值不为0,函数会返回一个错误代码,此代码可以使用strerror
函数转换为可读的错误消息。 - 主线程中打印主线程的进程ID和线程ID。
- 使用
pthread_join
阻塞主线程,等待子线程结束,并获取子线程返回的指针(即g_val
的地址)。 - 获取该指针指向的整数值并打印。
- 主线程结束。
- 定义线程ID变量
需要注意的是,这段代码的注释提到“循环创建子线程”,但实际上此代码只创建了一个子线程。如果你打算循环创建多个线程,你需要将 pthread_create
和相关的代码置于一个循环中。
//线程退出函数测试
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <pthread.h>
struct Test
{
int data;
char name[64];
};
int g_var = 9;
struct Test t;
//线程执行函数
void *mythread(void *arg)
{
printf("child thread, pid==[%d], id==[%ld]\n", getpid(), pthread_self());
//printf("[%p]\n", &g_var);
//pthread_exit(&g_var);
memset(&t, 0x00, sizeof(t));
t.data = 99;
strcpy(t.name, "xiaowen");
pthread_exit(&t);
}
int main()
{
//int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
// void *(*start_routine) (void *), void *arg);
//创建子线程
pthread_t thread;
int ret = pthread_create(&thread, NULL, mythread, NULL);
if(ret!=0)
{
printf("pthread_create error, [%s]\n", strerror(ret));
return -1;
}
printf("main thread, pid==[%d], id==[%ld]\n", getpid(), pthread_self());
//回收子线程
void *p = NULL;
pthread_join(thread, &p);
//int n = *(int *)p;
struct Test *pt = (struct Test *)p;
printf("child exit status:[%d],[%s],[%p]\n", pt->data, pt->name, p);
return 0;
}
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day8$ ./pmak pthread_exit
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day8$ ./pthread_exit
main thread, pid==[25058], id==[140411699648320]
child thread, pid==[25058], id==[140411695789760]
child exit status:[99],[xiaowen],[0x559e95779040]
2.8 pthread_detach函数
线程分离状态:指定该状态,线程主动与主控线程断开关系。线程结束后,其退出状态不由其他线程获取,而直接自己自动释放。网络、多线程服务器常用。
进程若有该机制,将不会产生僵尸进程。僵尸进程的产生主要由于进程死后,大部分资源被释放,一点残留资源仍存于系统中,导致内核认为该进程仍存在。
也可使用 pthread_create函数参2(线程属性)来设置线程分离。pthread_detach函数是在创建线程之后调用的。
- 函数描述
▶实现线程分离
- 函数原型
▶int pthread_detach(pthread_t thread);
- 函数返回值
▶成功:0;
▶失败:错误号
一般情况下,线程终止后,其终止状态一直保留到其它线程调用pthread_join获取它的状态为止。但是线程也可以被置为detach状态,这样的线程一旦终止就立刻回收它占用的所有资源,而不保留终止状态。不能对一个已经处于detach状态的线程调用pthread_join,这样的调用将返回EINVAL错误。也就是说,如果已经对一个线程调用了pthread_detach就不能再调用pthread_join了。
练习:编写程序,在创建线程之后设置线程的分离状态。
说明:如果线程已经设置了分离状态,则再调用pthread_join就会失败,可用这个方法验证是否已成功设置分离状态。
//设置子线程为分离属性
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <pthread.h>
//线程执行函数
void* mythread(void* arg)
{
printf("child thread, pid==[%d], id==[%ld]\n", getpid(), pthread_self());
}
int main()
{
//int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
// void *(*start_routine) (void *), void *arg);
//创建子线程
pthread_t thread;
int ret = pthread_create(&thread, NULL, mythread, NULL);
if (ret != 0)
{
printf("pthread_create error, [%s]\n", strerror(ret));
return -1;
}
printf("main thread, pid==[%d], id==[%ld]\n", getpid(), pthread_self());
//设置线程为分离属性
pthread_detach(thread);
//子线程设置分离属性,则pthread_join不再阻塞,立刻返回
ret = pthread_join(thread, NULL);//子线程资源被回收了,不再返回
if (ret != 0)
{
printf("pthread_join error:[%s]\n", strerror(ret));
}
//目的是为了让子线程能够执行起来
sleep(1);
return 0;
}
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day8$ ./pmak pthread_detach
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day8$ ./pthread_detach
main thread, pid==[3878],id==[140566855399232]
pthread_join error:[Invalid argument]
child thread,pid==[3878],id==[140566851483328]
//设置子线程为分离属性
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <pthread.h>
//线程执行函数
void* mythread(void* arg)
{
printf("child thread, pid==[%d], id==[%ld]\n", getpid(), pthread_self());
sleep(10);
}
int main()
{
//int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
// void *(*start_routine) (void *), void *arg);
//创建子线程
pthread_t thread;
int ret = pthread_create(&thread, NULL, mythread, NULL);
if (ret != 0)
{
printf("pthread_create error, [%s]\n", strerror(ret));
return -1;
}
printf("main thread, pid==[%d], id==[%ld]\n", getpid(), pthread_self());
//设置线程为分离属性
//pthread_detach(thread);
//子线程设置分离属性,则pthread_join不再阻塞,立刻返回
ret = pthread_join(thread, NULL);
if (ret != 0)
{
printf("pthread_join error:[%s]\n", strerror(ret));
}
//目的是为了让子线程能够执行起来
sleep(1);
return 0;
}
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day8$ ./pmak pthread_detach
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day8$ ./pthread_detach
main thread,pid==[4284],id==[140676128376640]
child pthread,pid==[4284],id==[140676125685440]
pthread_join函数阻塞了
这段代码演示了如何在C语言中使用POSIX线程(也称为pthreads)创建一个新线程,然后将其设置为分离状态,并试图使用pthread_join
来回收它。下面是代码的详细解释:
-
线程执行函数
mythread
:- 定义了线程的入口点函数。当新线程开始时,它会从这个函数开始执行。
- 打印子线程的进程ID和线程ID。
- 在函数结束时,线程也会结束。
-
main
函数:a. 创建线程:
- 定义线程ID变量thread
和整数变量ret
。
- 使用pthread_create
函数创建一个新线程。此新线程从mythread
函数开始执行。
- 检查pthread_create
的返回值以确定线程是否成功创建。如果返回值不为0,函数会返回一个错误代码,此代码可以使用strerror
函数转换为可读的错误消息。
- 主线程中打印主线程的进程ID和线程ID。b. 设置线程为分离属性:
- 使用pthread_detach
函数将线程thread
设置为分离状态。线程在分离状态下一旦完成其执行,其资源会立即被系统回收,而不需要其他线程对其进行回收。c. 尝试回收子线程:
- 尽管线程已经被设置为分离状态,代码仍然试图使用pthread_join
来回收它。
- 因为线程是分离的,所以pthread_join
会立即返回,并设置ret
为一个错误值,这表示线程不可以被连接。
- 使用strerror
函数将此错误代码转换为可读的错误消息并打印。d. 让主线程休眠:
- 主线程使用sleep(1)
休眠1秒。这是为了确保子线程有足够的时间来执行并打印其消息。因为子线程和主线程是并发运行的,如果主线程太快结束,子线程可能不会有机会运行。
- 主线程结束。
需要注意的是,使用pthread_detach
和pthread_join
的组合方式在实际情况中是不常见的。通常,线程要么被设置为可连接的并由其他线程使用pthread_join
来回收,要么被设置为分离的并自动结束。在这个例子中,代码试图演示这两种技术的组合会导致什么,尤其是尝试回收一个已经被设置为分离状态的线程会发生什么。
2.9 pthread_cancel函数
- 函数描述
▶杀死(取消)线程。其作用,对应进程中 kill() 函数。
- 函数原型
▶int pthread_cancel(pthread_t thread);
- 函数返回值
▶成功:0;
▶失败:错误号
【注意】:线程的取消并不是实时的,而有一定的延时。需要等待线程到达某个取消点(检查点)。调用进入内核。
类似于玩游戏存档,必须到达指定的场所(存档点,如:客栈、仓库、城里等)才能存储进度。杀死线程也不是立刻就能完成,必须要到达取消点。
取消点:是线程检查是否被取消,并按请求进行动作的一个位置。通常是一些系统调用creat,open,pause,close,read,write..... 执行命令man 7 pthreads可以查看具备这些取消点的系统调用列表。可粗略认为一个系统调用(进入内核)即为一个取消点。还以通过调用pthread_testcancel函数设置一个取消点。
▶函数原型:void pthread_testcancel(void);
练习:编写程序,让主线程取消子线程的执行。
先测试一下没有取消点看看能否使线程取消;然后调用pthread_testcancel设置一个取消点,看看能够使线程取消。
//创建子线程
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <pthread.h>
//线程执行函数
void* mythread(void* arg)
{
while (1)
{
int a;
int b;
}
}
int main()
{
//int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
// void *(*start_routine) (void *), void *arg);
//创建子线程
pthread_t thread;
int ret = pthread_create(&thread, NULL, mythread, NULL);
if (ret != 0)
{
printf("pthread_create error, [%s]\n", strerror(ret));
return -1;
}
printf("main thread, pid==[%d], id==[%ld]\n", getpid(), pthread_self());
//取消子线程
pthread_cancel(thread);
pthread_join(thread, NULL);
return 0;
}
终端1
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day8$ ./pmak pthread_cancel
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day8$ ./pthread_cancel
main thread, pid==[4763],id==[139647815903040]
终端2
ps -ef
cmt 4763 3327 88 18:11 pts/0 00:00:03 ./pthread_cancel
cmt 4765 4027 0 18:11 pts/1 00:00:00 ps -ef
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day8$ ps -Lf 4763
UID PID PPID LWP C NLWP STIME TTY STAT TIME CMD
cmt 4763 3327 4763 92 2 18:11 pts/0 Sl+ 0:13 ./pthread_cancel
cmt 4763 3327 4764 92 2 18:11 pts/0 Rl+ 0:13 ./pthread_cancel
2个线程,子线程还活着,因为子线程没有取消点
//创建子线程
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <pthread.h>
//线程执行函数
void* mythread(void* arg)
{
while (1)
{
int a;
int b;
//设置取消点
pthread_testcancel();
}
}
int main()
{
//int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
// void *(*start_routine) (void *), void *arg);
//创建子线程
pthread_t thread;
int ret = pthread_create(&thread, NULL, mythread, NULL);
if (ret != 0)
{
printf("pthread_create error, [%s]\n", strerror(ret));
return -1;
}
printf("main thread, pid==[%d], id==[%ld]\n", getpid(), pthread_self());
//取消子线程
pthread_cancel(thread);
pthread_join(thread, NULL);//子进程死了,直接返回
return 0;
}
mt@cmt-VMware-Virtual-Platform:~/C_Program/day8$ ./pmak pthread_cancel
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day8$ ./pthread_cancel
main thread, pid==[4776],id==[139902499075904]
//创建子线程
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <pthread.h>
//线程执行函数
void* mythread(void* arg)
{
while (1)
{
int a;
int b;
printf("-----\n");
}
}
int main()
{
//int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
// void *(*start_routine) (void *), void *arg);
//创建子线程
pthread_t thread;
int ret = pthread_create(&thread, NULL, mythread, NULL);
if (ret != 0)
{
printf("pthread_create error, [%s]\n", strerror(ret));
return -1;
}
printf("main thread, pid==[%d], id==[%ld]\n", getpid(), pthread_self());
//取消子线程
pthread_cancel(thread);
pthread_join(thread, NULL);
return 0;
}
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day8$ ./pmak pthread_cancel
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day8$ ./pthread_cancel
main thread, pid==[4798],id==[140359798429504]
----
这个程序的目的是创建一个子线程,并让这个子线程在其执行函数mythread
中不断打印“-----”。然后在main
线程中取消了这个子线程,并等待它结束。
让我们分析为什么printf
在mythread
中只执行了一次:
-
缓存:首先要明白
printf
有一个缓存机制。通常,输出不是立即显示,而是存储在一个缓冲区中,直到缓冲区满或遇到换行符时才会被实际输出。在mythread
函数中,printf("-----\n");
有一个换行符,这意味着每次调用都应该立即刷新缓冲区并显示输出。但在某些环境中,可能需要额外的刷新机制。 -
线程取消:在
main
线程中,您几乎立即取消了子线程,使用了pthread_cancel(thread);
。这意味着子线程可能仅有很短的时间来执行其任务。因此,如果子线程只有时间执行printf
一次就被取消了,这也是可能的。 -
线程取消点:线程不会在任意位置被取消,它们只会在特定的“取消点”被取消。
printf
可能是一个取消点,这意味着在printf
执行时,线程可能会被取消。
结论:
由于printf
有缓存机制,加上子线程被几乎立即取消,这可能是导致printf
在子线程中只执行一次的原因。如果您希望看到多次输出,可以考虑在pthread_cancel
之前稍微延迟一下,例如使用sleep(1);
。但即使这样,输出的次数也不能保证,因为线程的执行是由操作系统调度的,而且存在上下文切换和其他一些因素的影响。
2.10 pthread_equal函数
- 函数描述:
▶比较两个线程ID是否相等。
- 函数原型
▶int pthread_equal(pthread_t t1, pthread_t t2);
注意:这个函数是为了以能够扩展使用的, 有可能Linux在未来线程ID pthread_t 类型被修改为结构体实现。
//比较线程ID是否相等
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <pthread.h>
//线程执行函数
void* mythread(void* arg)
{
printf("child thread, pid==[%d], id==[%ld]\n", getpid(), pthread_self());
}
int main()
{
//int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
// void *(*start_routine) (void *), void *arg);
//创建子线程
pthread_t thread;
int ret = pthread_create(&thread, NULL, mythread, NULL);
if (ret != 0)
{
printf("pthread_create error, [%s]\n", strerror(ret));
return -1;
}
printf("main thread, pid==[%d], id==[%ld]\n", getpid(), pthread_self());
//比较线程ID
if (pthread_equal(thread, pthread_self()) != 0)
{
printf("two thread id is same\n");
}0
else
{
printf("two thread id is not same\n");
}
//目的是为了让子线程能够执行起来
sleep(1);
return 0;
}
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day8$ ./pmak pthread_equal
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day8$ ./pthread_equal
main thread, pid==[4833],id==[140349128791872]
two thread id is not same
child thread,pid==[4833],id==[140349125162688]
2.11 进程函数和线程函数比较
进程 | 线程 |
---|---|
fork | pthread_create |
exit | pthread_exit |
wait/waitpid | pthread_join |
kill | pthread_cancel |
getpid | pthread_self |
3.线程属性
linux下线程的属性是可以根据实际项目需要,进行设置,之前讨论的线程都是采用线程的默认属性,默认属性已经可以解决绝大多数开发时遇到的问题,如果对程序的性能提出更高的要求,则需要设置线程属性,本节以设置线程的分离属性为例讲解设置线程属性。
- 线程的分离状态决定一个线程以什么样的方式来终止自己,有两种状态:
▶非分离状态:线程的默认属性是非分离状态,这种情况下,原有的线程等待创建的线程结束。只有当pthread_join()函数返回时,创建的线程才算终止,才能释放自己占用的系统资源。
▶分离状态:分离线程没有被其他的线程所等待,自己运行结束了,线程也就终止了,马上释放系统资源。应该根据自己的需要,选择适当的分离状态。
- 设置线程属性分为以下步骤
第1步:定义线程属性类型类型的变量
▶pthread_attr_t attr;
第2步:对线程属性变量进行初始化
▶int pthread_attr_init (pthread_attr_t* attr);
第3步:设置线程为分离属性
▶int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
▷参数:
◆attr: 线程属性
◆detachstate:
◇PTHREAD_CREATE_DETACHED(分离)
◇PTHREAD_CREATE_JOINABLE(非分离)
注意:这一步完成之后调用pthread_create函数创建线程,则创建出来的线程就是分离线程;其实上述三步就是pthread_create的第二个参数做准备工作。
第4步:释放线程属性资源
▶int pthread_attr_destroy(pthread_attr_t *attr);
参数:线程属性
练习:编写程序,创建一个分离属性的线程。
验证:设置为分离属性的线程是不能够被pthread_join函数回收的,可以通过调用pthread_join函数测试该线程是否已经是分离属性的线程。
//在创建子线程的时候设置分离属性
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <pthread.h>
//线程执行函数
void *mythread(void *arg)
{
printf("child thread, pid==[%d], id==[%ld]\n", getpid(), pthread_self());
sleep(2);//保证子线程如果是分离属性,pthread_join立刻返回
}
int main()
{
//定义pthread_attr_t类型的变量
pthread_attr_t attr;
//初始化attr变量
pthread_attr_init(&attr);
//设置attr为分离属性
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
//创建子线程
pthread_t thread;
int ret = pthread_create(&thread, &attr, mythread, NULL);
if(ret!=0)
{
printf("pthread_create error, [%s]\n", strerror(ret));
return -1;
}
printf("main thread, pid==[%d], id==[%ld]\n", getpid(), pthread_self());
//释放线程属性
pthread_attr_destroy(&attr);
//验证子线程是否为分离属性
ret = pthread_join(thread, NULL);
if(ret!=0)
{
printf("pthread_join error:[%s]\n", strerror(ret));
}
return 0;
}
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day8$ ./pmak pthread_attr
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day8$ ./pthread_attr
main thread, pid==[4900],id==[139666594178880]
pthread_join error:[Invalid argument]
4.线程同步
学习目标:
-
熟练掌握互斥量的使用
-
说出什么叫死锁以及解决方案
-
熟练掌握读写锁的使用
-
熟练掌握条件变量的使用
-
理解条件变量实现的生产消费者模型
理解信号量实现的生产消费者模型
4.1 线程同步的概念
线程同步,指一个线程发出某一功能调用时,在没有得到结果之前,该调用不返回。同时其它线程为保证数据一致性,不能调用该功能。
4.2 线程同步的例子
创建两个线程,让两个线程共享一个全局变量int number, 然后让每个线程数5000次数,看最后打印出这个number值是多少?
线程A代码片段:
线程B代码片段:
- 代码片段说明
▶代码中使用调用usleep是为了让两个子线程能够轮流使用CPU,避免一个子线程在一个时间片内完成5000次数数。
▶对number执行++操作,使用了中间变量cur是为了尽可能的模拟cpu时间片用完而让出cpu的情况。
- 测试结果
▶经过多次测试最后的结果显示,有可能会出现number值少于5000*2=10000的情况。
- 分析原因
▶假如子线程A执行完了cur++操作,还没有将cur的值赋值给number失去了cpu的执行权,子线程B得到了cpu执行权,而子线程B最后执行完了number=cur,而后失去了cpu的执行权;此时子线程A又重新得到cpu的执行权,并执行number=cur操作,这样会把线程B刚刚写回number的值被覆盖了,造成number值不符合预期的值。
- 数据混乱的原因
▶资源共享(独享资源则不会)
▶调度随机(线程操作共享资源的先后顺序不确定)
▶线程间缺乏必要的同步机制。
以上3点中,前两点不能改变,欲提高效率,传递数据,资源必须共享。只要共享资源,就一定会出现竞争。只要存在竞争关系,数据就很容易出现混乱。所以只能从第三点着手解决。使多个线程在访问共享资源的时候,出现互斥。
- 如何解决问题
▶原子操作的概念
原子操作指的是该操作要么不做,要么就完成。
▶使用互斥锁解决同步问题
使用互斥锁其实是模拟原子操作,互斥锁示意图:
Linux中提供一把互斥锁mutex(也称之为互斥量)。每个线程在对资源操作前都尝试先加锁,成功加锁才能操作,操作结束解锁。
资源还是共享的,线程间也还是竞争的,但通过“锁”就将资源的访问变成互斥操作,而后与时间有关的错误也不会再产生了。
线程1访问共享资源的时候要先判断锁是否锁着,如果锁着就阻塞等待;若锁是解开的就将这把锁加锁,此时可以访问共享资源,访问完成后释放锁,这样其他线程就有机会获得锁。
应该注意:图中同一时刻,只能有一个线程持有该锁,只要该线程未完成操作就不释放锁。
使用互斥锁之后,两个线程由并行操作变成了串行操作,效率降低了,但是数据不一致的问题得到解决了。
//线程同步
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <pthread.h>
#define NUM 5000
int number = 0;
//线程执行函数
void* mythread1(void* arg)
{
int i = 0;
int n;
for (i = 0; i < NUM; i++)
{
n = number;
n++;
number = n;
printf("1:number==[%d]\n", number);
}
}
void* mythread2(void* arg)
{
int i = 0;
int n;
for (i = 0; i < NUM; i++)
{
n = number;
n++;
number = n;
printf("2:number==[%d]\n", number);
}
}
int main()
{
//int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
// void *(*start_routine) (void *), void *arg);
//创建子线程
pthread_t thread1;
int ret = pthread_create(&thread1, NULL, mythread1, NULL);
if (ret != 0)
{
printf("pthread_create error, [%s]\n", strerror(ret));
return -1;
}
pthread_t thread2;
ret = pthread_create(&thread2, NULL, mythread2, NULL);
if (ret != 0)
{
printf("pthread_create error, [%s]\n", strerror(ret));
return -1;
}
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
printf("number==[%d]\n", number);
return 0;
}
线程同步:
互斥锁: 线程A和线程B共同访问共享资源,当线程A想访问共享资源的时候,要先获得锁,如果锁被占用,则加锁不成功需要阻塞等待对方释放锁;若锁没有被占用,则获得锁成功--加锁,然后操作共享资源,操作完之后,必须解锁,同理B也是和A一样。
---->也就是说,同时不能有两个线程访问共享资源,属于互斥操作。
4.3互斥锁主要相关函数
pthread_mutex_t
类型
▶其本质是一个结构体,为简化理解,应用时可忽略其实现细节,简单当成整数看待。
▶pthread_mutex_t mutex;
变量mutex只有两种取值1、0。
pthread_mutex_init
函数
▶函数描述:
▷初始化一个互斥锁(互斥量) ---> 初值可看作1
▶函数原型:
▷int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
▶函数参数
▷mutex:传出参数,调用时应传 &mutex
▷attr:互斥锁属性。是一个传入参数,通常传NULL,选用默认属性(线程间共享)。
restrict关键字:只用于限制指针,告诉编译器,所有修改该指针指向内存中内容的操作,只能通过本指针完成。不能通过除本指针以外的其他变量或指针修改互斥量mutex的两种初始化方式:
◆静态初始化:如果互斥锁 mutex 是静态分配的(定义在全局,或加了static关键字修饰),可以直接使用宏进行初始化。
pthead_mutex_t muetx = PTHREAD_MUTEX_INITIALIZER;
◆动态初始化:局部变量应采用动态初始化。
pthread_mutex_init(&mutex, NULL)
- pthread_mutex_destroy函数
▶函数描述
▷销毁一个互斥锁
▶函数原型
▷int pthread_mutex_destroy(pthread_mutex_t *mutex);
▶函数参数
▷mutex—互斥锁变量
- pthread_mutex_lock函数
▶函数描述
▷对互斥所加锁,可理解为将mutex--
▶函数原型
int pthread_mutex_lock(pthread_mutex_t *mutex);
▶函数参数
▷mutex—互斥锁变量
- pthread_mutex_unlock函数
▶函数描述
▷对互斥所解锁,可理解为将mutex ++
▶函数原型
▷int pthread_mutex_unlock(pthread_mutex_t *mutex);
- pthread_mutex_trylock函数
▶函数描述
▷尝试加锁
▶函数原型
▷int pthread_mutex_trylock(pthread_mutex_t *mutex);
▶函数参数
▷mutex—互斥锁变量
加锁效率变低,线程不能并行
互斥锁使用原理:
第1步:创建一把互斥锁
pthread_mutex_t mutex;
第2步:在main()函数中初始化互斥锁
pthread_mutex_init(&mutex);---相当于mutex=1
第3步:锁的使用---在代码中寻找共享资源(也称为临界区),在共享资源出现的位置的上下加锁和解锁
pthread_mutex_lock(&mutex); // -- mutex = 0
[临界区代码]共享资源操作的代码
pthread_mutex_unlock(&mutex);// -- mutex = 1
第四步:在main()函数中释放互斥锁资源
pthread_mutex_destroy(&mutex);
注意:必须在所有操作共享资源的线程上都加上锁否则不能起到同步的效果。
4.4 加锁和解锁
-
lock尝试加锁,如果加锁不成功,线程阻塞,阻塞到持有该互斥量的其他线程解锁为止。
-
unlock主动解锁函数,同时将阻塞在该锁上的所有线程全部唤醒,至于哪个线程先被唤醒,取决于优先级、调度。默认:先阻塞、先唤醒。
练习:使用互斥锁解决两个线程数数不一致的问题。
代码片段:在访问共享资源前加锁,访问结束后立即解锁。锁的“粒度”应越小越好。
加锁
//线程同步
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <pthread.h>
#define NUM 5000
int number = 0;
//定义一把互斥锁
pthread_mutex_t mutex;
//线程执行函数
void* mythread1(void* arg)
{
int i = 0;
int n;
for (i = 0; i < NUM; i++)
{
//加锁
pthread_mutex_lock(&mutex);
n = number;
n++;
number = n;
printf("1:number==[%d]\n", number);
pthread_mutex_unlock(&mutex);
}
}
void* mythread2(void* arg)
{
int i = 0;
int n;
for (i = 0; i < NUM; i++)
{
pthread_mutex_lock(&mutex);
n = number;
n++;
number = n;
printf("2:number==[%d]\n", number);
pthread_mutex_unlock(&mutex);
}
}
int main()
{
//互斥锁初始化
pthread_mutex_init(&mutex, NULL);
//int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
// void *(*start_routine) (void *), void *arg);
//创建子线程
pthread_t thread1;
int ret = pthread_create(&thread1, NULL, mythread1, NULL);
if (ret != 0)
{
printf("pthread_create error, [%s]\n", strerror(ret));
return -1;
}
pthread_t thread2;
ret = pthread_create(&thread2, NULL, mythread2, NULL);
if (ret != 0)
{
printf("pthread_create error, [%s]\n", strerror(ret));
return -1;
}
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
//释放互斥锁
pthread_mutex_destroy(&mutex);
return 0;
}
加锁情况
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day8$ ./pmak pthread_lock
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day8$ time ./pthread_lock
...
2:number==[399998]
2:number==[399999]
2:number==[400000]
number == [400000]
real 0m3.910s
user 0m0.183s
sys 0m0.907s
不加锁
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day8$ ./pmak lock
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day8$ time ./lock
...
1:number==[399998]
1:number==[399999]
1:number==[400000]
number == [400000]
real 0m3.665s
user 0m0.133s
sys 0m0.939s
不加锁更快
4.5.互斥锁
(1)互斥锁的使用步骤
- 第1步:创建一把互斥锁
▶pthread_mutex_t mutex;
- 初始化互斥锁
▶pthread_mutex_init(&mutex);---相当于mutex=1
- 在代码中寻找共享资源(也称为临界区)
pthread_mutex_lock(&mutex); -- mutex = 0
[临界区代码]
pthread_mutex_unlock(&mutex); -- mutex = 1
- 释放互斥锁资源
pthread_mutex_destroy(&mutex);
注意:必须在所有操作共享资源的线程上都加上锁否则不能起到同步的效果。
(2)练习
- 编写思路:
1 定义一把互斥锁,应该为一全局变量
pthread_mutex_t mutex;
2 在main函数中对mutex进行初始化
pthread_mutex_init(&mutex, NULL);
3 创建两个线程,在两个线程中加锁和解锁
4 主线程释放互斥锁资源
pthread_mutex_destroy(&mutex);
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <pthread.h>
#include <time.h>
//定义一把锁
//pthread_mutex_t mutex;
void* mythread1(void* args)
{
while (1)
{
//加锁
//pthread_mutex_lock(&mutex);
printf("hello ");
sleep(rand() % 3);//目的是让两个线程间切换概率更高一些
printf("world\n");
//解锁
//pthread_mutex_unlock(&mutex);
sleep(rand() % 3);
}
pthread_exit(NULL);
}
void* mythread2(void* args)
{
while (1)
{
//加锁
//pthread_mutex_lock(&mutex);
printf("HELLO ");
sleep(rand() % 3);
printf("WORLD\n");
//解锁
//pthread_mutex_unlock(&mutex);
sleep(rand() % 3);
}
pthread_exit(NULL);
}
int main()
{
int ret;
pthread_t thread1;
pthread_t thread2;
//随机数种子
srand(time(NULL));
//互斥锁初始化
//pthread_mutex_init(&mutex, NULL);
ret = pthread_create(&thread1, NULL, mythread1, NULL);
if (ret != 0)
{
printf("pthread_create error, [%s]\n", strerror(ret));
return -1;
}
ret = pthread_create(&thread2, NULL, mythread2, NULL);
if (ret != 0)
{
printf("pthread_create error, [%s]\n", strerror(ret));
return -1;
}
//等待线程结束
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
//释放互斥锁
//pthread_mutex_destroy(&mutex);
return 0;
}
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day8$ ./pmak pthread_lock
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day8$ ./pthread_lock
HELLO hello world
hello WORLD
world
HELLO hello WORLD
world
hello HELLO WORLD
world
HELLO hello WORLD
HELLO WORLD
world
HELLO WORLD
hello HELLO WORLD
world
HELLO WORLD
HELLO hello WORLD
HELLO WORLD
world
两个线程加锁
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day8$ ./pmak pthread_lock
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day8$ ./pthread_lock
HELLO WORLD
hello world
hello world
hello world
HELLO WORLD
hello world
HELLO WORLD
hello world
HELLO WORLD
hello world
(3)死锁
死锁并不是linux提供给用户的一种使用方法,而是由于用户使用互斥锁不当引起的一种现象。
- 常见的死锁有两种:
▶第一种:自己锁自己,如下图代码片段
下面也会
void* mythread1(void* args)
{
while (1)
{
//加锁
pthread_mutex_lock(&mutex);
printf("hello ");
sleep(rand() % 3);
printf("world\n");
//解锁
//pthread_mutex_unlock(&mutex);
sleep(rand() % 3);
}
pthread_exit(NULL);
}
void* mythread2(void* args)
{
while (1)
{
//加锁
pthread_mutex_lock(&mutex);
printf("HELLO ");
sleep(rand() % 3);
printf("WORLD\n");
//解锁
pthread_mutex_unlock(&mutex);
sleep(rand() % 3);
}
pthread_exit(NULL);
}
▶第二种 线程A拥有A锁,请求获得B锁;线程B拥有B锁,请求获得A锁,这样造成线程A和线程B都不释放自己的锁,而且还想得到对方的锁,从而产生死锁,如下图所示:
- 如何解决死锁:
▶让线程按照一定的顺序去访问共享资源
▶在访问其他锁的时候,需要先将自己的锁解开
▶调用pthread_mutex_trylock,如果加锁不成功会立刻返回
死锁: 死锁不是linux提供给开发者的一种机制,而是由于开发者操作不当引起的。
1.自己锁自己.
注意点: 线程在异常退出的时候也需要解锁。
2.A线程占用着A锁,又想去获得B锁;B线程占用着B锁,又想去获得A锁,两个线程都不释放自己的锁,又想去获得对方的锁,从而造成了死锁。
解决方法:
a.需要先释放自己的锁再去获得其他锁
b.避免使用嵌套的锁,让线程按照一定的顺序加锁
c.可以调用pthread_mutex_trylock
函数加锁,该函数不阻塞,所以不会产生死锁。
4.6.读写锁
- 什么是读写锁
▶读写锁也叫共享-独占锁。当读写锁以读模式锁住时,它是以共享模式锁住的;当它以写模式锁住时,它是以独占模式锁住的。写独占、读共享。
- 读写锁使用场合
▶读写锁非常适合于对数据结构读的次数远大于写的情况。
- 读写锁特性
▶读写锁是“写模式加锁”时,解锁前,所有对该锁加锁的线程都会被阻塞。
▶读写锁是“读模式加锁”时,如果线程以读模式对其加锁会成功;如果线程以写模式加锁会阻塞。
▶读写锁是“读模式加锁”时, 既有试图以写模式加锁的线程,也有试图以读模式加锁的线程。那么读写锁会阻塞随后的读模式锁请求。优先满足写模式锁。读锁、写锁并行阻塞,写锁优先级高
- 读写锁场景练习:
▶线程A加写锁成功,线程B请求读锁
▷线程B阻塞
▶线程A持有读锁,线程B请求写锁
▷线程B阻塞
▶线程A拥有读锁,线程B请求读锁
▷线程B加锁成功
▶线程A持有读锁,然后线程B请求写锁,然后线程C请求读锁
▷B阻塞,C阻塞 - 写的优先级高
▷A解锁,B线程加写锁成功,C继续阻塞
▷B解锁,C加读锁成功
▶线程A持有写锁, 然后线程B请求读锁, 然后线程C请求写锁
▷BC阻塞
▷A解锁,C加写锁成功,B继续阻塞
▷C解锁,B加读锁成功
- 读写锁总结
读并行,写独占,当读写同时等待锁的时候写的优先级高
- 读写锁主要操作函数
▶定义一把读写锁
▷pthread_rwlock_t rwlock;
▶初始化读写锁
▷int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);
▷函数参数
◆rwlock-读写锁
◆attr-读写锁属性,传NULL为默认属性
▇销毁读写锁
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
▇加读锁
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
▇尝试加读锁
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
▇加写锁
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
▇尝试加写锁
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
▇解锁
int pthread_rwlock_unlock(&pthread_rwlock_t *rwlock);
- 练习:3个线程不定时写同一全局资源,5个线程不定时读同一全局资源。
//读写锁测试程序
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <pthread.h>
int number = 0;
//定义一把读写锁
pthread_rwlock_t rwlock;
//写线程回调函数
void* thread_write(void* arg)
{
int i = *(int*)arg;
int cur;
while (1)
{
//加写锁
pthread_rwlock_wrlock(&rwlock);
cur = number;
cur++;
number = cur;
printf("[%d]-W:[%d]\n", i, cur);
//解锁
pthread_rwlock_unlock(&rwlock);
sleep(rand() % 3);
}
}
//读线程回调函数
void* thread_read(void* arg)
{
int i = *(int*)arg;
int cur;
while (1)
{
//加读锁
pthread_rwlock_rdlock(&rwlock);
cur = number;
printf("[%d]-R:[%d]\n", i, cur);
//解锁
pthread_rwlock_unlock(&rwlock);
sleep(rand() % 3);
}
}
int main()
{
int n = 8;
int i = 0;
int arr[8];
pthread_t thread[8];
//读写锁初始化
pthread_rwlock_init(&rwlock, NULL);
//创建3个写子线程
for (i = 0; i < 3; i++)
{
arr[i] = i;
pthread_create(&thread[i], NULL, thread_write, &arr[i]);
}
//创建5个读子线程
for (i = 3; i < n; i++)
{
arr[i] = i;
pthread_create(&thread[i], NULL, thread_read, &arr[i]);
}
//回收子线程
int j = 0;
for (j = 0; j < n; j++)
{
pthread_join(thread[j], NULL);
}
//释放锁
pthread_rwlock_destroy(&rwlock);
return 0;
}
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day9$ ./pmak 01-pthread_rwlock
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day9$ ./01-pthread_rwlock
[3]-R:[0]
[4]-R:[0]
[5]-R:[0]
[6]-R:[0]
[7]-R:[0]
[2]-W:[1]
[4]-R:[1]
...
[2]-W:[130]
[0]-W:[130]
[1]-W:[130]
[3]-R:[130]
[6]-R:[130]
[4]-R:[130]
[7]-R:[130]
[5]-R:[130]
[2]-W:[131]
[0]-W:[131]
[1]-W:[131]
^C
130两个写重了
读写锁:读写锁是一把锁
读写锁场景练习:
█线程A加写锁成功,线程B请求读锁
线程B阻塞,当线程A解锁之后,线程B加锁成功
█线程A持有读锁,线程B请求写锁
线程B会阻塞;
当线程A解锁之后,线程B加锁成功
█线程A拥有读锁,线程B请求读锁
线程B请求锁成功
█线程A持有读锁,然后线程B请求写锁,然后线程C请求读锁
线程B和C都阻塞;
当A释放锁之后,B先获得锁,C阻塞
当B释放锁之后,C获得锁
█线程A持有写锁,然后线程B请求读锁,然后线程C请求写锁
线程B和C都阻塞;
当线程A解锁之后,C先获得锁,B阻塞;
当C解锁之后,B获得锁
读写锁总结
写独占,读共享,当读和写一起等待锁的时候,写的优先级高
读写锁使用步骤:
1.先定义一把读写锁:
pthread_rwlock_t rwlock;
2.初始化读写锁
pthread_rwlock_init(&rwlock, NULL);
3.加锁
pthread_rwlock_rdlock(&rwlock);---->加读锁
pthread_rwlock_wrlock(&rwlock);---->加写锁
////////////////////////
共享资源出现的位置
/////////////////////////
4.解锁
pthread_rwlock_unlock(&rwlock);
5.释放锁
pthread_rwlock_destroy(&rwlock);
//读写锁测试程序
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <pthread.h>
int number = 0;
//写线程回调函数
void* thread_write(void* arg)
{
int i = *(int*)arg;
int cur;
while (1)
{
cur = number;
cur++;
usleep(500);
number = cur;
printf("[%d]-W:[%d]\n", i, cur);
}
}
//读线程回调函数
void* thread_read(void* arg)
{
int i = *(int*)arg;
int cur;
while (1)
{
cur = number;
printf("[%d]-R:[%d]\n", i, cur);
usleep(500);
}
}
int main()
{
int n = 8;
int i = 0;
int arr[8];
pthread_t thread[8];
//创建3个写子线程
for (i = 0; i < 3; i++)
{
arr[i] = i;
pthread_create(&thread[i], NULL, thread_write, &arr[i]);
}
//创建5个读子线程
for (i = 3; i < n; i++)
{
arr[i] = i;
pthread_create(&thread[i], NULL, thread_read, &arr[i]);
}
//回收子线程
int j = 0;
for (j = 0; j < n; j++)
{
pthread_join(thread[j], NULL);
}
return 0;
}
这段代码主要测试多线程下的读写问题。它创建了3个写线程和5个读线程,它们共享同一个全局变量number
进行读和写操作。
-
写线程回调函数 (
thread_write
):- 此函数首先读取全局变量
number
的值。 - 然后将此值增加1。
- 通过
usleep(500)
模拟了一个延迟(500微秒),此时其他线程有机会执行。 - 更新全局变量
number
。 - 打印线程ID和增加后的值。
- 此函数首先读取全局变量
-
读线程回调函数 (
thread_read
):- 此函数只是读取全局变量
number
的值并打印。 - 同样地,通过
usleep(500)
进行了延迟。
- 此函数只是读取全局变量
-
main 函数:
- 创建3个写线程。
- 创建5个读线程。
- 使用
pthread_join
来回收所有线程。
分析:
-
竞态条件:此代码有一个明显的竞态条件,它出现在写线程中。当一个写线程读取
number
的值并暂停(因为usleep
)时,其他线程可能会改变number
的值。当原始线程恢复并写入新值时,它可能会覆盖另一个线程的更改。这导致数据不一致和不可预测的结果。 -
读线程与写线程之间的冲突:由于此代码中没有任何形式的同步机制(如互斥锁或读写锁),读线程可能在一个写线程更改
number
时读取number
的值,这同样也会导致不可预测的结果。 -
期望结果:由于上述问题,当你运行这个程序时,你会看到写线程可能不会每次都递增
number
,因为它们的更改可能被其他写线程覆盖。而读线程则可能在写线程之间读取number
,从而产生不一致的读取值。
改进:
为了避免上述问题,你应该在读取或写入number
时使用同步机制。考虑使用pthread的读写锁(pthread_rwlock_t
)。读写锁允许多个读线程同时访问,但只允许一个写线程在没有读线程时访问,这正是这种情况所需的。
简而言之,这个程序展示了在没有适当同步的多线程环境中可能会出现的问题。
什么时候运行写线程,什么时候运行读线程
在给出的代码中,写线程和读线程的启动和执行完全取决于操作系统的线程调度器。因为没有明确的同步机制或条件变量来决定哪个线程应该何时运行,所以线程的执行顺序是不确定的。
以下是关于写线程和读线程在此代码中可能的运行模式的简要概述:
-
写线程:在
main
函数中,前三个线程是写线程,它们由thread_write
函数控制。这些线程首先读取全局变量number
,然后增加它,模拟延迟(通过usleep(500)
),并写回新的值。 -
读线程:在
main
函数中,接下来的五个线程是读线程,由thread_read
函数控制。这些线程只是读取number
的值并打印。
由于线程的执行取决于操作系统的线程调度器,以下是可能的几种情况:
- 一个或多个写线程可能先运行,然后读线程运行。
- 一个或多个读线程可能先运行,然后写线程运行。
- 读写线程之间可能交替运行。
再次强调,因为给定的代码没有任何同步机制,所以不能预测或保证特定的线程执行顺序。为了使读写操作更可预测和稳定,您应该引入适当的线程同步,如读写锁(pthread_rwlock_t
)或互斥锁(pthread_mutex_t
)。
加锁
//读写锁测试程序
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <pthread.h>
int number = 0;
//定义一把读写锁
pthread_rwlock_t rwlock;
//写线程回调函数
void* thread_write(void* arg)
{
int i = *(int*)arg;
int cur;
while (1)
{
//加写锁
pthread_rwlock_wrlock(&rwlock);
cur = number;
cur++;
number = cur;
printf("[%d]-W:[%d]\n", i, cur);
//解锁
pthread_rwlock_unlock(&rwlock);
usleep(400);
}
}
//读线程回调函数
void* thread_read(void* arg)
{
int i = *(int*)arg;
int cur;
while (1)
{
//加读锁
pthread_rwlock_rdlock(&rwlock);
cur = number;
printf("[%d]-R:[%d]\n", i, cur);
//解锁
pthread_rwlock_unlock(&rwlock);
usleep(400);
}
}
int main()
{
int n = 8;
int i = 0;
int arr[8];
pthread_t thread[8];
//读写锁初始化
pthread_rwlock_init(&rwlock, NULL);
//创建3个写子线程
for (i = 0; i < 3; i++)
{
arr[i] = i;
pthread_create(&thread[i], NULL, thread_write, &arr[i]);
}
//创建5个读子线程
for (i = 3; i < n; i++)
{
arr[i] = i;
pthread_create(&thread[i], NULL, thread_read, &arr[i]);
}
//回收子线程
int j = 0;
for (j = 0; j < n; j++)
{
pthread_join(thread[j], NULL);
}
//释放锁
pthread_rwlock_destroy(&rwlock);
return 0;
}
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day9$ ./pmak 01-pthread_rwlock
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day9$ ./01-pthread_rwlock
...
[3]-R:[4959]
[4]-R:[4959]
[1]-W:[4960]
[6]-R:[4960]
[2]-W:[4961]
[5]-R:[4961]
[7]-R:[4961]
[0]-W:[4962]
[3]-R:[4962]
[4]-R:[4962]
//读写锁测试程序
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <pthread.h>
int number = 0;
//定义一把读写锁
pthread_rwlock_t rwlock;
//写线程回调函数
void *thread_write(void *arg)
{
int i = *(int *)arg;
int cur;
while(1)
{
//加写锁
pthread_rwlock_wrlock(&rwlock);
cur = number;
cur++;
number = cur;
printf("[%d]-W:[%d]\n", i, cur);
//解锁
pthread_rwlock_unlock(&rwlock);
sleep(rand()%3);
}
}
//读线程回调函数
void *thread_read(void *arg)
{
int i = *(int *)arg;
int cur;
while(1)
{
//加读锁
pthread_rwlock_rdlock(&rwlock);
cur = number;
printf("[%d]-R:[%d]\n", i, cur);
//解锁
pthread_rwlock_unlock(&rwlock);
sleep(rand()%3);
}
}
int main()
{
int n = 8;
int i = 0;
int arr[8];
pthread_t thread[8];
//读写锁初始化
pthread_rwlock_init(&rwlock, NULL);
//创建3个写子线程
for(i=0; i<3; i++)
{
arr[i] = i;
pthread_create(&thread[i], NULL, thread_write, &arr[i]);
}
//创建5个读子线程
for(i=3; i<n; i++)
{
arr[i] = i;
pthread_create(&thread[i], NULL, thread_read, &arr[i]);
}
//回收子线程
int j = 0;
for(j=0;j<n; j++)
{
pthread_join(thread[j], NULL);
}
//释放锁
pthread_rwlock_destroy(&rwlock);
return 0;
}
[4]-R:[4043]
[0]-W:[4044]
[3]-R:[4044]
[6]-R:[4044]
[5]-R:[4044]
[2]-W:[4045]
[1]-W:[4046]
[7]-R:[4046]
[4]-R:[4046]
[0]-W:[4047]
[3]-R:[4047]
[6]-R:[4047]
[5]-R:[4047]
[2]-W:[4048]
4.7条件变量
- 条件本身不是锁!但它也可以造成线程阻塞。通常与互斥锁配合使用。给多线程提供一个会合的场所。
▶使用互斥量保护共享数据;
▶使用条件变量可以使线程阻塞, 等待某个条件的发生, 当条件满足的时候解除阻塞.
- 条件变量的两个动作:
▶条件不满足, 阻塞线程
▶条件满足, 通知阻塞的线程解除阻塞, 开始工作.
- 条件变量相关函数
▶pthread_cond_t cond;
▷定义一个条件变量
▶int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
▷函数描述:初始化条件变量
▷函数参数:
cond: 条件变量
attr: 条件变量属性,通常传NULL
▷函数返回值:成功返回0,失败返回错误号
▶int pthread_cond_destroy(pthread_cond_t *cond);
▷函数描述:销毁条件变量
▷函数参数:条件变量
▷返回值: 成功返回0,失败返回错误号
▶int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
▷函数描述: 条件不满足,引起线程阻塞并解锁;
▷条件满足,解除线程阻塞,并加锁
▷函数参数:
cond: 条件变量
mutex: 互斥锁变量
▷函数返回值: 成功返回0, 失败返回错误号
▶int pthread_cond_signal(pthread_cond_t *cond);
▷函数描述: 唤醒至少一个阻塞在该条件变量上的线程
▷函数参数: 条件变量
▷函数返回值: 成功返回0,失败返回错误号
- 使用条件变量的代码片段
上述代码中,生产者线程调用pthread_cond_signal
函数会使消费者线程在pthread_cond_wait
处解除阻塞。
//使用条件变量实现生产者和消费者模型
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <pthread.h>
typedef struct node
{
int data;
struct node* next;
}NODE;
NODE* head = NULL;
//生产者线程
void* producer(void* arg)
{
NODE* pNode = NULL;
while (1)
{
//生产一个节点
pNode = (NODE*)malloc(sizeof(NODE));
if (pNode == NULL)
{
perror("malloc error");
exit(-1);
}
pNode->data = rand() % 1000;
printf("P:[%d]\n", pNode->data);
pNode->next = head;
head = pNode;
sleep(rand() % 3);
}
}
//消费者线程
void* consumer(void* arg)
{
NODE* pNode = NULL;
while (1)
{
pNode = (NODE*)malloc(sizeof(NODE));
printf("C:[%d]\n", head->data);
pNode = head;
head = head->next;
free(pNode);
pNode = NULL;
sleep(rand() % 3);
}
}
int main()
{
int ret;
pthread_t thread1;
pthread_t thread2;
ret = pthread_create(&thread1, NULL, producer, NULL);
if (ret != 0)
{
printf("pthread_create error, [%s]\n", strerror(ret));
return -1;
}
ret = pthread_create(&thread2, NULL, consumer, NULL);
if (ret != 0)
{
printf("pthread_create error, [%s]\n", strerror(ret));
return -1;
}
//等待线程结束
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
return 0;
}
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day9$ ./pmak 02-pthread_cond
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day9$ ./02-pthread_cond
段错误 (核心已转储)
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day9$ ./02-pthread_cond
段错误 (核心已转储)
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day9$ ./02-pthread_cond
段错误 (核心已转储)
消费者先运行立刻死掉
//使用条件变量实现生产者和消费者模型
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <pthread.h>
typedef struct node
{
int data;
struct node* next;
}NODE;
NODE* head = NULL;
//定义一把锁
pthread_mutex_t mutex;
//定义条件变量
pthread_cond_t cond;
//生产者线程
void* producer(void* arg)
{
NODE* pNode = NULL;
while (1)
{
//生产一个节点
pNode = (NODE*)malloc(sizeof(NODE));
if (pNode == NULL)
{
perror("malloc error");
exit(-1);
}
pNode->data = rand() % 1000;
printf("P:[%d]\n", pNode->data);
//加锁
pthread_mutex_lock(&mutex);
pNode->next = head;
head = pNode;
//解锁
pthread_mutex_unlock(&mutex);
//通知消费者线程解除阻塞
pthread_cond_signal(&cond);
sleep(rand() % 3);
}
}
//消费者线程
void* consumer(void* arg)
{
NODE* pNode = NULL;
while (1)
{
//加锁
pthread_mutex_lock(&mutex);
if (head == NULL)
{
//若条件不满足,需要阻塞等待
//若条件不满足,则阻塞等待并解锁;
//若条件满足(被生成者线程调用pthread_cond_signal函数通知),解除阻塞并加锁
pthread_cond_wait(&cond, &mutex);
}
printf("C:[%d]\n", head->data);
pNode = head;
head = head->next;
//解锁
pthread_mutex_unlock(&mutex);
free(pNode);
pNode = NULL;
sleep(rand() % 3);
}
}
int main()
{
int ret;
pthread_t thread1;
pthread_t thread2;
//初始化互斥锁
pthread_mutex_init(&mutex, NULL);
//条件变量初始化
pthread_cond_init(&cond, NULL);
//创建生产者线程
ret = pthread_create(&thread1, NULL, producer, NULL);
if (ret != 0)
{
printf("pthread_create error, [%s]\n", strerror(ret));
return -1;
}
//创建消费者线程
ret = pthread_create(&thread2, NULL, consumer, NULL);
if (ret != 0)
{
printf("pthread_create error, [%s]\n", strerror(ret));
return -1;
}
//等待线程结束
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
//释放互斥锁
pthread_mutex_destroy(&mutex);
//释放条件变量
pthread_cond_destroy(&cond);
return 0;
}
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day9$ ./pmak 02-pthread_cond
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day9$ ./02-pthread_cond
P:[383]
C:[383]
P:[915]
C:[915]
P:[386]
C:[386]
P:[421]
C:[421]
P:[690]
C:[690]
P:[926]
P:[426]
C:[426]
P:[211]
C:[211]
C:[926]
^C
//使用条件变量实现生产者和消费者模型
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <pthread.h>
typedef struct node
{
int data;
struct node *next;
}NODE;
NODE *head = NULL;
//定义一把锁
pthread_mutex_t mutex;
//定义条件变量
pthread_cond_t cond;
//生产者线程
void *producer(void *arg)
{
NODE *pNode = NULL;
int n = *(int *)arg;
while(1)
{
//生产一个节点
pNode = (NODE *)malloc(sizeof(NODE));
if(pNode==NULL)
{
perror("malloc error");
exit(-1);
}
pNode->data = rand()%1000;
printf("P[%d]:[%d]\n", n, pNode->data);
//加锁
pthread_mutex_lock(&mutex);
pNode->next = head;
head = pNode;
//解锁
pthread_mutex_unlock(&mutex);
//通知消费者线程解除阻塞
pthread_cond_signal(&cond);
sleep(rand()%3);
}
}
//消费者线程
void *consumer(void *arg)
{
NODE *pNode = NULL;
int n = *(int *)arg;
while(1)
{
//加锁
pthread_mutex_lock(&mutex);
if(head==NULL)
{
//若条件不满足,需要阻塞等待
//若条件不满足,则阻塞等待并解锁;
//若条件满足(被生成者线程调用pthread_cond_signal函数通知),解除阻塞并加锁
pthread_cond_wait(&cond, &mutex);
}
if(head==NULL)
{
//解锁
pthread_mutex_unlock(&mutex);
continue;
}
printf("C[%d]:[%d]\n", n, head->data);
pNode = head;
head = head->next;
//解锁
pthread_mutex_unlock(&mutex);
free(pNode);
pNode = NULL;
sleep(rand()%3);
}
}
int main()
{
int ret;
int i = 0;
pthread_t thread1[5];
pthread_t thread2[5];
//初始化互斥锁
pthread_mutex_init(&mutex, NULL);
//条件变量初始化
pthread_cond_init(&cond, NULL);
int arr[5];
for(i=0; i<5; i++)
{
arr[i]= i;
//创建生产者线程
ret = pthread_create(&thread1[i], NULL, producer, &arr[i]);
if(ret!=0)
{
printf("pthread_create error, [%s]\n", strerror(ret));
return -1;
}
//创建消费者线程
ret = pthread_create(&thread2[i], NULL, consumer, &arr[i]);
if(ret!=0)
{
printf("pthread_create error, [%s]\n", strerror(ret));
return -1;
}
}
//等待线程结束
for(i=0; i<5; i++)
{
pthread_join(thread1[i], NULL);
pthread_join(thread2[i], NULL);
}
//释放互斥锁
pthread_mutex_destroy(&mutex);
//释放条件变量
pthread_cond_destroy(&cond);
return 0;
}
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day9$ ./pmak 03-pthread_cond
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day9$ ./03-pthread_cond
P[1]:[383]
C[1]:[383]
P[2]:[886]
C[2]:[886]
P[3]:[386]
C[3]:[386]
P[4]:[421]
C[4]:[421]
P[1]:[27]
P[3]:[690]
C[3]:[690]
C[0]:[27]
P[0]:[172]
C[3]:[172]
P[3]:[368]
C[4]:[368]
P[0]:[782]
C[1]:[782]
P[3]:[123]
C[2]:[123]
P[2]:[929]
C[0]:[929]
P[4]:[802]
C[4]:[802]
//使用信号量实现生产者和消费者模型
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>
typedef struct node
{
int data;
struct node *next;
}NODE;
NODE *head = NULL;
//定义信号量
sem_t sem_producer;
sem_t sem_consumer;
//生产者线程
void *producer(void *arg)
{
NODE *pNode = NULL;
while(1)
{
//生产一个节点
pNode = (NODE *)malloc(sizeof(NODE));
if(pNode==NULL)
{
perror("malloc error");
exit(-1);
}
pNode->data = rand()%1000;
printf("P:[%d]\n", pNode->data);
//加锁
sem_wait(&sem_producer); //--
pNode->next = head;
head = pNode;
//解锁
sem_post(&sem_consumer); //相当于++
sleep(rand()%3);
}
}
//消费者线程
void *consumer(void *arg)
{
NODE *pNode = NULL;
while(1)
{
//加锁
sem_wait(&sem_consumer); //相当于--
printf("C:[%d]\n", head->data);
pNode = head;
head = head->next;
//解锁
sem_post(&sem_producer); //相当于++
free(pNode);
pNode = NULL;
sleep(rand()%3);
}
}
int main()
{
int ret;
pthread_t thread1;
pthread_t thread2;
//初始化信号量
sem_init(&sem_producer, 0, 5);
sem_init(&sem_consumer, 0, 0);
//创建生产者线程
ret = pthread_create(&thread1, NULL, producer, NULL);
if(ret!=0)
{
printf("pthread_create error, [%s]\n", strerror(ret));
return -1;
}
//创建消费者线程
ret = pthread_create(&thread2, NULL, consumer, NULL);
if(ret!=0)
{
printf("pthread_create error, [%s]\n", strerror(ret));
return -1;
}
//等待线程结束
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
//释放信号量资源
sem_destroy(&sem_producer);
sem_destroy(&sem_consumer);
return 0;
}
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day9$ ./pmak 04-pthread_sem
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day9$ ./04-pthread_sem
P:[383]
C:[383]
P:[915]
C:[915]
P:[386]
C:[386]
P:[421]
C:[421]
P:[690]
C:[690]
P:[926]
P:[426]
C:[426]
P:[211]
C:[211]
C:[926]
P:[782]
C:[782]
^C