目录
(2)对多线程中使用线程安全的函数。线程安全函数---> 如果一个函数能被多个线程同时调用且不发生竞态条件,则称线程是线程安全的。
一、进程间通信的方式(IPC机制)
1、管道
(1)管道分为:有名管道---->(任意两个进程)、无名管道------->(父子进程)
(2)管道的通信方式:半双工
(3)通信时,连个进程都要打开管道
(4)写入管道的数据都在内存中存储。
1、1 有名管道fifo实现文件读取:
1、2 无名管道:
(1)创建无名管道:pipe,返回值是一个含有两个元素的整型数组,出错:-1,成功:0
1 #include<stdlib.h>
2 #include<stdio.h>
3 #include<unistd.h>
4 #include<string.h>
5
6 int main()
7 {
8 int fd[2];//fd[0]->r,fd[1]->w
9 int res = pipe(fd); //res:-1 0
10 if(res == -1)
11 {
12 printf("pipe err\n");
13 exit(1);
14 }
15
16 pid_t pid = fork();
17 if(pid == -1)
18 {
19 exit(1);
20 }
21 //子进程写
22 if(pid==0)
23 {
24 sleep(3);
25 write(fd[1],"hello",5);
26 }
27 //父进程读
28 else
29 {
30 char buff[128] = {0};
31 read(fd[0],buff,127);
32 printf("buff=%s\n",buff);
33 }
34 close(fd[0]);
35 close(fd[1]);
36 }
2、信号量
(1)什么是信号量?
信号量是特殊的变量,一般取正数值,用来同步进程。提供pv操作。原子操作。v:代表释放资源,p:获取资源。作用控制程序如何执行。(就像红绿灯,控制程序中对某个资源是否能访问,能访问就能通过,否则会被阻塞)
- P 操作(wait 操作):当一个进程或线程执行 P 操作时,它会先检查信号量的值。如果信号量的值大于 0,则将信号量的值减 1,并继续执行后续操作;如果信号量的值等于 0,则该进程或线程会被阻塞,直到信号量的值大于 0 为止。
- V 操作(signal 操作):当一个进程或线程执行 V 操作时,它会将信号量的值加 1。如果此时有其他进程或线程因为等待该信号量而被阻塞,那么系统会唤醒其中一个被阻塞的进程或线程。
(2)临界资源:同一时刻,只允许被一个进程或线程访问的资源
(3)临界区:访问临界资源的代码段。
(4)信号量的创建、pv操作、销毁
#include"sem.h"
2
3 static int semid=-1;
4
5 //创建信号量
6 void sem_init()
7 {
8 semid = semget((key_t)1234,1,IPC_CREAT|IPC_EXCL|0600);
9 if(semid == -1)
10 {
11 semget((key_t)1234,1,0600);
12 }
13 else
14 {
15 union semun a;
16 a.val=1;
17 // semctl(id,下标(一个信号量,下标为0),命令,a)
18 if(semctl(semid,0,SETVAL,a) == -1)
19 {
20 perror("semctl error");
21 }
22 }
23 }
24 //全新创建信号量成功,赋初始值为1
25
26 void sem_p()//p-1
27 {
28 struct sembuf buf;
29 buf.sem_num=0;//对第几个信号量进行操作/地址
30 buf.sem_op=-1;//操作
31 buf.sem_flg=SEM_UNDO;//标志位
32
33 if( semop(semid,&buf,1) == -1)
34 {
35 printf("semop p err\n");
36 }
37 }
38 void sem_v()//v+1
39 {
40 struct sembuf buf;
41 buf.sem_num=0;//对第几个信号量进行操作/地址
42 buf.sem_op=1;//操作
43 buf.sem_flg=SEM_UNDO;//标志位
44
45 if( semop(semid,&buf,1) == -1)
46 {
47 printf("semop p err\n");
48 }
49 }
50 void sem_destory()
51 {
52 if(semctl(semid,0,IPC_RMID) == -1)
53 {
54 perror("semctl del error");
55 }
56 }
3、共享内存
(1)什么是共享内存?
它使得多个进程可以将同一块物理内存映射到各自的虚拟地址空间中。这样,这些进程就可以像访问自己的内存一样直接访问共享内存区域,从而实现数据的共享。
把一块内存空间做成共享内存,它可以映射到两个不同进程的地址空间中,当一个进程写入数据时,另外的进程在访问共享内存时是可以看到的。
工作原理
- 创建共享内存区域:一个进程(通常是主进程)首先创建一个共享内存区域,并向操作系统申请一定大小的物理内存空间。
- 映射共享内存:需要访问共享内存的进程将该共享内存区域映射到自己的虚拟地址空间中。这样,进程就可以通过虚拟地址来访问共享内存。
- 数据读写:多个进程可以同时对共享内存区域进行读写操作,实现数据的共享和交换。
- 解除映射和销毁:当进程不再需要访问共享内存时,将其从自己的虚拟地址空间中解除映射。当所有进程都不再使用共享内存时,主进程可以销毁该共享内存区域,释放物理内存空间。
4、消息队列
基本概念
消息队列是一种先进先出(FIFO)的数据结构,它可以存储一系列的消息。进程或线程可以将消息发送到消息队列中,也可以从消息队列中接收消息。消息队列提供了一种异步通信的方式,发送者和接收者不需要同时在线,发送者只需将消息放入队列,接收者在合适的时候从队列中取出消息进行处理。
工作原理
- 消息发送:发送进程或线程将消息封装好后,通过系统调用将消息发送到指定的消息队列中。消息队列会为每条消息分配一个唯一的标识符,并按照先进先出的原则对消息进行排序。
- 消息存储:消息队列通常会将消息存储在内存或磁盘中,具体的存储方式取决于操作系统和消息队列的实现。内存存储方式速度快,但在系统崩溃时可能会丢失消息;磁盘存储方式可以保证消息的持久性,但读写速度相对较慢。
- 消息接收:接收进程或线程通过系统调用从消息队列中获取消息。可以根据消息的类型、优先级等条件进行筛选,只接收符合条件的消息。
5、套接字
二、线程的创建与同步
2、1 线程
(1)线程:进程内部的一条执行路径(序列)
(2)进程和线程的区别?
- 进程是资源分配的最小单位,线程是CPU调度的最小单位。
- 进程有自己的独立地址空间,线程共享进程中的地址空间。
- 进程的创建消耗资源大,线程的创建相对较小。
- 进程的切换开销大,线程的切换开销相对较小。
(3)pthread_create():用于创建线程
#include<stdlib.h>
#include<stdio.h>
#include<unistd.h>
#include<pthread.h>
#include<string.h>
void * fun(void* arg)
{
for(int i=0;i<5;i++)
{
printf("fun run\n");
sleep(1);
}
}
int main()
{
//创建线程
pthread_t id;
pthread_create(&id,NULL,fun,NULL);
for(int i=0;i<5;i++)
{
printf("main run\n");
sleep(1);
}
}
为什么+sleep?若是不加,主函数结束的时候会结束掉整个进程。 fun函数没有机会再打印。
运行时要+库:
(4) 线程的实现方式:
- 用户级线程:用户级线程的管理和调度完全在用户空间进行,由用户程序库负责线程的创建、销毁、调度等操作,操作系统内核并不知道用户级线程的存在。线程的上下文信息(如程序计数器、寄存器值等)保存在用户空间的线程控制块中。
- 内核级线程:内核级线程由操作系统内核直接管理和调度,内核维护着每个线程的上下文信息。线程的创建、销毁、调度等操作都通过系统调用由内核完成。
并发性
- 用户级线程:在多处理器系统中,同一进程内的多个用户级线程不能同时在不同的处理器上并行执行,因为操作系统内核只能看到进程,而看不到进程内的用户级线程。因此,用户级线程的并发性能受到一定限制。
- 内核级线程:内核级线程可以在多处理器系统中并行执行,操作系统内核可以将不同的线程分配到不同的处理器上,充分利用多核处理器的计算能力,提高系统的并发性能。
(5)并行并发的区别?
- 并行:指的是在同一时刻,多个任务同时执行。这需要系统具备多个处理单元,例如多核 CPU 或者多台计算机组成的集群。在并行系统中,每个处理单元可以独立地执行一个任务,这些任务在物理上是同时进行的。
- 并发:指的是在同一时间段内,多个任务交替执行。系统通过快速地在不同任务之间进行切换,使得从宏观上看多个任务似乎是同时执行的。并发并不要求系统具备多个处理单元,单 CPU 系统也可以实现并发。
并行属于特殊的并发。
(6)多线程的并发运行
1 #include<stdio.h>
2 #include<stdlib.h>
3 #include<unistd.h>
4 #include<string.h>
5 #include<pthread.h>
6
7 void* fun(void * arg)
8 {
9 int index = *(int*)arg;
10 printf("index=%d\n",index);
11 }
12 int main()
13 {
14 pthread_t id[5];
15 int i=0;
16 //启动5个线程
17 for(;i<5;i++)
18 {
19 pthread_create(&id[i],NULL,fun,(void*)&i);
20 }
21
22 for(i=0;i<5;i++)
23 {
24 pthread_join(id[i],NULL);
25 }
26
27 exit(0);
28 }
为什么执行结果会出现如图?
主线程一直在改变i的值,进行i++,fun函数获取i值时,不一定是create的时候i的值。线程不改变i的值,但主线程一直在改变i的值。 会导致fun获取到同样的值。5条路径一起运行,若2条同时执行,会漏掉某些数据,导致有重复。即:多个线程可能会访问同一个变量i
的地址。由于线程的创建和执行是异步的,当某个线程开始执行fun
函数时,i
的值可能已经被for
循环修改,导致打印出的index
值可能不是预期的结果。
为了解决这个问题,可以为每个线程传递一个独立的变量副本。为每个线程动态分配了一个int
类型的内存空间,并将i
的值复制到该内存空间中,然后将该内存空间的地址传递给线程执行函数,这样每个线程都有自己独立的index
值,避免了上述问题。同时,在fun
函数中使用free
函数释放了动态分配的内存,防止内存泄漏。以下是修改后的代码:
2、2 线程同步
(1)线程同步的方法:
互斥锁、信号量、条件变量、读写锁
(2) 用信号量实现同步:
sem_init;
sem_wait;
sem_post;
sem_destory;
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<pthread.h>
#include<semaphore.h>
sem_t sem;
int g_val = 1;
void* fun(void * arg)
{
for(int i=0;i<1000;i++)
{
sem_wait(&sem);//p操作
printf("g_Val=%d\n",g_val++);
sem_post(&sem);//v操作
}
}
int main()
{
sem_init(&sem,0,1);//初始化信号量1;0:代表能否在几个进程间共享
pthread_t id[5];
int i=0;
//启动5个线程
for(;i<5;i++)
{
int *p= (int*) malloc(sizeof(int));
*p=i;
pthread_create(&id[i],NULL,fun,(void*)p);
}
for(i=0;i<5;i++)
{
pthread_join(id[i],NULL);
}
sem_destroy(&sem);//销毁信号量
exit(0);
}
(3)互斥锁
pthread_mutex_init;
pthread_mutex_lock;
pthread_mutex_unlock
pthread_mutex_destory;
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<pthread.h>
#include<semaphore.h>
//sem_t sem;
pthread_mutex_t mutex;
int g_val = 1;
void* fun(void * arg)
{
for(int i=0;i<1000;i++)
{
// sem_wait(&sem);//p操作
pthread_mutex_lock(&mutex);
printf("g_Val=%d\n",g_val++);
pthread_mutex_unlock(&mutex);
// sem_post(&sem);//v操作
}
}
int main()
{
// sem_init(&sem,0,1);//初始化信号量1;0:代表能否在几个进程间共享
pthread_mutex_init(&mutex,NULL);
pthread_t id[5];
int i=0;
//启动5个线程
for(;i<5;i++)
{
int *p= (int*) malloc(sizeof(int));
*p=i;
pthread_create(&id[i],NULL,fun,(void*)p);
}
for(i=0;i<5;i++)
{
pthread_join(id[i],NULL);
}
pthread_mutex_destroy(&mutex);
// sem_destroy(&sem);//销毁信号量
exit(0);
}
(4)读写锁
pthread_rwlock_init;
pthread_rwlock_rdlock;读锁
pthread_rwlock_wrlock;写锁
pthread_rwlock_unlock;解锁
pthread_rwlock_destroy;
读和写不能同时进行,但允许两个读线程同时进行(允许两个线程同时操作)
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<pthread.h>
#include<semaphore.h>
pthread_rwlock_t rwlock;
void* fun1(void* arg)
{
for(int i=0;i<20;i++)
{
pthread_rwlock_rdlock(&rwlock);//读🔓
printf("fun1 start read\n");
sleep(1);
printf("fun1 end read\n");
pthread_rwlock_unlock(&rwlock);
sleep(1);
}
}
void* fun2(void* arg)
{
for(int i=0;i<10;i++)
{
pthread_rwlock_rdlock(&rwlock);//读🔓
printf("fun2 start read\n");
sleep(2);
printf("fun2 end read\n");
pthread_rwlock_unlock(&rwlock);
sleep(1);
}
}
void* fun3(void* arg)
{
for(int i=0;i<10;i++)
{
pthread_rwlock_wrlock(&rwlock);//写🔓
printf("------fun3 start write\n");
sleep(2);
printf("---------fun3 end write\n");
pthread_rwlock_unlock(&rwlock);
sleep(1);
}
}
int main()
{
pthread_rwlock_init(&rwlock,NULL);
pthread_t id1,id2,id3;
//创建3个线程,2个读1个写
pthread_create(&id1,NULL,fun1,NULL);
pthread_create(&id2,NULL,fun2,NULL);
pthread_create(&id3,NULL,fun3,NULL);
pthread_join(id1,NULL);
pthread_join(id2,NULL);
pthread_join(id3,NULL);
pthread_rwlock_destroy(&rwlock);
exit(0);
}
(5)条件变量
pthread_cond_init;
pthread_cond_wait; 放到队列中
pthread_cond_signal; 唤醒单个线程
pthread_cond_broadcast; 唤醒所有等待的线程
唤醒时被锁保护--->不会进出队列。
#include<stdlib.h>
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<pthread.h>
#include<semaphore.h>
pthread_mutex_t mutex;
pthread_cond_t cond;
char buff[128]= {0};
void* funa()
{
while(1)
{
pthread_mutex_lock(&mutex); //加锁
pthread_cond_wait(&cond,&mutex);//将线程放到条件变量队列中
pthread_mutex_unlock(&mutex);
if(strncmp(buff,"end",3) == 0)
{
break;
}
printf("funa:%s\n",buff);
}
}
void* funb()
{
while(1)
{
pthread_mutex_lock(&mutex); //加锁
pthread_cond_wait(&cond,&mutex);
pthread_mutex_unlock(&mutex);
if(strncmp(buff,"end",3) == 0)
{
printf("funb break\n");
break;
}
printf("funb:%s\n",buff);
}
}
int main()
{
pthread_mutex_init(&mutex,NULL);
pthread_cond_init(&cond,NULL);
//启动2线程
pthread_t id1,id2;
pthread_create(&id1,NULL,funa,NULL);
pthread_create(&id2,NULL,funb,NULL);
while(1)
{
fgets(buff,128,stdin);
if(strncmp(buff,"end",3) == 3)
{
pthread_mutex_lock(&mutex);
pthread_cond_broadcast(&cond);
pthread_mutex_unlock(&mutex);
break;
}
else
{
//唤醒
pthread_mutex_lock(&mutex);
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);
}
}
pthread_join(id1,NULL);
pthread_join(id2,NULL);
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond);
exit(0);
}
funa
和funb
是两个线程函数,它们的逻辑基本一致。pthread_mutex_lock(&mutex);
:对互斥锁mutex
加锁,确保在访问共享资源buff
时不会发生数据竞争。pthread_cond_wait(&cond,&mutex);
:将当前线程放入条件变量cond
的等待队列中,并释放互斥锁mutex
。当其他线程调用pthread_cond_signal
或pthread_cond_broadcast
唤醒该线程时,该线程会重新获取互斥锁mutex
。pthread_mutex_unlock(&mutex);
:释放互斥锁mutex
。strncmp(buff,"end",3) == 0
:检查buff
中的字符串是否以end
开头,如果是,则跳出循环,结束线程。printf("funa:%s\n",buff);
或printf("funb:%s\n",buff);
:打印buff
中的字符串。
pthread_mutex_init(&mutex,NULL);
和pthread_cond_init(&cond,NULL);
:分别初始化互斥锁mutex
和条件变量cond
。pthread_create(&id1,NULL,funa,NULL);
和pthread_create(&id2,NULL,funb,NULL);
:创建两个线程,分别执行funa
和funb
函数。fgets(buff,128,stdin);
:从标准输入读取一行字符串,存储到buff
中。if(strncmp(buff,"end",3) == 0)
:检查输入的字符串是否以end
开头。- 如果是,则调用
pthread_cond_broadcast(&cond);
唤醒所有等待在条件变量cond
上的线程,然后跳出循环。 - 如果不是,则调用
pthread_cond_signal(&cond);
唤醒一个等待在条件变量cond
上的线程。
- 如果是,则调用
pthread_join(id1,NULL);
和pthread_join(id2,NULL);
:等待两个线程执行完毕。pthread_mutex_destroy(&mutex);
和pthread_cond_destroy(&cond);
:销毁互斥锁mutex
和条件变量cond
。
2、3 线程安全
多线程运行的时候,不论线程的调度顺序怎样,最终的结果都是一样的,正确的。那么就说这些线程是安全的。
(1)对线程同步,保证同一时刻只有一个线程访问临界资源
(2)对多线程中使用线程安全的函数。线程安全函数---> 如果一个函数能被多个线程同时调用且不发生竞态条件,则称线程是线程安全的。
(3)线程不安全的示例:
主线程和子线程用到的是同一个指针来记录分割到哪里,共享的。(生存期考虑)
#include<stdlib.h>
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<pthread.h>
void* fun(void* arg)
{
char buff[128]={"a b c d e f g "};
char* p=strtok(buff," ");
while(p!=NULL)
{
printf("fun:%c\n",*p);
p=strtok(NULL," ");
sleep(1);
}
}
int main()
{
pthread_t id;
int res= pthread_create(&id,NULL,fun,NULL);
if(res != 0)
{
printf("err");
}
char buff[]="1 2 3 4 5 6";
char* p= strtok(buff," ");
while(p!=NULL)
{
printf("main:%c\n",*p);
p=strtok(NULL," ");
sleep(1);
}
}
- 函数定义:
void* fun(void* arg)
定义了一个线程函数,该函数会在新线程中执行。arg
参数在本函数中未被使用。- 字符串分割:
char buff[128]={"a b c d e f g "};
:定义并初始化一个字符数组buff
,用于存储待分割的字符串。char* p=strtok(buff," ");
:使用strtok
函数将字符串buff
按空格分割成多个子字符串,p
指向第一个分割后的子字符串。while(p!=NULL)
:循环遍历所有分割后的子字符串。printf("fun:%c\n",*p);
:打印每个子字符串的首字符,并在前面加上"fun:"
标识。p=strtok(NULL," ");
:继续分割字符串,获取下一个子字符串。sleep(1);
:线程休眠 1 秒,控制输出节奏。
- 线程创建:
pthread_t id;
:定义一个pthread_t
类型的变量id
,用于存储新线程的 ID。int res= pthread_create(&id,NULL,fun,NULL);
:调用pthread_create
函数创建一个新线程。&id
是存储线程 ID 的变量地址;NULL
表示使用默认的线程属性;fun
是新线程要执行的函数;最后一个NULL
是传递给fun
函数的参数。if(res != 0)
:检查线程创建是否成功,若res
不为 0,表示创建失败,打印"err"
。- 主线程字符串处理:
char buff[]="1 2 3 4 5 6";
:定义并初始化一个字符数组buff
,存储待分割的字符串。char* p= strtok(buff," ");
:使用strtok
函数按空格分割字符串,p
指向第一个分割后的子字符串。while(p!=NULL)
:循环遍历所有分割后的子字符串。printf("main:%c\n",*p);
:打印每个子字符串的首字符,并在前面加上"main:"
标识。p=strtok(NULL," ");
:继续分割字符串,获取下一个子字符串。
(4)保证线程安全的示例:(使用线程安全函数)
#include<stdlib.h>
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<pthread.h>
void* fun(void* arg)
{
char buff[128]={"a b c d e f g "};
char *q = NULL;
char *p=strtok_r(buff," ",&q);
while(p!=NULL)
{
printf("fun:%c\n",*p);
p=strtok_r(NULL," ",&q);
sleep(1);
}
}
int main()
{
pthread_t id;
int res= pthread_create(&id,NULL,fun,NULL);
if(res != 0)
{
printf("err");
}
char buff[]="1 2 3 4 5 6";
char *q=NULL;
char *p= strtok_r(buff," ",&q);
while(p!=NULL)
{
printf("main:%c\n",*p);
p=strtok_r(NULL," ",&q);
sleep(1);
}
}
40,1 底端
char *q = NULL;
:strtok_r
函数需要一个额外的指针q
来保存分割的上下文信息。char *p=strtok_r(buff," ",&q);
:调用strtok_r
函数将字符串buff
按空格分割成多个子字符串,p
指向第一个分割后的子字符串,q
用于保存分割的位置。- 主线程字符串处理:
char buff[]="1 2 3 4 5 6";
:定义并初始化一个字符数组buff
,存储待分割的字符串。char *q=NULL;
:同样,strtok_r
需要一个额外指针q
来保存分割上下文。char *p= strtok_r(buff," ",&q);
:使用strtok_r
函数按空格分割字符串,p
指向第一个分割后的子字符串。while(p!=NULL)
:循环遍历所有分割后的子字符串。printf("main:%c\n",*p);
:打印每个子字符串的首字符,并在前面加上"main:"
标识。p=strtok_r(NULL," ",&q);
:继续分割字符串,获取下一个子字符串。
三、生产者-消费者模型
生产者消费者问题即:两个或者更多的线程共享同一个缓冲区,其中一个或多个线程作为“生产者”会不断向缓冲区中添加数据,另一个或者多个线程作为“消费者”从缓冲区中取走数据。
生产者 - 消费者问题是一个经典的多线程同步问题,以下是其工作原理的详细解释:
(1)概念定义
- 生产者:是负责生产数据或任务的线程或进程。它不断地生成数据,并将其放入一个共享的缓冲区中。
- 消费者:是负责处理数据或任务的线程或进程。它从共享缓冲区中取出数据进行处理。
(2)共享缓冲区
- 它是生产者和消费者之间的通信桥梁,具有一定的容量限制。生产者将生产的数据放入缓冲区,消费者从缓冲区取出数据。当缓冲区已满时,生产者需要等待,直到有空间可用;当缓冲区为空时,消费者需要等待,直到有数据可消费。
注意:
- 生产者和消费者必须互斥的使用缓冲区;
- 缓冲区为空时,消费者不能读取数据;
- 缓冲区满时,生产者不能添加数据;
(3)工作流程
- 生产者生产数据:生产者线程不断地生成数据。假设生产者生产一个产品,比如一个整数。
- 检查缓冲区空间:生产者在将数据放入缓冲区之前,需要检查缓冲区是否有空间。如果缓冲区已满,生产者线程会被阻塞,进入等待状态,直到缓冲区有空间。
- 放入数据到缓冲区:如果缓冲区有空间,生产者将数据放入缓冲区,并通知消费者有新的数据可用。例如,将生产的整数放入一个队列类型的缓冲区中。
- 消费者检查数据:消费者线程会不断检查缓冲区是否有数据。当缓冲区为空时,消费者线程会被阻塞,进入等待状态,直到有数据可消费。
- 取出数据并处理:当消费者发现缓冲区有数据时,它从缓冲区取出数据进行处理。例如,消费者取出生产者放入的整数,并进行一些计算或其他操作。
- 通知生产者缓冲区状态:消费者在取出数据后,可能会通知生产者缓冲区有了新的空间,以便生产者可以继续生产数据。
(4)实现方式
- 使用互斥锁和条件变量:互斥锁用于保护共享缓冲区,确保同一时间只有一个线程可以访问缓冲区。条件变量用于实现线程间的通信和同步,让生产者和消费者在适当的时候等待和唤醒。
- 使用信号量:信号量可以用来控制对共享资源的访问。例如,使用一个信号量来表示缓冲区中可用的空间数量,另一个信号量来表示缓冲区中已有的数据数量。生产者在生产数据时,需要等待可用空间信号量,然后将数据放入缓冲区,并增加已用空间信号量;消费者在消费数据时,需要等待已用空间信号量,然后从缓冲区取出数据,并增加可用空间信号量。
(5)具体实现以及代码示例:
加锁的话,怎么加?
先看ps1是否执行通过,能否向缓冲区写入数据,(缓冲区会满,若直接写,容易发生死锁)能写再加锁,然后进行操作,再解锁,再vs2释放;消费者先申请ps2,看缓冲区是否被占用,若申请成功,加锁,继续后续操作。
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<stdio.h>
#include<pthread.h>
#include<semaphore.h>
#define BUFF_SIZE 30
#define XF_NUM 3
#define SC_NUM 2
sem_t sc_sem; //生产者信号量
sem_t xf_sem; //消费者信号量
pthread_mutex_t mutex;
//int sc_num=2; //2个生产者
//int xf_num=3; //3个消费者
int arr[BUFF_SIZE]; //缓冲区
int in = 0; //写入位置
int out = 0; //从0位置开始读
void* sc_fun(void* arg)
{
for(int i = 0;i<30; i++)
{
sem_wait(&sc_sem);//ps1
pthread_mutex_lock(&mutex);
arr[in] = rand () % 100;
printf("生产者在:%d位置写入数据:%d\n",in,arr[in]);
in = (in + 1) % BUFF_SIZE; //到30要重新开始写
pthread_mutex_unlock(&mutex);
sem_post(&xf_sem);//vs2
}
}
void* xf_fun(void* arg)
{
for(int i = 0;i < 20;i++)
{
sem_wait(&xf_sem);//ps2
pthread_mutex_lock(&mutex);
printf("-------------消费者在:%d位置消费数据:%d\n",out,arr[out]);
out = (out + 1)% BUFF_SIZE;
pthread_mutex_unlock(&mutex);
sem_post(&sc_sem);
}
}
int main()
{
sem_init(&sc_sem,0,BUFF_SIZE);//消费者信号量,初始值30
sem_init(&xf_sem,0,0);
pthread_mutex_init(&mutex,NULL);
pthread_t sc_id[SC_NUM];
for(int i=0;i<SC_NUM;i++)
{
//创建生产者线程
pthread_create(&sc_id[i],NULL,sc_fun,NULL);
}
pthread_t xf_id[XF_NUM];
for(int i=0;i<XF_NUM;i++)
{
pthread_create(&xf_id[i],NULL,xf_fun,NULL);
}
for(int i= 0; i < SC_NUM ; i++)
{
pthread_join(sc_id[i],NULL);
}
for(int i =0 ;i < XF_NUM ; i++)
{
pthread_join(xf_id[i],NULL);
}
pthread_mutex_destroy(&mutex);
sem_destroy(&sc_sem);
sem_destroy(&xf_sem);
exit(0);
}
(6)死锁:
死锁产生的原因
- 资源竞争:系统中存在多个进程共享有限的资源,当进程对资源的需求超过资源的可用数量时,就可能导致死锁。例如,两个进程都需要使用打印机和扫描仪,而系统中只有一台打印机和一台扫描仪,若两个进程分别占用了其中一个资源并等待另一个资源,就会发生死锁。
- 进程推进顺序不当:进程在运行过程中,其推进顺序如果不恰当,也可能引发死锁。例如,进程 A 先占用了资源 R1,然后请求资源 R2;进程 B 先占用了资源 R2,然后请求资源 R1。如果这两个进程按照这样的顺序推进,就会形成死锁。
死锁产生的必要条件
- 互斥条件:资源在某一时刻只能被一个进程所使用,其他进程若要使用该资源,必须等待资源被释放。
- 请求和保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而新请求的资源又被其他进程占用,此时进程不会释放自己已保持的资源,而是继续等待新资源。
- 不可剥夺条件:进程所获得的资源在未使用完之前,不能被其他进程强行剥夺,只能由获得资源的进程自己释放。
- 循环等待条件:存在一组进程,它们之间形成了一个循环等待链,每个进程都在等待下一个进程释放其所需要的资源。
操作系统解决死锁的方法
- 预防死锁:通过破坏死锁产生的必要条件来预防死锁的发生。
- 破坏互斥条件:允许资源共享,但是有些资源本身具有互斥性,如打印机,很难完全破坏互斥条件。
- 破坏请求和保持条件:可以要求进程在启动时一次性申请所有需要的资源,或者在申请新资源时先释放已占用的资源。
- 破坏不可剥夺条件:规定当一个进程申请的资源被其他进程占用时,操作系统可以剥夺该资源分配给其他进程。
- 破坏循环等待条件:对资源进行编号,要求进程按照编号的顺序申请资源,避免形成循环等待。
- 避免死锁:在资源分配过程中,通过某种算法来动态地检测系统是否处于安全状态,避免系统进入不安全状态,从而避免死锁的发生。常见的算法是银行家算法,该算法通过检查系统的资源分配情况和进程的资源请求情况,判断是否存在安全序列。如果存在安全序列,则系统处于安全状态,可以满足进程的资源请求;否则,系统处于不安全状态,拒绝进程的资源请求。
- 检测死锁:定期检查系统是否存在死锁。操作系统可以通过维护资源分配图来检测死锁。资源分配图中包含进程和资源两种节点,边表示进程对资源的请求或分配关系。通过对资源分配图进行化简,如果最终图中存在不可化简的环,则表示系统发生了死锁。
- 解除死锁:当检测到死锁发生后,需要采取措施来解除死锁。
- 资源剥夺法:从其他进程中剥夺足够数量的资源给死锁进程,以解除死锁状态。
- 撤销进程法:强制撤销部分甚至全部死锁进程,并剥夺这些进程的资源,以打破死锁。可以选择撤销那些优先级较低、已运行时间较短的进程,以减少系统的损失。
- 进程回滚法:让死锁进程回滚到之前的某个状态,重新申请资源,避免死锁。这种方法需要操作系统具有进程回滚的功能,并且要记录进程的运行历史。
进程池和线程池是在并发编程中用于管理和复用进程或线程的技术,它们在提高系统性能、资源利用率和降低开销等方面发挥着重要作用。以下是对进程池和线程池的详细介绍:
进程池
概念
进程池是一种预先创建一定数量的进程并将其保存在一个池中,当有任务到来时,从进程池中选取一个空闲的进程来执行该任务,任务完成后,该进程不会被销毁,而是返回进程池等待下一个任务。
工作原理
- 初始化:在程序启动时,创建一定数量的进程并将它们放入进程池中。
- 任务分配:当有新任务到来时,从进程池中选择一个空闲的进程,将任务分配给该进程执行。
- 任务执行:被选中的进程执行任务,完成后通知进程池。
- 进程回收:任务完成后,进程返回进程池,继续等待下一个任务。
优点
- 减少进程创建和销毁的开销:进程的创建和销毁是比较昂贵的操作,使用进程池可以避免频繁创建和销毁进程,从而提高系统性能。
- 控制并发度:可以根据系统资源和任务需求,限制进程池中的进程数量,避免过多的进程竞争资源,导致系统性能下降。
- 提高响应速度:由于进程已经预先创建好,当有任务到来时,可以立即分配给进程执行,减少了任务的等待时间。
缺点
- 资源占用较大:每个进程都有自己独立的内存空间和系统资源,进程池中的进程数量过多会导致系统资源的浪费。
- 进程间通信复杂:进程之间的通信需要使用专门的机制,如管道、消息队列、共享内存等,增加了编程的复杂度。
应用场景
- 计算密集型任务:如数据处理、科学计算等,进程池可以充分利用多核 CPU 的优势,提高计算效率。
- 长时间运行的任务:对于一些需要长时间运行的任务,使用进程池可以避免频繁创建和销毁进程带来的开销。
线程池
概念
线程池是一种预先创建一定数量的线程并将其保存在一个池中,当有任务到来时,从线程池中选取一个空闲的线程来执行该任务,任务完成后,该线程不会被销毁,而是返回线程池等待下一个任务。
工作原理
- 初始化:在程序启动时,创建一定数量的线程并将它们放入线程池中。
- 任务分配:当有新任务到来时,从线程池中选择一个空闲的线程,将任务分配给该线程执行。
- 任务执行:被选中的线程执行任务,完成后通知线程池。
- 线程回收:任务完成后,线程返回线程池,继续等待下一个任务。
优点
- 减少线程创建和销毁的开销:线程的创建和销毁比进程的创建和销毁开销小,使用线程池可以进一步减少系统开销。
- 提高资源利用率:线程共享进程的内存空间和系统资源,线程池可以更高效地利用系统资源。
- 提高响应速度:由于线程已经预先创建好,当有任务到来时,可以立即分配给线程执行,减少了任务的等待时间。
缺点
- 线程安全问题:多个线程同时访问共享资源时,可能会导致数据不一致等线程安全问题,需要使用同步机制来解决。
- 线程数量过多会影响性能:线程数量过多会导致线程上下文切换频繁,增加系统开销,降低系统性能。
应用场景
- I/O 密集型任务:如网络请求、文件读写等,线程池可以在等待 I/O 操作完成时,让其他线程继续执行任务,提高系统的并发性能。
- 短时间内有大量任务的场景:对于一些短时间内有大量任务需要处理的场景,使用线程池可以快速响应任务,提高系统的吞吐量。
进程池和线程池的比较
- 资源占用:进程池中的每个进程都有自己独立的内存空间和系统资源,资源占用较大;线程池中的线程共享进程的内存空间和系统资源,资源占用较小。
- 创建和销毁开销:进程的创建和销毁开销比线程的创建和销毁开销大,因此线程池在这方面的性能更好。
- 并发度:进程池可以利用多核 CPU 的优势,实现更高的并发度;线程池由于共享进程的资源,并发度相对较低。
- 编程复杂度:进程间通信需要使用专门的机制
(7)一个多线程的程序执行了fork会怎么样?
fork只会有一条路径,子进程只有一条执行路径。
多线程相关影响
- 线程状态:父进程中的所有线程都会在子进程中得到复制。但是,子进程中只有一个线程(通常是调用
fork
的那个线程)会继续执行,其他线程在子进程中处于 “冻结” 状态。这是因为fork
之后,子进程需要一个确定的执行起点,以避免多个线程同时执行可能导致的混乱。- 锁状态:如果父进程中的线程持有锁,那么在
fork
之后,子进程中的相应锁状态是未定义的。这可能导致子进程在后续执行中出现死锁或其他错误。例如,如果一个线程在父进程中持有互斥锁,然后调用fork
,子进程中该互斥锁的状态是不确定的,其他试图获取该锁的线程可能会被阻塞,而持有锁的线程在子进程中又不会主动释放锁,从而导致死锁。- 线程局部存储(Thread - Local Storage,TLS):父进程中的线程局部存储变量在子进程中是独立的副本。也就是说,子进程对这些变量的修改不会影响到父进程中的对应变量,反之亦然。
执行顺序
- 父进程和子进程的执行顺序是不确定的,这取决于操作系统的调度策略。在
fork
之后,父进程和子进程会并发执行,它们可能会交替执行,也可能会有一个进程先执行一段时间,然后另一个进程再执行。
Linux,从内核角度来说,它并没有线程这个概念。Linux把所有的线程当做进程来实现。线程仅仅被视为一个与其他进程来共享某些资源的进程。每个线程都拥有唯一隶属于自己的task_struct,在内核中,只是该进程和其他一些进程共享某些资源,如地址空间。