当前位置: 首页 > news >正文

C/C++语法|pthread线程库的使用

笔记主要内容来自

爱编程的大柄–线程
爱编程的大柄–线程同步

在进入代码实践之前,我们应该搞清楚。

线程是成语的最小执行单位,进程是操作系统中最小的资源分配单位。

这样的话我们可以理解以下两点:

  • 同一地址空间中的多个线程独有的是:每个线程都有属于自己的栈区和寄存器(内核中管理的),寄存器主要记录的就是上下文
  • 共享的是:.text、.rodata、.data、.heap、.bss、文件描述符

关于线程个数的确定:

  1. 文件IO操作:文件IO对CPU是使用率不高, 因此可以分时复用CPU时间片, 线程的个数 = 2 * CPU核心数 (效率最高)
  2. 处理复杂的算法(主要是CPU进行运算, 压力大),线程的个数 = CPU的核心数 (效率最高)

文章目录

  • 1.线程创建
    • 代码练习
  • 2.线程退出
    • 主线程调用退出函数
    • 子线程调用退出函数
  • 3.线程回收
    • 使用主线程栈
    • 使用子线程堆区
    • 使用全局变量
  • 4.线程分离
  • ⭐️5.线程同步(或者叫线程间通信?)
    • 互斥锁
    • 读写锁
    • ⭐️条件变量
    • ⭐️信号量
    • 信号量实现生产者、消费者模型
      • 总资源数为1
      • 总资源数大于1

1.线程创建

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

我们主要用到的就是第一个和第三个、第四个参数。

  • 第一个参数如果线程创建成功,线程ID写入到该指针指向的内存
    pthread_t itd1; pthread_create(&tid1, ...)
  • 第二个参数是线程属性,一般为NULL
  • 第三个参数是线程函数,创建出的子线程的处理动作,也就是该函数在子线程中执行
  • 第四个参数作为实参传递到 start_routine指针指向的函数内部。可以传入一个函数指针等等作为线程的回调函数。

代码练习

#include <iostream>
#include <pthread.h>#include <unistd.h>void* working(void* arg) {std::cout << "子线程" << pthread_self() << std::endl;for (int i = 0; i < 3; i++) {std::cout << "chiled say: " << i << std::endl;}
}int main () {pthread_t tid;pthread_create(&tid, NULL, working, NULL);sleep(1); //为啥这里一定要睡一会儿?std::cout << "parent say:" << tid << std::endl;return 0;
}
//输出:
子线程140470444414528
chiled say: 0
chiled say: 1
chiled say: 2
parent say:140470444414528

为什么主线程要sleep(1)呢?
因为主线程和子线程都是在抢CPU时间片,谁抢到谁干活,所以完全有可能子线程还没有抢到资源,主线程结束,那么整个进程就结束了,子线程根本就来不及干活。

我们这里也可以使用信号量,等子线程执行结束了,通知主线程,这里就涉及到线程间通信,后面会进行详细讲解。

2.线程退出

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

参数表示线程退出的时候携带的数据,当前子线程的主线程会得到该数据。如果不需要使用,指定为NULL(这是重点,因为我们C++中的没有这个功能)

主线程可以调用退出函数退出,但是地址空间不会被释放。
子线程调用退出函数退出,一般目的是带出一些有价值的数据。

主线程调用退出函数

#include <pthread.h>
#include <stdio.h>
#include <unistd.h>void* child_thread(void* arg) {sleep(1);printf("Child thread is running.\n");// 子线程执行一些工作pthread_exit(NULL); // 正常退出子线程
}int main() {pthread_t tid;// 创建子线程if (pthread_create(&tid, NULL, child_thread, NULL) != 0) {perror("Failed to create thread");return 1;}// 主线程立即退出,子线程继续运行printf("Main thread is exiting.\n");pthread_exit(NULL);return 0; // 这行代码不会执行,因为主线程已经退出
}

在这里我们可以发现主线程在创建子线程后立即退出,而子线程在继续执行。
但是我们一般不会这样调用函数,因为一般认为主线程的退出就代表程序执行结束。

要注意的是:
即使主线程通过调用 pthread_exit 退出,子线程也不会变成新的主线程。在 POSIX 线程(pthread)模型中,当主线程退出时,它创建的所有子线程仍然继续执行,直到它们自己结束或被其他线程终止。

子线程调用退出函数

