Linux系统编程 day09 线程同步
Linux系统编程 day09 线程同步
- 1.互斥锁
- 2.死锁
- 3.读写锁
- 4.条件变量(生产者消费者模型)
- 5.信号量
1.互斥锁
互斥锁是一种同步机制,用于控制多个线程对共享资源的访问,确保在同一时间只有一个线程可以访问特定的资源或执行特定的操作。如果没有互斥锁,对于多个线程的程序则可能会出现一些未知的问题。比如下面有两个线程,一个是打印“hello world”的线程,一个是打印“HELLO WORLD”的线程。但是不同的是这个词汇是分两次分别打印的,程序代码如下。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <pthread.h>
#include <time.h>// 打印 hello world
void *mythread1(void *args)
{while(1){printf("hello ");sleep(rand() % 3);printf("world\n");sleep(rand() % 3);}pthread_exit(NULL);
}// 打印HELLO WORLD
void *mythread2(void *args)
{while(1){printf("HELLO ");sleep(rand() % 3);printf("WORLD\n");sleep(rand() % 3);}pthread_exit(NULL);
}int main()
{int ret = 0;pthread_t thread1, thread2;// 初始化随机数种子srand(time(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);return 0;
}
将上述文件命名为01.pthread_lock.c,使用命令make 01.pthread_lock
,则会自动编译01.pthread_lock.c生成01.pthread_lock,运行该程序可以看到如下现象。
可以发现两个进程打印出来的“hello world”和“HELLO WORLD”并不是连续在一起的,所以需要加互斥锁将打印的代码进行加锁。下面是互斥锁的一些函数。
/*** @brief 定义一把互斥锁*/
pthread_mutex_t mutex_var;/** * @brief 初始化互斥锁* @param mutex 互斥锁* @param mutexattr 互斥锁属性,传入NULL为默认属性* @return 是否初始化成功,初始化成功返回0,初始化失败返回错误码* @retval int*/
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr);/** * @brief 互斥锁加锁* @param mutex 互斥锁* @return 是否加锁成功,加锁成功返回0,加锁失败会阻塞在这里,发生错误返回错误码* @retval int*/
int pthread_mutex_lock(pthread_mutex_t *mutex);/** * @brief 互斥锁尝试加锁* @param mutex 互斥锁* @return 是否加锁成功,加锁成功返回0,加锁失败直接返回0,发生错误返回错误码* @retval int*/
int pthread_mutex_trylock(pthread_mutex_t *mutex);/** * @brief 互斥锁解锁* @param mutex 互斥锁* @return 是否解锁成功,解锁成功返回0,解锁失败返回错误码* @retval int*/
int pthread_mutex_unlock(pthread_mutex_t *mutex);/** * @brief 摧毁互斥锁* @param mutex 互斥锁* @return 摧毁成功返回0,摧毁失败返回错误码* @retval int*/
int pthread_mutex_destroy(pthread_mutex_t *mutex);
现在在每个线程运行的函数中加入互斥锁,代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <pthread.h>
#include <time.h>// 定义一把互斥锁
pthread_mutex_t mutex;// 打印 hello world
void *mythread1(void *args)
{while(1){// 加锁pthread_mutex_lock(&mutex);printf("hello ");sleep(rand() % 3);printf("world\n");sleep(rand() % 3);// 解锁pthread_mutex_unlock(&mutex);}pthread_exit(NULL);
}// 打印HELLO WORLD
void *mythread2(void *args)
{while(1){// 加锁pthread_mutex_lock(&mutex);printf("HELLO ");sleep(rand() % 3);printf("WORLD\n");sleep(rand() % 3);// 解锁pthread_mutex_unlock(&mutex);}pthread_exit(NULL);
}int main()
{int ret = 0;pthread_t thread1, 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;
}
编译后运行如下结果:
可以观察到这次的运行结果并不会出现线程间交叉打印的情况。
2.死锁
死锁是由程序员对互斥锁的使用不当而产生的,并不是操作系统提供的一种机制。死锁的产生主要由两种形式。
第一种是自己锁自己,也就是调用了两次加锁。也就是说线程1加锁了,再没释放锁的时候线程1又申请加锁,此时就会产生死锁,且死锁阻塞到该位置,还有一种情况是线程1加锁了,但是线程1的整个程序里面没有释放锁的相关操作或者根本执行不到释放锁的代码,此时若线程2申请加锁则程序阻塞在此处,若线程1继续申请加锁则阻塞在线程1加锁处。
第二种是相互锁住,线程1拥有A锁请求B锁,线程2拥有B锁请求A锁,这样就相互阻塞住了。
合理的编写程序能够避免死锁,解决司所有一下一些方法:
- 1.让线程按照一定的顺序去访问共享资源。
- 2.在访问其它锁的时候先释放自己的锁。
- 3.调用
pthread_mutex_trylock
,该函数若加锁不成功会立刻返回,此时就不会造成阻塞死等。
3.读写锁
读写锁也叫共享独占锁,读时共享,写时独占,极大地提高了程序运行的效率。也就是当读写锁以读模式锁住的时候,它是共享的,当读写锁以写模式锁住的时候,是独占的。读写锁适合的场景是读的次数远大于写的情况下的。
读写锁有以下一些特性:
- 1.读写锁是写模式加锁的时候,在解锁前所有要对该锁加锁的线程都会阻塞。
- 2.读写锁是读模式加锁的时候,如果线程以读模式加锁则会成功,以写模式加锁会阻塞。
- 3.读写锁是读模式加锁的时候,如果既有读线程也有写线程,则尝试加锁都会被阻塞。若锁被释放,则写线程会请求锁成功,而读线程会继续阻塞。也就是读写都有的时候优先满足写模式的锁,也就是写比读优先级更高。
下面是读写锁的接口函数:
/*** @brief 定义读写锁变量*/
pthread_rwlock_t rwlock_var;/*** @brief 读写锁初始化* * 初始化读写锁 * @param rwlock 读写锁* @param attr 读写锁属性,传入NULL为默认属性* @return 初始化成功返回0,反之返回错误码* @retval int*/
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);/*** @brief 读写锁加读锁* @param rwlock 读写锁* @return 成功加锁返回0,失败则返回错误码,锁被占用则一直阻塞* @retval int*/
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);/*** @brief 读写锁尝试加读锁* @param rwlock 读写锁* @return 成功加锁返回0,失败则返回错误码,锁被占用也会直接返回* @retval int*/
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);/*** @brief 读写锁加写锁* @param rwlock 读写锁* @return 成功加锁返回0,失败则返回错误码,锁被占用则一直阻塞* @retval int*/
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);/*** @brief 读写锁尝试加写锁* @param rwlock 读写锁* @return 成功加锁返回0,失败则返回错误码,锁被占用也会直接返回* @retval int*/
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);/*** @brief 读写锁解锁* @param rwlock 读写锁* @return 成功解锁返回0,失败则返回错误码* @retval int*/
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);/*** @brief 摧毁读写锁* @param rwlock 读写锁* @return 成功摧毁返回0,失败则返回错误码* @retval int*/
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
下面举一个关于读写锁的例子,有3个写线程,5个读线程,写线程修改数字,读线程读取数字输出到终端。代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <time.h>
#include <sys/types.h>
#include <pthread.h>// 线程总数
#define THREAD_COUNT 8// 定义读写锁
pthread_rwlock_t rwlock;// 计数
int number = 0;// 读线程回调函数
void *thread_read(void *args)
{int i = *(int *)args;int cur;while(1){// 读写锁加读锁pthread_rwlock_rdlock(&rwlock);cur = number;printf("[%d]-R: [%d]\n", i, cur);// 读写锁加写锁pthread_rwlock_unlock(&rwlock);sleep(rand() % 3);}
}// 写线程回调函数
void *thread_write(void *args)
{int i = *(int *)args;int cur;while(1){// 读写锁加写锁pthread_rwlock_wrlock(&rwlock);cur = number;cur += 1;number = cur;printf("[%d]-W: [%d]\n", i, cur);// 读写锁解锁pthread_rwlock_unlock(&rwlock);sleep(rand() % 3);}
}int main()
{int ret = 0;int arr[THREAD_COUNT]; // 记录第几个线程pthread_t threads[THREAD_COUNT]; // 线程数组// 初始化随机数种子srand(time(NULL));// 初始化读写锁pthread_rwlock_init(&rwlock, NULL);// 创建3个写线程for(int i = 0; i < 3; i++){arr[i] = i;ret = pthread_create(&threads[i], NULL, thread_write, &arr[i]);if(ret != 0){printf("pthread_create error: [%s]\n", strerror(ret));return -1;}}// 创建5个读线程for(int i = 3; i < THREAD_COUNT; i++){arr[i] = i;ret = pthread_create(&threads[i], NULL, thread_read, &arr[i]);if(ret != 0){printf("pthread_create error: [%s]\n", strerror(ret));return -1;}}// 回收子线程for(int i = 0; i < THREAD_COUNT; i++){pthread_join(threads[i], NULL);}// 释放锁pthread_rwlock_destroy(&rwlock);return 0;
}
编译程序运行的结果如下:
可以发现读线程输出的数字都是写线程修改后的,并不会出现读线程的数据是不符合写进程修改后的。如果没有加读写锁,则会让读写乱套,当写线程修改完数据之后读线程也可能正好输出修改前的值。
4.条件变量(生产者消费者模型)
条件变量主要是用于生产者消费者模型的。所谓生产者消费者模型就是生产者负责生成东西,而消费者负责对生产者生产者的东西用于消费。在这种情况下,生产者消费的东西其实也就是临界资源,所以需要使用互斥锁进行访问。而在生产者消费者模型中无法确定是生产者先访问还是消费者先访问,所以在生产者没有生产东西的时候消费者也可能需要进行消费,或者生产者生产的进度会更不上消费者消耗的进度。因此,为了保证消费者每次消费都是在生产者生成东西之后或者生产的东西有剩余,所以需要使用到条件变量,此时原来并行的生产者消费者就变成了串行的生产者消费者。关于条件变量的接口如下:
/*** @brief 条件变量定义,定义一个条件变量*/
pthread_cond_t cond_var;/*** @brief 初始化条件变量* @param cond 条件变量* @param cond_attr 条件变量属性,传入NULL为默认属性* @return 初始化成功返回0,失败返回错误码* @retval int*/
int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr);/*** @brief 唤醒至少一个阻塞在该条件变量上的线程* @param cond 条件变量* @return 唤醒成功返回0,失败返回0* @retval int*/
int pthread_cond_signal(pthread_cond_t *cond);/*** @brief 唤醒所有阻塞在该条件变量上的线程* @param cond 条件变量* @return 唤醒成功返回0,失败返回0* @retval int*/
int pthread_cond_broadcast(pthread_cond_t *cond);/*** @brief 阻塞等待条件变量满足* * 当条件变量不满足的时候,会阻塞线程并进行解锁* 当条件变量满足的时候,会解除线程阻塞并加锁* @param cond 条件变量* @param mutex 互斥锁* @return 成功返回0,失败返回0* @retval int*/
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);/*** @brief 销毁条件变量* @param cond 条件变量* @return 销毁成功返回0,失败返回0* @retval int*/
int pthread_cond_destroy(pthread_cond_t *cond);
需要注意的是条件变量本身并不是锁,但是它可以造成线程的阻塞,所以通常需要与互斥锁配合使用。使用互斥锁是为了保护共享数据,使用条件变量可以使线程阻塞,等待某个条件的发生,当条件满足的时候解除阻塞。
下面给一个关于生产者消费者的例子,有一个链表,生产者负责不断地从堆区申请空间开辟内存并且存入数据并将节点放到链表上,而消费者负责在链表上不断地读取数据并且释放节点,代码如下所示。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <time.h>
#include <pthread.h>// 链表节点定义
typedef struct node
{int data;struct node *next;
} node_t;// 互斥锁
pthread_mutex_t mutex;
// 条件变量
pthread_cond_t cond;
// 头结点
node_t *head = NULL;// 生产者线程
void *thread_product(void *args)
{while(1){// 申请节点node_t *pnode = (node_t *)malloc(sizeof(node_t));if(pnode == NULL){perror("malloc error");exit(-1);}pnode->data = rand() % 1000;// 加互斥锁pthread_mutex_lock(&mutex);pnode->next = head;head = pnode;printf("[Productor]: %d\n", pnode->data);// 解锁pthread_mutex_unlock(&mutex);// 通知消费者线程pthread_cond_signal(&cond);sleep(rand() % 3);}pthread_exit(NULL);
}// 消费者线程
void *thread_consume(void *args)
{while(1){// 加互斥锁pthread_mutex_lock(&mutex);if(head == NULL){// 条件满足解除阻塞并加锁// 条件不满足阻塞并解锁pthread_cond_wait(&cond, &mutex);}node_t *pnode = head;head = pnode->next;printf("[Consumer]: %d\n", pnode->data);free(pnode);pnode = NULL;// 解锁pthread_mutex_unlock(&mutex);sleep(rand() % 3);}pthread_exit(NULL);
}int main()
{int ret;// 生产者与消费者线程pthread_t thread_productor, thread_consumer;// 初始化随机数种子srand(time(NULL));// 初始化条件变量与互斥锁pthread_mutex_init(&mutex, NULL);pthread_cond_init(&cond, NULL);ret = pthread_create(&thread_productor, NULL, thread_product, NULL);if(ret != 0){printf("pthread_create error: [%s]\n", strerror(ret));return -1;}ret = pthread_create(&thread_consumer, NULL, thread_consume, NULL);if(ret != 0){printf("pthread_create error: [%s]\n", strerror(ret));return -1;}// 回收线程pthread_join(thread_productor, NULL);pthread_join(thread_consumer, NULL);// 销毁互斥锁与条件变量pthread_mutex_destroy(&mutex);pthread_cond_destroy(&cond);return 0;
}
运行可以发现消费者消费的是生产者最后一次生产出来的。
如果将上述的代码改为多个消费者线程和多个生产者线程的时候,则上述的代码就会出现Segmentation fault (core dumped)
的情况。首先需要分析这个问题是如何产生的。这个问题的原因就是访问了非法的空间内存,而这种情况只会在消费者线程中出现。而经过定位可以发现对NULL
指针进行了操作,也就是head = pnode->next; printf("[Consumer]: %d\n", pnode->data);
。首先在生产者线程中调用了pthread_cond_signal
函数的时候会通知一个或者多个消费者线程唤醒满足,而此时这几个线程都不在处于阻塞状态,就会进行解锁,当连续多个消费者线程依次解锁的时候向下运行就会导致访问到非法空间。比如链表中只有一个节点,而此时有三个消费者线程处于阻塞状态且阻塞在pthread_cond_wait
处。此时生产者线程调用了pthread_cond_signal
则会唤醒这消费者三个线程的一个或者是两个,又或者是三个。此时三个的条件变量都满足,第一个消费者线程抢到了锁消费了一个结点。而此时生产者线程没有创造新的节点,由于第二个线程已经满足了条件变量,此时它抢到了互斥锁,则也会向下执行,由于前面的线程已经消费了唯一的一个线程,所以此时就会访问到非法的内存空间。应对这种情况则是在pthread_cond_wait
后面对头结点再一次进行判断,若满足条件则向下执行,不满足条件则解锁并使用continue
进入下一次循环。以5个生产者线程10个消费者线程为例,代码如下所示:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <time.h>
#include <string.h>// 线程数量
#define PRODUCER_THREAD_COUNT 5 // 生产线程数量
#define CONSUMER_THREAD_COUNT 10 // 消费线程数量// 链表节点
struct node
{int data;struct node *next;
};// 头结点
struct node *head = NULL;// 互斥锁
pthread_mutex_t mutex;// 条件变量
pthread_cond_t cond;void *thread_producer(void *arg)
{struct node *pnode = NULL;int idx = (int)(long)arg;while(1){pnode = (struct node *)malloc(sizeof(struct node));if(pnode == NULL){perror("malloc error");exit(-1);}pnode->data = rand() % 100;// 加锁pthread_mutex_lock(&mutex);pnode->next = head;head = pnode;printf("P[%d]: %d\n", idx, pnode->data);pthread_mutex_unlock(&mutex);pthread_cond_signal(&cond);sleep(rand() % 3);}pthread_exit(NULL);
}void *thread_consumer(void *arg)
{struct node *pnode = NULL;int idx = (int)(long)arg;while(1){ pthread_mutex_lock(&mutex);if(head == NULL){pthread_cond_wait(&cond, &mutex);}// 若头节点为空if(head == NULL){// 解锁pthread_mutex_unlock(&mutex);continue;}pnode = head;head = head->next;printf("C[%d]: %d\n", idx, pnode->data);pthread_mutex_unlock(&mutex);free(pnode);pnode = NULL;sleep(rand() % 3);}pthread_exit(NULL);
}int main()
{int ret = 0;pthread_t producer_thread[PRODUCER_THREAD_COUNT], consumer_thread[CONSUMER_THREAD_COUNT];srand((unsigned int)time(NULL));// 初始化互斥锁pthread_mutex_init(&mutex, NULL);// 初始化条件变量pthread_cond_init(&cond, NULL);// 创建生产者线程for(int i = 1; i <= PRODUCER_THREAD_COUNT; i++){ret = pthread_create(&producer_thread[i - 1], NULL, thread_producer, (void *)(long)i);if(ret != 0){printf("pthread_create error: %s\n", strerror(ret));exit(-1);}}// 创建消费者线程for(int i = 1; i <= CONSUMER_THREAD_COUNT; i++){ ret = pthread_create(&consumer_thread[i - 1], NULL, thread_consumer, (void *)(long)i);if(ret != 0){printf("pthread_create error: %s\n", strerror(ret));exit(-1);}}// 回收线程for(int i = 0; i < PRODUCER_THREAD_COUNT; i++){pthread_join(producer_thread[i], NULL);}for(int i = 0; i < CONSUMER_THREAD_COUNT; i++){pthread_join(consumer_thread[i], NULL);}// 摧毁互斥锁pthread_mutex_destroy(&mutex);return 0;
}
运行结果如下:
5.信号量
信号量可以理解为是升级版的互斥锁,因为互斥锁只能运行一个线程进行访问,而信号量可以支持多个线程或者进程。关于信号量的接口如下所示:
/*** @brief 定义信号量*/
sem_t sem_var;/*** @brief 初始化信号量* @param sem 信号量* @param pshared 0表示线程同步,1表示进程同步* @param value 最多有多少个线程操作共享数据* @return 成功返回0,失败返回-1,并设置errno的值。* @retval int*/
int sem_init(sem_t *sem, int pshared, unsigned int value);/*** @brief 相当于sem--,当sem为0的时候,引起阻塞,即加锁。* @param sem 信号量* @return 成功返回0,失败返回-1,并设置error的值* @retval int*/
int sem_wait(sem_t *sem);/*** @brief 与sem_wait一样,但是不会阻塞* @param sem 信号量* @return 成功返回0,失败返回-1,并设置error的值* @retval int*/
int sem_trywait(sem_t *sem);/*** @brief 相当于sem++* @param sem 信号量* @return 成功返回0,失败返回-1,并设置errno的值* @retval int*/
int sem_post(sem_t *sem);/*** @brief 摧毁信号量* @param sem 信号量* @return 成功返回0,失败返回-1,并设置errno的值*/
int sem_destroy(sem_t *sem);
什么时候用信号量呢?也就是当一个共享资源能够被多个现成进行操作的时候。比如停车场,里面会有很多个车位,可以同时允许多个车位进行停靠,代码如下所示:
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>// 车位数量
#define PARKING_COUNT 10// 线程数量
#define ENTRY_PARKING_THREAD_COUNT 10 // 进入停车场线程
#define EXIT_PARKING_THREAD_COUNT 10 // 离开停车场线程// 结点模拟停车场
struct park
{int use; // 是否被使用int parking_space; // 停车位编号int license_plate_number; // 停车车牌号
}park[PARKING_COUNT];// 信号量
sem_t entry_sem; // 进入停车场信号量
sem_t exit_sem; // 离开停车场信号量// 进入停车场线程
void *thread_entryParking(void *arg)
{// 标记是否停车int parking_space = -1;int license_plate_number = 0;while(1){// 车位标志parking_space = 0;license_plate_number = rand() % 10000 + 10000;printf("车牌号[%d]驶来了\n", license_plate_number);sleep(rand() % 5);// 开始停车sem_wait(&entry_sem);// 寻找车位for(int i = 0; i < PARKING_COUNT; i++){if(park[i].use == 0){parking_space = park[i].parking_space;break;}}// 没有找到停车位if(parking_space == 0){sem_post(&entry_sem);printf("车牌号[%d]因为没有车位离开了\n", license_plate_number);continue;}// 开始停车park[parking_space - 1].license_plate_number = license_plate_number;park[parking_space - 1].use = 1;printf("车牌号[%d]停进了停车场的[%d]车位\n", license_plate_number, park[parking_space - 1].parking_space);// 通知可以有车离开sem_post(&exit_sem);}pthread_exit(NULL);
}// 离开停车场
void *thread_exitParking(void *arg)
{// 车位int parking_space = 0;while(1){sleep(rand() % 5);// 随机车位parking_space = rand() % PARKING_COUNT + 1;sem_wait(&exit_sem);// 该车位没有车继续下一次if(park[parking_space - 1].use == 0){sem_post(&exit_sem);continue;}printf("车牌号[%d]离开了停车场的[%d]车位\n", park[parking_space - 1].license_plate_number, park[parking_space - 1].parking_space);// 标记车位没有使用park[parking_space - 1].use = 0;// 可停车数加1sem_post(&entry_sem);}pthread_exit(NULL);
}int main()
{int ret = 0;// 进入停车场线程pthread_t entryParking_thread[ENTRY_PARKING_THREAD_COUNT];// 离开停车场线程pthread_t exitParking_thread[EXIT_PARKING_THREAD_COUNT];srand((unsigned int)time(NULL));// 初始化信号量sem_init(&entry_sem, 0, PARKING_COUNT);sem_init(&exit_sem, 0, 0);// 初始化停车场memset(&park, 0x00, sizeof park);for(int i = 0; i < PARKING_COUNT; i++){park[i].parking_space = i + 1;}// 创建停车线程for(int i = 0; i < ENTRY_PARKING_THREAD_COUNT; i++){ret = pthread_create(&entryParking_thread[i], NULL, thread_entryParking, NULL);if(ret != 0){printf("pthread_create error: %s\n", strerror(ret));exit(-1);}}// 创建离开停车场线程for(int i = 0; i < EXIT_PARKING_THREAD_COUNT; i++){ret = pthread_create(&exitParking_thread[i], NULL, thread_exitParking, NULL);if(ret != 0){printf("pthread_create error: %s\n", strerror(ret));exit(-1);}}// 回收线程for(int i = 0; i < ENTRY_PARKING_THREAD_COUNT; i++){pthread_join(entryParking_thread[i], NULL);}for(int i = 0; i < EXIT_PARKING_THREAD_COUNT; i++){pthread_join(exitParking_thread[i], NULL);}// 摧毁信号量sem_destroy(&entry_sem);sem_destroy(&exit_sem);return 0;
}
运行上述代码,结果如下:
相关文章:

Linux系统编程 day09 线程同步
Linux系统编程 day09 线程同步 1.互斥锁2.死锁3.读写锁4.条件变量(生产者消费者模型)5.信号量 1.互斥锁 互斥锁是一种同步机制,用于控制多个线程对共享资源的访问,确保在同一时间只有一个线程可以访问特定的资源或执行特定的操作…...

Vue快速入门(四)——Vue3及组合式API(一)
文章目录 一、认识Vue31. Vue2 选项式 API vs Vue3 组合式API2. Vue3的优势二、使用create-vue搭建Vue3项目1、认识create-vue2. 使用create-vue创建项目三、组合式API - setup选项1. setup选项的写法和执行时机2. setup中写代码的特点3. `<script setup>`语法糖4.小结四…...

vue项目名修改、webstorm和idea创建的项目重命名、重构项目、修改项目名称
一、需求 就是创建了一个项目,后期需要重命名,怎么办?----> 直接修改?肯定不行,因为里面有些配置也需要修改,假如你只改文件夹名称的话,里面配置都没修改,后期可能会出问题。 二…...

【MySQL】数据库约束和多表查询
目录 1.前言 2.数据库约束 2.1约束类型 2.2 NULL约束 2.3 NUIQUE:唯一约束 2.4 DEFAULT:默认值约束 2.5 PRIMARY KEY:主键约束 2.6 FOREIGN KEY:外键约束 1.7 CHECK约束 3.表的设计 3.1一对一 3.2一对多 3.3多对多 …...

抖店飞鸽客服自动回复软件开发教程与下载体验(.NET版)
转载请注明出处! 原文链接:https://blog.csdn.net/zgyulongfei/article/details/140960430 本文适合的读者为: 抖店(抖音小店)个体商家;抖店店群商家(店群商家:指的是开了几十个抖…...
如何关闭redis的自动清理缓存,声明式事务(含有redis)如何解决,redis setnx锁的使用。
20240809 一、解决redis数据被删除的方案1、发现问题2、解决注意!! 二、声明式事务(当有redis的时候)1. 先看代码2. Transactional(rollbackFor Exception.class)3. 如何解决redis在事务里面,如何保证原子性和一致性3…...
C#中抽象类的使用
前言 我们在C#中使用抽象类可以发挥C#多态的功能,把具有共性的方法定义在抽象类中,然后在不同的类中去实现,可增强代码的可读性、扩展性。 1、不使用抽象类 我们定义了下面两个类XiaoWang、XiaoMing,他们有一个Country方法&…...
揭秘网络攻击:深入理解JavaScript中的跨站点请求伪造(CSRF)
标题:揭秘网络攻击:深入理解JavaScript中的跨站点请求伪造(CSRF) 在当今数字化时代,网络安全已成为每个开发者和用户必须关注的重点。其中,跨站点请求伪造(CSRF)是一种常见的网络攻…...

【项目实战】C++视频共享点播系统
目录 一、项目介绍 1.1 对视频共享点播系统的认识 1.2服务端程序负责功能 1.3 服务端功能模块划分 1.4 项目界面演示 1.5预备知识 二.环境搭建 2.1 安装 Jsoncpp 库 2.1.1 使用jsoncpp 2.2 引入httplib库 2.2.1 安装Git(如果你的系统尚未安装Git…...

Android逆向题解 攻防世界难度4- Android2.0
Jeb打开apk 关键代码在Native函数getResult IDA 打开 so 发现代码比较简单,可以直接静态分析。 输出字符串也就是flag 长度是15,然后分成三段,第一段是可以整除3,第二段是除3取余1,第三段是除3取余等于2࿱…...

P4155 [SCOI2015] 计划
[SCOI2015] 计划 - 洛谷 核心思路 注意到, 可推出, 表示 战士 走 步到达战士位置。 若可以走到且 r < 终点 则答案 然后再加上自己这个哨兵,和走回自己的一个哨兵即可。 AC 代码 #include<bits/stdc.h> using namespace std…...
今日(2024年8月12日)科技新闻
国内: 航空航天领域 我国成功发射卫星互联网高轨卫星。我国试验性冰川保护项目取得积极成效,被形容为“为冰川盖棉被”。2024西太平洋国际航次科考队起航,开启探秘深海海山之旅。我国首架固定翼海上专业搜救航空器正式列编。“祥云”as700载…...
CP AUTOSAR标准之ECUStateManager(AUTOSAR_SWS_ECUStateManager)(更新中……)
1 简介和功能概述 ECU管理器模块(如本文档中所述)是一个基本软件模块(参见[1]),用于管理ECU状态的常见方面。具体来说,ECU管理器模块: 初始化和取消初始化OS、SchM和BswM以及一些基本软件驱动模块。根据请求配置ECU进入休眠和关机状态。管理ECU上的所有唤醒事件ECU管理器模块…...
Java中的中介者模式:解耦复杂系统的有效策略
Java中的中介者模式:解耦复杂系统的有效策略 在软件开发中,随着系统规模的扩大和复杂度的增加,各组件之间的直接交互会导致代码的耦合性增高,从而影响系统的可维护性和可扩展性。为了应对这种复杂性,中介者模式&#…...

transformer(李宏毅老师系列)
自学参考: Transformer:Attention Is All You Need Transformer论文逐段精读 视频课 课件资料 笔记 一、引入 seq2seq:输入一个序列的向量作为input,output的长度由机器自己决定seq2seq model应用: 语音辨识 输入是声音讯号的一串vector 输出…...

XCode15.4真机运行调试
更新Xcode后,没有模拟器内容,而且真机也不显示,编译按钮无法点击,设备在管理运行目标中可见,但无法选中解决方案:下载iOS17.5模拟器,但最坑的是直接点击“Get”下载总是中断,且无法断…...

Google Mock 和 Google Test编写单元测试入门(环境配置、简单执行)
文章目录 环境的配置方法1:从源代码构建第一步:克隆库的源代码第二步:构建库 方法 2:使用 CMake 的 FetchContent示例 CMakeLists.txt 项目的创建项目结构CMakeLists.txt (根目录)main.cpp (示例程序)tests/CMakeLists.txt (测试部…...

shell外壳与Linux权限
🌈个人主页:Yui_ 🌈Linux专栏:Linux 🌈C语言笔记专栏:C语言笔记 🌈数据结构专栏:数据结构 文章目录 1.shell命令以及运行原理2. Linux权限的概念3.Linux权限管理3.1 文件访问者的分类…...
越混越好的项目经理做对了哪些事?现在知道还不晚
作为一名项目经理,你最害怕的是什么? 是做不完的项目?延迟的进度条?还是团队人心涣散? 很多人都知道,得人心者得天下,一个成功的领导者,一定是能做到让人心服口服的。如果失去了团…...

haproxy是什么?以及haproxy基础实验
目录 一、什么是负载均衡? 二、为什么要用haproxy? 三、haproxy的基本部署实验: 3.1 基本配置实验 环境准备: 详细步骤: 3.2 haproxy-多进程与多线程实验: 多进程: 多线程:…...
在鸿蒙HarmonyOS 5中实现抖音风格的点赞功能
下面我将详细介绍如何使用HarmonyOS SDK在HarmonyOS 5中实现类似抖音的点赞功能,包括动画效果、数据同步和交互优化。 1. 基础点赞功能实现 1.1 创建数据模型 // VideoModel.ets export class VideoModel {id: string "";title: string ""…...
蓝桥杯 2024 15届国赛 A组 儿童节快乐
P10576 [蓝桥杯 2024 国 A] 儿童节快乐 题目描述 五彩斑斓的气球在蓝天下悠然飘荡,轻快的音乐在耳边持续回荡,小朋友们手牵着手一同畅快欢笑。在这样一片安乐祥和的氛围下,六一来了。 今天是六一儿童节,小蓝老师为了让大家在节…...
c++ 面试题(1)-----深度优先搜索(DFS)实现
操作系统:ubuntu22.04 IDE:Visual Studio Code 编程语言:C11 题目描述 地上有一个 m 行 n 列的方格,从坐标 [0,0] 起始。一个机器人可以从某一格移动到上下左右四个格子,但不能进入行坐标和列坐标的数位之和大于 k 的格子。 例…...

【SQL学习笔记1】增删改查+多表连接全解析(内附SQL免费在线练习工具)
可以使用Sqliteviz这个网站免费编写sql语句,它能够让用户直接在浏览器内练习SQL的语法,不需要安装任何软件。 链接如下: sqliteviz 注意: 在转写SQL语法时,关键字之间有一个特定的顺序,这个顺序会影响到…...

让AI看见世界:MCP协议与服务器的工作原理
让AI看见世界:MCP协议与服务器的工作原理 MCP(Model Context Protocol)是一种创新的通信协议,旨在让大型语言模型能够安全、高效地与外部资源进行交互。在AI技术快速发展的今天,MCP正成为连接AI与现实世界的重要桥梁。…...

2025季度云服务器排行榜
在全球云服务器市场,各厂商的排名和地位并非一成不变,而是由其独特的优势、战略布局和市场适应性共同决定的。以下是根据2025年市场趋势,对主要云服务器厂商在排行榜中占据重要位置的原因和优势进行深度分析: 一、全球“三巨头”…...
【前端异常】JavaScript错误处理:分析 Uncaught (in promise) error
在前端开发中,JavaScript 异常是不可避免的。随着现代前端应用越来越多地使用异步操作(如 Promise、async/await 等),开发者常常会遇到 Uncaught (in promise) error 错误。这个错误是由于未正确处理 Promise 的拒绝(r…...
文件上传漏洞防御全攻略
要全面防范文件上传漏洞,需构建多层防御体系,结合技术验证、存储隔离与权限控制: 🔒 一、基础防护层 前端校验(仅辅助) 通过JavaScript限制文件后缀名(白名单)和大小,提…...
window 显示驱动开发-如何查询视频处理功能(三)
D3DDDICAPS_GETPROCAMPRANGE请求类型 UMD 返回指向 DXVADDI_VALUERANGE 结构的指针,该结构包含特定视频流上特定 ProcAmp 控件属性允许的值范围。 Direct3D 运行时在D3DDDIARG_GETCAPS的 pInfo 成员指向的变量中为特定视频流的 ProcAmp 控件属性指定DXVADDI_QUER…...
SpringCloud优势
目录 完善的微服务支持 高可用性和容错性 灵活的配置管理 强大的服务网关 分布式追踪能力 丰富的社区生态 易于与其他技术栈集成 完善的微服务支持 Spring Cloud 提供了一整套工具和组件来支持微服务架构的开发,包括服务注册与发现、负载均衡、断路器、配置管理等功能…...