如果子线程退出想往外面传递什么参数,也是配合pthread_join()一起使用,它的作用是等待子线程结束,并且获取返回状态:

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>void* child_thread(void* arg) {int* data = (int*)arg;printf("Child thread is processing data.\n");// 模拟计算*data = 42;pthread_exit(data); // 子线程结束,并返回数据指针
}int main() {pthread_t tid;int result;// 分配内存用于存储子线程的结果,该数据位于堆上int* data = (int*)malloc(sizeof(int));// 创建子线程pthread_create(&tid, NULL, child_thread, data);//主线程在干自己的任务,把修改data数据的任务交给了子线程// 等待子线程结束,并获取返回状态pthread_join(tid, (void**)&data);// 检查子线程的返回值if (data != NULL) {printf("Child thread returned: %d\n", *data);free(data);} else {printf("Child thread failed to return data.\n");free(data);}return 0;
}

3.线程回收

在刚才我们已经初步认识了线程回收函数:pthread_join(),这个函数是一个阻塞函数,如果还有子线程在运行,调用该函数就会阻塞,子线程退出函数解除阻塞进行资源的回收,函数被调用一次,只能回收一个子线程,如果有多个子线程则需要循环进行回收。

#include <pthread.h>
// 这是一个阻塞函数, 子线程在运行这个函数就阻塞
// 子线程退出, 函数解除阻塞, 回收对应的子线程资源, 类似于回收进程使用的函数 wait()
int pthread_join(pthread_t thread, void **retval);
pthread_join(tid, (void**)&data);
  • thread: 要被回收的子线程的线程ID
  • retval: 二级指针, 指向一级指针的地址, 这个地址中存储了pthread_exit() 传递出的数据,如果不需要这个参数,可以指定为NULL

现在我们来系统描述一下针对回收子线程数据的线程回收技术吧!

使用主线程栈

在上面子线程调用退出函数部分,我们就是使用的主线程栈上的数据,传递给子线程处理该数据,然后我们主线程在干自己的任务,把修改data数据的任务交给了子线程,最后阻塞在pthread_join()检查子线程活干的咋样。

使用子线程堆区

你觉得可以使用子线程栈区的数据然后回传吗?肯定是不行的,因为栈区数据在线程退出后会被销毁。子线程返回的指针将指向一个无效的内存地址,导致未定义行为。所以我们可以在子线程上堆区分配内存,然后把数据交给主线程:

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string>
#include <iostream>void* child_thread(void* arg) {std::string* str = new std::string("hello world"); // 在堆上分配内存pthread_exit((void*)str); // 返回指向堆上字符串的指针
}int main() {pthread_t tid;// 创建子线程pthread_create(&tid, NULL, child_thread, NULL);void* ptr = nullptr;//主线程执行自己的业务逻辑,把写一个hello world字符串的任务交给子线程// 等待子线程结束,并获取返回状态pthread_join(tid, &ptr);// 将void*指针转换为std::string*指针,并打印字符串std::string* str_ptr = static_cast<std::string*>(ptr);std::cout << *str_ptr << std::endl;// 释放堆上分配的内存delete str_ptr;return 0;
}

使用全局变量

在文章开篇我们就说过,主线程和子线程是共享.text、.rodata、.data、.heap、.bss和文件描述符的。所以子线程操作全局变量,然后把修改好的值传回给主线程当然也是允许的,具体实验请读者自己设计一个吧

4.线程分离

之前我们说过 pthread_join() 是一个阻塞函数,只要子线程不退出主线程会被一直阻塞,但是主线程有自己的业务逻辑要去执行,那应该怎么办呢?

这就涉及到我们的线程分离函数pthread_detach()上场了。

调用这个函数之后指定的子线程就可以和主线程分离,当子线程退出的时候,其占用的内核资源就被系统的其他进程接管并回收了。线程分离之后在主线程中使用pthread_join()就回收不到子线程资源了。

其实也就是父子线程各干各的了:

#include <iostream>
#include <pthread.h>
#include <unistd.h>void* working(void *arg) {for (int i = 0; i < 10; i ++) {std::cout << "child say: "  << i << std::endl;}
}int main () {pthread_t tid;pthread_create(&tid, NULL, working, NULL);//子线程与主线程分离pthread_detach(tid);//主线程执行自己的逻辑for (int i = 100; i < 110; i++) {std::cout << "parent say: " << i << std::endl;}std::cout << "task done!!!" << std::endl;return 0;
}

线程分离技术一般用在什么情况下?

  1. 简单的后台任务
    当子线程执行的是一个简单的、短暂的后台任务,而主线程不需要等待该子线程完成,也不需要获取子线程的返回值时,线程分离技术可以很方便地使用。
  2. 长期运行的任务
    当子线程需要执行一个长期运行的任务,而主线程不需要等待它完成,这种情况下也可以使用线程分离。这样主线程可以继续执行其他任务,而不必被子线程的运行时间所阻碍。
  3. 不可预测的结束时间
    当子线程的结束时间不可预测,主线程不能在合理的时间内使用pthread_join等待子线程结束时,线程分离技术也很有用。这样可以避免主线程长时间等待,导致资源

⭐️5.线程同步(或者叫线程间通信?)

由于线程的运行顺序是由操作系统的调度算法决定的,谁也不知道哪个线程先执行哪个后执行,所以我们必须使用线程同步技术来管理相关的资源。

所谓的同步并不是多个线程同时对内存进行访问,而是按照先后顺序依次进行的。

每一个环节我都会给定一个题目,先给出实现代码,随后讲解相关的知识。

互斥锁

互斥锁就不赘述了,主要就是对于一个共享资源必须加锁,不然有可能出现资源错乱的问题。

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>// 定义一个互斥锁
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;// 共享数据
int shared_data = 0;// 线程函数
void* thread_function(void* arg) {// 锁定互斥锁pthread_mutex_lock(&mutex);// 对共享数据进行操作shared_data++;// 打印共享数据printf("Thread %ld - shared_data: %d\n", pthread_self(), shared_data);// 解锁互斥锁pthread_mutex_unlock(&mutex);return NULL;
}int main() {pthread_t tid1, tid2;// 创建两个线程pthread_create(&tid1, NULL, thread_function, NULL);pthread_create(&tid2, NULL, thread_function, NULL);// 等待线程结束pthread_join(tid1, NULL);pthread_join(tid2, NULL);// 销毁互斥锁pthread_mutex_destroy(&mutex);return 0;
}

它的用法也比较简单,首先想要使用互斥锁必须先完成初始化,
pthread_mutex_init()的第二个参数表示互斥锁属性,一般写NULL。

使用完之后记得销毁,销毁时传入的是互斥锁所在的地址,在调用的时候也是传入地址。

读写锁

读写锁允许多个线程同时获取读锁(只要没有线程持有写锁),但写锁是排他的,其他线程必须等待写锁释放后才能获取读锁或写锁。

示例代码如下:我们定义两个读线程,一个写线程。

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>// 定义一个读写锁
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;// 共享数据
int shared_data = 0;// 读取共享数据的线程函数
void* reader(void* arg) {(void)arg; // 未使用的参数// 读取锁pthread_rwlock_rdlock(&rwlock);printf("Reader: shared_data = %d\n", shared_data);// 释放读取锁pthread_rwlock_unlock(&rwlock);return NULL;
}// 写入共享数据的线程函数
void* writer(void* arg) {(void)arg; // 未使用的参数// 写入锁pthread_rwlock_wrlock(&rwlock);// 修改共享数据shared_data++;printf("Writer: updated shared_data to %d\n", shared_data);// 释放写入锁pthread_rwlock_unlock(&rwlock);return NULL;
}int main() {pthread_t r1, r2, w1;// 创建读者线程pthread_create(&r1, NULL, reader, NULL);// 创建另一个读者线程pthread_create(&r2, NULL, reader, NULL);// 等待读者线程完成pthread_join(r1, NULL);pthread_join(r2, NULL);// 创建写入者线程pthread_create(&w1, NULL, writer, NULL);// 等待写入者线程完成pthread_join(w1, NULL);// 销毁读写锁pthread_rwlock_destroy(&rwlock);return 0;
}

它的使用和互斥锁是一模一样的,值不过多了读取锁和写入锁的调用,释放锁都是一样的:

// 读取锁
pthread_rwlock_rdlock(&rwlock);
// 写入锁
pthread_rwlock_wrlock(&rwlock);
//释放读取锁或者写入锁
pthread_rwlock_unlock(&rwlock);

⭐️条件变量

学完条件变量,我们就可以实现所谓的“线程依次执行”。
整个使用方法如下:

#include <pthread.h>
//定义条件变量类型变量
pthread_cond_t cond;//初始化
//第一个传参&cond
//第二个参数为条件变量属性,一般使用默认属性,指定为NULL
int pthread_cond_init(pthread_cond_t *cond, NULL) 
//释放资源
int pthread_cond_destroy(pthread_cond_t *cond);//线程阻塞函数:它的工作流程如下
//1. 释放与条件变量cond关联的互斥锁mutex
//2. 之后,调用线程会被阻塞,并从运行状态中移除,进入等待条件变量的状态。
//3. 直到另一个线程执行了对应的 pthread_cond_signal 或 pthread_cond_broadcast 操作来唤醒它
//4. 被唤醒后重新获取互斥锁
//5.解除阻塞
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);//有超时时间的线程阻塞函数,时间到达之后,解除阻塞
int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);// 唤醒阻塞在条件变量上的线程, 至少有一个被解除阻塞
int pthread_cond_signal(pthread_cond_t *cond);
// 唤醒阻塞在条件变量上的线程, 被阻塞的线程全部解除阻塞
int pthread_cond_broadcast(pthread_cond_t *cond);

这里的案例就使用我们经典的生产者单消费者模型
这里有三个生产者、三个消费者,生产者只生产50个商品,如果当前生产者发现任务队列有超过10个商品,生产者休息,如果消费者消费完了,消费者阻塞,通知生产者生产,生产者生产

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>// 链表的节点
struct Node
{int number;struct Node* next;
};// 定义条件变量, 控制消费者线程
pthread_cond_t cond;
// 互斥锁变量
pthread_mutex_t mutex;
// 指向头结点的指针
struct Node * head = NULL;void* producer(void *arg) {while(1) {//模拟生产时间sleep(rand() % 3); pthread_mutex_lock(&mutex);Node* pnew = (struct Node*)malloc(sizeof(Node));pnew->number = rand() % 1000;pnew->next = head; head = pnew;printf("producer, number = %d, tid=%ld\n", pnew->number, pthread_self());pthread_mutex_unlock(&mutex);//生产了任务,通知消费者消费pthread_cond_broadcast(&cond);}return nullptr;
}void* consumer(void *arg) {while(1) {pthread_mutex_lock(&mutex);while(head == nullptr) {pthread_cond_wait(&cond, &mutex);}//消费过程Node* pnode = head;printf("consumer, number = %d, tid = %ld\n", pnode->number, pthread_self());head = pnode->next;free(pnode);pthread_mutex_unlock(&mutex);//模拟消费时间sleep(rand() % 3);}return nullptr;
}int main()
{pthread_cond_init(&cond, nullptr);pthread_mutex_init(&mutex, nullptr);//创建5个生产者,5个消费者pthread_t ptid[5];pthread_t ctid[5];//启动线程for (int i = 0; i < 5; i++) {pthread_create(&ptid[i], nullptr, producer, nullptr);}for (int i = 0; i < 5; i++) {pthread_create(&ptid[i], nullptr, consumer, nullptr);}//释放资源for (int i = 0; i < 5; i++) {pthread_join(ptid[i], nullptr);}for (int i = 0; i < 5; i++) {pthread_join(ctid[i], nullptr);}//销毁互斥锁和条件变量pthread_cond_destroy(&cond);pthread_mutex_destroy(&mutex);
}

⭐️信号量

信号量用在多线程多任务同步的,一个线程完成了某一个动作就通过信号量告诉别的线程,别的线程再进行某些动作。信号量不一定是锁定某一个资源,而是流程上的概念,比如:有A,B两个线程,B线程要等A线程完成某一任务以后再进行自己下面的步骤,这个任务并不一定是锁定某一资源,还可以是进行一些计算或者数据处理之类

强调!!!

信号量主要用来阻塞线程,不能保证线程安全,如果要保证线程安全,需要信号量和互斥锁一起使用!

如果五个线程同时被阻塞在sem_wait(&sem),有一个线程调用了sem_post(&sem),很可能多个线程同时解除阻塞!

#include <semaphore.h>
//定义变量
sem_t sem;//初始化
// pshared = 0 线程同步
// pshared 非 0 进程同步
// value:初始化当前信号量拥有的资源数(>=0),如果资源数为0,线程就会被阻塞了。
int sem_init(sem_t *sem, int pshared, unsighed int val);
//释放资源
int sem_destroy(sem_t *sem);//线程阻塞函数:如果资源数被耗尽,则函数阻塞
// 函数被调用, sem中的资源就会被消耗1个, 资源数-1
int sem_wait(sem_t *sem);//如果资源被耗尽,直接返回错误号,用于处理获取资源失败之后的情况
int sem_trywait(sem_t *sem);//超时阻塞:就算被阻塞了,超过某时间解除阻塞
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);//调用该函数给sem中的资源数+1
int sem_post(sem_t *sem);

这里给一个简单的使用案例:
该代码可以清晰查看sem_wait和sem_post的行为

#include <pthread.h>
#include <semaphore.h>
#include <stdio.h>
#include <unistd.h>#define MAXNUM 2
sem_t semPtr;
pthread_t a_thread, b_thread, c_thread;
int g_phreadNum = 1;void *func1(void *arg) {sem_wait(&semPtr);printf("a_thread get a semaphore \n");sleep(5);sem_post(&semPtr);printf("a_thread release semaphore \n");
}void *func2(void *arg) {sem_wait(&semPtr);printf("b_thread get a semaphore \n");sleep(5);sem_post(&semPtr);printf("b_thread release semaphore \n");
}void *func3(void *arg) {sem_wait(&semPtr);printf("c_thread get a semaphore \n");sleep(5);sem_post(&semPtr);printf("c_thread release semaphore \n");
}int main() {int taskNum;// 创建2个信号量sem_init(&semPtr, 0, MAXNUM);//线程1获取1个信号量,5秒后释放pthread_create(&a_thread, NULL, func1, NULL);//线程2获取1个信号量,5秒后释放pthread_create(&b_thread, NULL, func2, NULL);sleep(1);//线程3获取信号量,只有线程1或者线程2释放后,才能获取到pthread_create(&c_thread, NULL, func3, NULL);sleep(10);//销毁信号量sem_destroy(&semPtr);return 0;
}
  1. 互斥锁:防止多个线程同时访问某个特定的资源或代码段。
  2. 同步:协调多个线程的执行顺序,确保它们按正确的顺序执行。
  3. 限制资源的并发访问数量:控制同时访问某些资源(如数据库连接、文件句柄等)的线程数量。
  4. 线程池管理:管理线程池中的线程数量,以及任务队列中的待处理任务数量。

信号量实现生产者、消费者模型

场景描述:使用信号量实现生产者和消费者模型,生产者有5个,往链表头部添加节点,消费者也有5个,删除链表头部的节点。

总资源数为1

如果生产者和消费者使用的信号量总资源数为1,那么不会出现生产者线程和消费者线程同时访问共享资源的情况,不管生产者和消费者线程有多少个,它们都是顺序执行的。

主要执行的逻辑就是,定义生产者信号量和消费者信号量两个信号量,他们一共只持有1个资源。在生产者生产完之后,给消费者增加一个资源,消费者消费完了给生产者增加一个资源

所以本节完全可以不使用互斥锁

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <semaphore.h>
#include <pthread.h>// 链表的节点
struct Node
{int number;struct Node* next;
};// 生产者线程信号量
sem_t psem;
// 消费者线程信号量
sem_t csem;// 指向头结点的指针
struct Node * head = NULL;// 生产者的回调函数
void* producer(void* arg)
{// 一直生产while(1){// 生产者拿一个信号量sem_wait(&psem);//生产过程struct Node* pnew = (struct Node*)malloc(sizeof(struct Node));pnew->number = rand() % 1000;pnew->next = head;head = pnew;printf("+++producer, number = %d, tid = %ld\n", pnew->number, pthread_self());// 通知消费者消费, 给消费者加一个信号量sem_post(&csem);// 生产慢一点sleep(rand() % 3);}return NULL;
}// 消费者的回调函数
void* consumer(void* arg)
{while(1){sem_wait(&csem);// 取出链表的头结点, 将其删除struct Node* pnode = head;printf("--consumer: number: %d, tid = %ld\n", pnode->number, pthread_self());head  = pnode->next;free(pnode);// 通知生产者生成, 给生产者加信号灯sem_post(&psem);sleep(rand() % 3);}return NULL;
}int main()
{// 初始化信号量// 生产者和消费者拥有的信号灯的总和为1sem_init(&psem, 0, 1);  // 生产者线程一共有1个信号灯sem_init(&csem, 0, 0);  // 消费者线程一共有0个信号灯// 创建5个生产者, 5个消费者pthread_t ptid[5];pthread_t ctid[5];for(int i=0; i<5; ++i){pthread_create(&ptid[i], NULL, producer, NULL);}for(int i=0; i<5; ++i){pthread_create(&ctid[i], NULL, consumer, NULL);}// 释放资源for(int i=0; i<5; ++i){pthread_join(ptid[i], NULL);}for(int i=0; i<5; ++i){pthread_join(ctid[i], NULL);}sem_destroy(&psem);sem_destroy(&csem);return 0;
}

该代码有一个很大的问题,就是可能出现连续多个生产者生产,这是不应该发生的。这是为什么呢?百思不得其解。

总资源数大于1

如果生产者和消费者线程使用的信号量对应的总资源数为大于1,这种场景下出现的情况就比较多了:

  • 多个生产者线程同时生产
  • 多个消费者同时消费
  • 生产者线程和消费者线程同时生产和消费

所以说这个时候就会产生数据竞争了

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <semaphore.h>
#include <pthread.h>// 链表的节点
struct Node
{int number;struct Node* next;
};// 生产者线程信号量
sem_t psem;
// 消费者线程信号量
sem_t csem;// 互斥锁变量
pthread_mutex_t mutex;
// 指向头结点的指针
struct Node * head = NULL;// 生产者的回调函数
void* producer(void* arg)
{// 一直生产while(1){// 生产者拿一个信号灯sem_wait(&psem);// 加锁, 这句代码放到 sem_wait()上边, 有可能会造成死锁pthread_mutex_lock(&mutex);// 创建一个链表的新节点struct Node* pnew = (struct Node*)malloc(sizeof(struct Node));// 节点初始化pnew->number = rand() % 1000;// 节点的连接, 添加到链表的头部, 新节点就新的头结点pnew->next = head;// head指针前移head = pnew;printf("+++producer, number = %d, tid = %ld\n", pnew->number, pthread_self());pthread_mutex_unlock(&mutex);// 通知消费者消费sem_post(&csem);// 生产慢一点sleep(rand() % 3);}return NULL;
}// 消费者的回调函数
void* consumer(void* arg)
{while(1){sem_wait(&csem);pthread_mutex_lock(&mutex);struct Node* pnode = head;printf("--consumer: number: %d, tid = %ld\n", pnode->number, pthread_self());head  = pnode->next;// 取出链表的头结点, 将其删除free(pnode);pthread_mutex_unlock(&mutex);// 通知生产者生成, 给生产者加信号灯sem_post(&psem);sleep(rand() % 3);}return NULL;
}int main()
{// 初始化信号量sem_init(&psem, 0, 5);  // 生成者线程一共有5个信号灯sem_init(&csem, 0, 0);  // 消费者线程一共有0个信号灯// 初始化互斥锁pthread_mutex_init(&mutex, NULL);// 创建5个生产者, 5个消费者pthread_t ptid[5];pthread_t ctid[5];for(int i=0; i<5; ++i){pthread_create(&ptid[i], NULL, producer, NULL);}for(int i=0; i<5; ++i){pthread_create(&ctid[i], NULL, consumer, NULL);}// 释放资源for(int i=0; i<5; ++i){pthread_join(ptid[i], NULL);}for(int i=0; i<5; ++i){pthread_join(ctid[i], NULL);}sem_destroy(&psem);sem_destroy(&csem);pthread_mutex_destroy(&mutex);return 0;
}

相关文章:

C/C++语法|pthread线程库的使用

笔记主要内容来自 爱编程的大柄–线程 爱编程的大柄–线程同步 在进入代码实践之前&#xff0c;我们应该搞清楚。 线程是成语的最小执行单位&#xff0c;进程是操作系统中最小的资源分配单位。 这样的话我们可以理解以下两点&#xff1a; 同一地址空间中的多个线程独有的是&…...

四川汇聚荣聚荣科技有限公司是正规的吗?

在当今社会&#xff0c;随着科技的飞速发展&#xff0c;越来越多的科技公司如雨后春笋般涌现。然而&#xff0c;在这个信息爆炸的时代&#xff0c;如何判断一家公司是否正规成为了许多人关注的焦点。本文将围绕“四川汇聚荣聚荣科技有限公司是否正规”这一问题展开讨论&#xf…...

tomcat学习--部署java项目

主流开发项目&#xff0c;springboot框架下&#xff0c;jar部署java传统的tomcat发布war包 一 什么是tomcat&#xff1f; 是一个用于运行java程序的软件&#xff0c;发布的时候&#xff1a;开发将源码使用maven打包&#xff0c;生产war包 二 安装tomcat tomcat是java写的&a…...

用 vue3 + phaser 实现经典小游戏:飞机大战

本文字数&#xff1a;7539字 预计阅读时间&#xff1a;30分钟 01 前言 说起小游戏&#xff0c;最经典的莫过于飞机大战了&#xff0c;相信很多同学都玩过。今天我们也来试试开发个有趣的小游戏吧&#xff01;我们将从零开始&#xff0c;看看怎样一步步实现一个H5版的飞机大战&a…...

【Linux|数据恢复】extundelete和ext4magic数据恢复工具使用

环境&#xff1a;Centos7.6_x86 一、extundelete工具 1、extundelete介绍 Extundelete 是一个数据恢复工具&#xff0c;用于从 ext3 或 ext4 分区中恢复删除文件。根据官网0.2.4版本介绍是支持ext4&#xff0c;但实际上使用发现ext4格式不行&#xff0c;会报以下错误&#xf…...

用户接入和认证技术

一、用户接入和认证配置 称为网络接入控制&#xff0c;通过对接入网络的客NAC (Network Admission Control)户端和用户的认证保证网络的安全&#xff0c;是一种“端到端”的安全技术。包括802.1x认证、MAC认证与Portal认证。 二、三种认证方式简介 1、Portal认证 Portal认证通…...

【面试】Java虚拟机的生命周期

目录 1. 说明2. 启动&#xff08;Initialization&#xff09;3. 运行&#xff08;Running&#xff09;4. 服务&#xff08;Servicing&#xff09;5. 终止&#xff08;Termination&#xff09; 1. 说明 1.Java虚拟机&#xff08;JVM&#xff09;的生命周期通常指的是JVM实例从启…...

Nginx高可用性架构:实现负载均衡与故障转移的探索

随着网络应用的不断发展和用户访问量的增长&#xff0c;如何确保系统的高可用性、实现负载均衡以及快速响应故障转移成为了每个运维和开发团队必须面对的挑战。Nginx作为一款高性能的HTTP和反向代理服务器&#xff0c;凭借其强大的功能和灵活的配置&#xff0c;成为了实现这些目…...

计算机网络-运输层

运输层 网络层在两个端系统之间的交付服务拓展到运行在两个不同端系统上的应用层进程之间的交付服务。 概述和运输层服务 运输层协议为运行在不同主机上的引用进程之间提供了逻辑通信功能。通过逻辑通信&#xff0c;运行在不同进程之间的主机好像直接连接一样。 运输层协议…...

网络通信(一)

网络编程 1.网络编程概念及相关名词 &#xff1a; 网络编程是计算机科学中一个重要的领域&#xff0c;它涉及到在不同计算机之间通过计算机网络进行通信和数据交换的程序设计。网络编程的核心是实现网络通信协议&#xff0c;这些协议定义了数据如何在网络上发送、接收和解释。…...

Linux环境中部署docker私有仓库Registry与远程访问详细流程

目录 前言 1. 部署Docker Registry 2. 本地测试推送镜像 3. Linux 安装cpolar 4. 配置Docker Registry公网访问地址 5. 公网远程推送Docker Registry 6. 固定Docker Registry公网地址 前言 作者简介&#xff1a; 懒大王敲代码&#xff0c;计算机专业应届生 今天给大家聊…...

springboot项目使用validated参数校验框架

目录 前言 一、validated是什么&#xff1f; 二、使用步骤 1.引入maven依赖 2.使用实现 总结 前言 当谈到Spring的参数校验功能时&#xff0c;Validated注解无疑是一个重要的利器。它为我们提供了一种简单而又强大的方式来验证请求参数的合法性&#xff0c;保证了系统的稳…...

Azure Chatgpt demo部署——本地CentOS Docker

参见上一篇 http://t.csdnimg.cn/JcyfM 由于本地部署环境&#xff0c;与之前系统、网络、配置等环境不同&#xff0c;可能会遇见一些新的问题。 取2023年8月27日代码 git checkout -b a02796b063381c10ca9ca8189590b289a4d09129 由于本地情况的网络等环境不太一样&#xff0c…...

MybatisPlus中自定义sql

背景 在开发过程中&#xff0c;可能会出现除了where条件&#xff0c;其它sql比较复杂&#xff0c;这时候就需要用到自定义sql了。 问题 如&#xff1a;用户状态为正常的数据年龄加一&#xff08;所有用户年龄加一&#xff09; 数据库中sql&#xff1a; UPDATE USER SET…...

HCIA--DHCP: 动态主机配置协议 (复习)

DHCP: 动态主机配置协议 -- 同一分发管理ip地址 基于UDP 67/68端口工作 网络中存在DHCP的服务器为需要自动生成ip地址的设备分配ip地址&#xff1b;--C/S模型 成为DHCP服务器的条件&#xff1a; 该设备存在接口或网卡连接到所要分发ip地址的广播域内该接口或网卡必须已经配置…...

MySQL select for update 加锁

背景 当多人操作同一个客户下账号的时候&#xff0c;希望顺序执行&#xff0c;某个时刻只有一个人在操作&#xff1b;当然可以通过引入redis这种中间件实现&#xff0c;但考虑到并发不会很多&#xff0c;所以不想再引入别的中间件。 表结构 create table jiankunking_accoun…...

MongoDB CRUD操作:投影Project详解

MongoDB CRUD操作&#xff1a;投影Project详解 文章目录 MongoDB CRUD操作&#xff1a;投影Project详解返回文档的全部字段返回指定的字段和_id字段不输出_id字段指定排除的字段返回内嵌文档中的指定字段禁止内嵌文档中的特定字段数组中内嵌文档的投影聚合表达式的投影字段 默认…...

redis 集群 底层原理以及实操

前言 上篇我们讲解了哨兵集群是怎么回事 也说了对应的leader选举raft算法 也说了对应的slave节点是怎么被leader提拔的 主要是比较优先级 比较同步偏移量 比较runid等等 今天我们再说说,其实哨兵也有很多缺点 虽然在master挂了之后能很快帮我们选举出新的master 但是对于单个ma…...

MVC架构中的servlet层重定向404小坑

servlet层中的UserLoginServlet.java package com.mhys.servlet; /*** ClassName: ${NAME}* Description:** Author 数开_11* Create 2024-05-29 20:32* Version 1.0*/import com.mhys.pojo.User; import com.mhys.service.UserService; import com.mhys.service.impl.UserSer…...

Java-RabbitMQ

RabbitMQ使用场景 1、跨系统异步通信 2、多应用之间解耦 3、应用内流程同步变异步 4、整体架构即采用消息驱动 5、应用内部解耦 RabbitMQ内部角色 角色简介生产者消息创建者消费者消息接收者代理RabbitMQ本身&#xff0c;用于存储转发消息&#xff0c;快递功能 RabbitMQ有哪…...

ABAP 在增强中COMMIT

前言 呃&#xff0c;又是很磨人的需求&#xff0c;正常情况下是不允许在增强中COMMIT的&#xff0c;会影响源程序本身的逻辑&#xff0c;但是这个需求就得这么干… 就是在交货单增强里面要再调用一次交货单BAPI&#xff0c;通过SO的交货单自动创建STO的交货单&#xff0c;如果…...

【UML用户指南】-02-UML的14种图

1、结构图 1、类图&#xff08;class diagram&#xff09; 展现了一组类、接口、协作和它们之间的关系。 在面向对象系统的建模中所建立的最常见的图就是类图。类图给出系统的静态设计视图。 包含主动类的类图给出系统的静态进程视图。构件图是类图的变体。 2、对象图&a…...

Linux驱动开发笔记(二) 基于字符设备驱动的I/O操作

文章目录 前言一、设备驱动的作用与本质1. 驱动的作用2. 有无操作系统的区别 二、内存管理单元MMU三、相关函数1. ioremap( )2. iounmap( )3. class_create( )4. class_destroy( ) 四、GPIO的基本知识1. GPIO的寄存器进行读写操作流程2. 引脚复用2. 定义GPIO寄存器物理地址 五、…...

三品软件:打造高效安全的图文档管理体系

在数字化转型的浪潮中&#xff0c;工程设计单位和企业设计部门面临着电子图文档管理的巨大挑战。随着电子图纸和文档数量的激增&#xff0c;如何有效组织、管理和共享这些资源&#xff0c;成为提升工作效率和保障信息安全的关键。本文将探讨当前图文档管理面临的问题&#xff0…...

N1 one-hot编码

&#x1f368; 本文为&#x1f517;365天深度学习训练营 中的学习记录博客&#x1f356; 原作者&#xff1a;K同学啊# 前言 前言 onehot编码在机器学习比较常见&#xff0c;例如推荐系统中类别变量的处理等。 onehot 编码简介 One-hot编码&#xff08;one-hot encoding&…...

数据库基础+增删查改初阶

数据库基础增删查改初阶 一。数据库操作 1.概念&#xff1a; 一个mysql服务器上有很多的表&#xff0c;把有关系的表放在一起就构成了一个数据集合&#xff0c;此时称为“数据库”&#xff0c;一个mysql1服务器上可以有多个这样的数据库 2.创建数据库&#xff1a; create …...

大模型日报2024-05-29

大模型日报 2024-05-29 大模型资讯 大型语言模型在金融预测中将超越人类分析师 摘要: 新研究表明&#xff0c;大型语言模型如ChatGPT在金融预测方面表现优于人类专家&#xff0c;为交易策略提供了宝贵的见解。这意味着未来这些模型将在金融领域发挥更重要的作用&#xff0c;提升…...

如何摆脱打工人任人宰割的命运

那就是为自己打工。 要有自己的思想&#xff0c;自己的目标&#xff0c;有自己的方向&#xff0c;坚决的非常自信的去执行它。 这样才是活出属于自己的人生&#xff0c;活出自己的精彩。 当然&#xff0c;这是在你已经比周围人优秀的情况下&#xff0c;至少是你觉得你比他们…...

“图片在哪”、“我是temunx”、“变成思维导图用xmindparser”gpt给出文本变字典

需求 我的意思是什么 分类清单“图片在哪 我是temunx变成思维导图 用xmindparser用 shell 画思维导图 x mind&#xff0c;可以 /storage/emulated/0/字体/黑体.ttf 保存/storage/emulated/0/print/图片/input图纸/完整代码 给个文本内容”任务清单 调整语言顺序文不对题的…...

【LeetCode】【5】最长回文子串

文章目录 [toc]题目描述样例输入输出与解释样例1样例2 提示Python实现动态规划 个人主页&#xff1a;丷从心 系列专栏&#xff1a;LeetCode 刷题指南&#xff1a;LeetCode刷题指南 题目描述 给一个字符串s&#xff0c;找到s中最长的回文子串 样例输入输出与解释 样例1 输入…...