【C++高并发服务器WebServer】-9:多线程开发

devtools/2025/2/3 12:47:04/

在这里插入图片描述

本文目录

  • 一、线程概述
    • 1.1 线程和进程的区别
    • 1.2 线程之间共享和非共享资源
    • 1.3 NPTL
  • 二、线程操作
    • 2.1 pthread_create
    • 2.2 pthread_exit
    • 2.3 pthread_join
    • 2.4 pthread_detach
    • 2.5 patch_cancel
    • 2.6 pthread_attr
  • 三、实战demo
  • 四、线程同步
  • 五、死
  • 六、读写
  • 七、生产消费者模型

一、线程概述

与进程(process)类似,线程(thread)是允许应用程序并发执行多个任务的一种机制。一个进程可以包含多个线程。同一个程序中的所有线程均会独立执行相同程序,且共享同一份全局内存区域,其中包括初始化数据段、未初始化数据段,以及堆内存段。(传统意义上的 UNIX 进程只是多线程程序的一个特例,该进程只包含一个线程)进程是 CPU 分配资源的最小单位,线程是操作系统调度执行的最小单位。

线程是轻量级的进程(LWP:LightweightProcess),在inux环境下线程的本质仍是进程。

查看指定进程的 LWP 号:ps-Lf pid

比如说当在服务器中打开一个火狐浏览器后,可以通过命令ps aux查看对应的进程号,然后通过ps -Lf <火狐进程号>查看浏览器启动之后的线程。

1.1 线程和进程的区别

进程间的信息难以共享,由于除去只读代码之外,父子进程并未共享内存,因此必须采用一些进程间的通信方式,才能在进程间进行信息交换。

调用fork()创建进程的代价比较高,就算利用写时复制的技术,任然需要复制内存页表文件描述符表之类的多种进程属性,也就是fork的开销其实还是不菲的。

线程之间能够方便、快速的共享信息,只需将数据复制到共享(全局或者堆)变量中即可。在虚拟地址空间的栈空间中,每个子线程都有自己的一块对应区域,.text代码段也是每个线程都有自己对应的。

创建线程 比进程通常快10倍不止,线程间是共享虚拟地址空间的,无需采用写时复制,无需复制页表。

1.2 线程之间共享和非共享资源

共享资源中,除了虚拟地址空间,其他的都是内核区的。

在这里插入图片描述

1.3 NPTL

当 Linux 最初开发时,在内核中并不能真正支持线程。但是它的确可以通过 clone() 系统调用将进程作为可调度的实体。这个调用创建了调用进程(caling process)的一个拷贝,这个拷贝与调用进程共享相同的地址空间。

LinuxThreads 项目使用这个调用来完成在用户空间模拟对线程的支持。不幸的是,这种方法有一些缺点,尤其是在信号处理、调度和进程间同步等方面都存在问题。另外,这个线程模型也不符合 POSIX的要求。

要改进 LinuxThreads,需要内核的支持,并且重写线程库。有两个相互竞争的项目开始来满足这些要求。一个包括 IBM 的开发人员的团队开展了 NGPT(Next-Generation POSIXThreads)项目。同时Red Hat 的一些开发人员开展了 NPTL 项目。NGPT 在 2003 年中期被放弃了,把这个领域完全留给了NPTL。

NPTL,或称为 Native POSlX Thread Library,是 Linux线程的一个新实现,它克服了 LinuxThreads的缺点,同时也符合 POSIX的需求。与 LinuxThreads 相比,它在性能和稳定性方面都提供了重大的改进。査看当前 pthread 库版本:
getconf GNU LIBPTHREAD VERSION

在这里插入图片描述

二、线程操作

pthread_t pthread_self(void);int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *
(*start_routine) (void *), void *arg);int pthread_equal(pthread_t t1, pthread_t t2);
void pthread_exit(void *retval);
int pthread_join(pthread_t thread, void **retval);
int pthread_detach(pthread_t thread);
int pthread_cancel(pthread_t thread);

2.1 pthread_create

#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, 
void *(*start_routine) (void *), void *arg);- 功能:创建一个子线程- 参数:- thread:传出参数,线程创建成功后,子线程的线程ID被写到该变量中。- attr : 设置线程的属性,一般使用默认值,NULL- start_routine : 函数指针,这个函数是子线程需要处理的逻辑代码- arg : 给第三个参数使用,传参- 返回值:成功:0失败:返回错误号。这个错误号和之前errno不太一样。获取错误号的信息:  char * strerror(int errnum);
#include <stdio.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>void * callback(void * arg) {printf("child thread...\n");printf("arg value: %d\n", *(int *)arg);return NULL;
}int main() {pthread_t tid;int num = 10;// 创建一个子线程  (void *)&num 是把num的地址转换为void *类型,然后传给子线程进行使用int ret = pthread_create(&tid, NULL, callback, (void *)&num);if(ret != 0) {char * errstr = strerror(ret);printf("error : %s\n", errstr);} for(int i = 0; i < 5; i++) {printf("%d\n", i);}sleep(1); //防止主线程过快结束,导致子线程没有运行return 0;   // exit(0);
}

如果直接用命令编译: gcc pthread_create.c -o phread, 会显示如下报错:

在这里插入图片描述

需要在命令的末尾加上-pthread才能运行(可以查看man文档)
也就是gcc pthread_create.c -o pthread -pthread等价于gcc thread_create.c -o pthread -lpthread(通过-lib指定pthread这个库。)

2.2 pthread_exit

#include <pthread.h>
void pthread_exit(void *retval);功能:终止一个线程,在哪个线程中调用,就表示终止哪个线程参数:retval:需要传递一个指针,作为一个返回值,可以在pthread_join()中获取到。pthread_t pthread_self(void);功能:获取当前的线程的线程IDint pthread_equal(pthread_t t1, pthread_t t2);功能:比较两个线程ID是否相等不同的操作系统,pthread_t类型的实现不一样,有的是无符号的长整型,有的是使用结构体去实现的。
#include <stdio.h>
#include <pthread.h>
#include <string.h>void * callback(void * arg) {printf("child thread id : %ld\n", pthread_self());return NULL;    // return NULL 等价于pthread_exit(NULL);
} int main() {// 创建一个子线程pthread_t tid;int ret = pthread_create(&tid, NULL, callback, NULL);if(ret != 0) {char * errstr = strerror(ret);printf("error : %s\n", errstr);}// 主线程for(int i = 0; i < 5; i++) {printf("%d\n", i);}printf("tid : %ld, main thread id : %ld\n", tid ,pthread_self());// 让主线程退出,当主线程退出时,不会影响其他正常运行的线程。pthread_exit(NULL);printf("main thread exit\n");return 0;   // exit(0);
}

2.3 pthread_join

#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);- 功能:和一个已经终止的线程进行连接回收子线程的资源这个函数是阻塞函数,调用一次只能回收一个子线程一般在主线程中使用- 参数:- thread:需要回收的子线程的ID- retval: 接收子线程退出时的返回值- 返回值:0 : 成功非0 : 失败,返回的错误号

如果运行的value是全局变量,那么能够顺利回收资源。

但是如果采用的是下面代码的pthread_exit((void *)&value); 这一行是问题的关键。value 是一个局部变量,它存储在子线程的栈空间中。当子线程执行到 pthread_exit 时,value 的作用域已经结束,子线程的栈空间可能会被回收或覆盖。因此,pthread_exit 返回的指针指向了一个已经被销毁的局部变量的地址,这导致了未定义行为。所以回收了打印的value是乱值。

#include <stdio.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>//int value = 10;void * callback(void * arg) {printf("child thread id : %ld\n", pthread_self());sleep(3);// return NULL; int value = 10; // 局部变量pthread_exit((void *)&value);   // return (void *)&value;
} int main() {// 创建一个子线程pthread_t tid;int ret = pthread_create(&tid, NULL, callback, NULL);if(ret != 0) {char * errstr = strerror(ret);printf("error : %s\n", errstr);}// 主线程for(int i = 0; i < 5; i++) {printf("%d\n", i);}printf("tid : %ld, main thread id : %ld\n", tid ,pthread_self());// 主线程调用pthread_join()回收子线程的资源int * thread_retval;ret = pthread_join(tid, (void **)&thread_retval);if(ret != 0) {char * errstr = strerror(ret);printf("error : %s\n", errstr);}printf("exit data : %d\n", *thread_retval);printf("回收子线程资源成功!\n");// 让主线程退出,当主线程退出时,不会影响其他正常运行的线程。pthread_exit(NULL);return 0; 
}

2.4 pthread_detach

#include <pthread.h>
int pthread_detach(pthread_t thread);- 功能:分离一个线程。被分离的线程在终止的时候,会自动释放资源返回给系统。1.不能多次分离,会产生不可预料的行为。2.不能去连接一个已经分离的线程,会报错。- 参数:需要分离的线程的ID- 返回值:成功:0失败:返回错误号
#include <stdio.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>void * callback(void * arg) {printf("chid thread id : %ld\n", pthread_self());return NULL;
}int main() {// 创建一个子线程pthread_t tid;int ret = pthread_create(&tid, NULL, callback, NULL);if(ret != 0) {char * errstr = strerror(ret);printf("error1 : %s\n", errstr);}// 输出主线程和子线程的idprintf("tid : %ld, main thread id : %ld\n", tid, pthread_self());// 设置子线程分离,子线程分离后,子线程结束时对应的资源就不需要主线程释放ret = pthread_detach(tid);if(ret != 0) {char * errstr = strerror(ret);printf("error2 : %s\n", errstr);}// 设置分离后,对分离的子线程进行连接 pthread_join()// ret = pthread_join(tid, NULL);// if(ret != 0) {//     char * errstr = strerror(ret);//     printf("error3 : %s\n", errstr);// }pthread_exit(NULL);return 0;
}

2.5 patch_cancel

pthread_cancel 是一个用于取消线程的函数。它的功能是让线程终止运行,但需要注意的是,这并不意味着线程会立即终止。线程的终止需要等到它执行到一个“取消点”时才会发生。所谓“取消点”,可以简单理解为系统规定的一些系统调用,这些调用通常涉及从用户空间到内核空间的切换。当线程执行到这些取消点时,它才会真正终止。

#include <stdio.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>void * callback(void * arg) {printf("chid thread id : %ld\n", pthread_self());for(int i = 0; i < 5; i++) {printf("child : %d\n", i);}return NULL;
}int main() {// 创建一个子线程pthread_t tid;int ret = pthread_create(&tid, NULL, callback, NULL);if(ret != 0) {char * errstr = strerror(ret);printf("error1 : %s\n", errstr);}// 取消线程pthread_cancel(tid);for(int i = 0; i < 5; i++) {printf("%d\n", i);}// 输出主线程和子线程的idprintf("tid : %ld, main thread id : %ld\n", tid, pthread_self());pthread_exit(NULL);return 0;
}

2.6 pthread_attr

int pthread_attr_init(pthread_attr_t *attr);- 初始化线程属性变量int pthread_attr_destroy(pthread_attr_t *attr);- 释放线程属性的资源int pthread_attr_getdetachstate(const pthread_attr_t *attr, int *detachstate);- 获取线程分离的状态属性int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);- 设置线程分离的状态属性
#include <stdio.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>void * callback(void * arg) {printf("chid thread id : %ld\n", pthread_self());return NULL;
}int main() {// 创建一个线程属性变量pthread_attr_t attr;// 初始化属性变量pthread_attr_init(&attr);// 设置属性pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);// 创建一个子线程pthread_t tid;int ret = pthread_create(&tid, &attr, callback, NULL);if(ret != 0) {char * errstr = strerror(ret);printf("error1 : %s\n", errstr);}// 获取线程的栈的大小size_t size;pthread_attr_getstacksize(&attr, &size);printf("thread stack size : %ld\n", size);// 输出主线程和子线程的idprintf("tid : %ld, main thread id : %ld\n", tid, pthread_self());// 释放线程属性资源pthread_attr_destroy(&attr);pthread_exit(NULL);return 0;
}

三、实战demo

下面程序也存在一定的问题,就是可能多个线程卖同一张票,也可能最后会出现卖0张、卖第-1张票的情况出现。这就是线程同步的问题。

/*使用多线程实现买票的案例。有3个窗口,一共是100张票。
*/#include <stdio.h>
#include <pthread.h>
#include <unistd.h>// 全局变量,所有的线程都共享这一份资源。
// 如果放到sellticket中,俺么大家都会卖100张,一共卖300张。
int tickets = 100;void * sellticket(void * arg) {// 卖票while(tickets > 0) {usleep(6000);printf("%ld 正在卖第 %d 张门票\n", pthread_self(), tickets);tickets--;}return NULL;
}int main() {// 创建3个子线程pthread_t tid1, tid2, tid3;pthread_create(&tid1, NULL, sellticket, NULL);pthread_create(&tid2, NULL, sellticket, NULL);pthread_create(&tid3, NULL, sellticket, NULL);// 回收子线程的资源,阻塞// pthread_join(tid1, NULL);// pthread_join(tid2, NULL);// pthread_join(tid3, NULL);// 设置线程分离。pthread_detach(tid1);pthread_detach(tid2);pthread_detach(tid3);pthread_exit(NULL); // 退出主线程return 0;
}

四、线程同步

从上面的demo可以看出线程同步的情况需要考虑。

线程的主要优势在于,能够通过全局变量来共享信息。不过,这种便捷的共享是有代价的,必须确保多个线程不会同时修改同一变量,或者某一线程不会读取正在由其他线程修改的变量。

临界区是指访问某一共享资源的代码片段,并且这段代码的执行应为原子操作,也就是同时访问同一共享资源的其他线程不应终端该片段的执行。

线程同步:即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作,其他线程才能对该内存地址进行操作,而其他线程则处于等待状态。

为避免线程更新共享变量时出现问题,可以使用互斥量(mutex是 mutual exclusion的缩写)来确保同时仅有一个线程可以访问某项共享资源。可以使用互斥量来保证对任意共享资源的原子访问。

互斥量有两种状态:已定(locked)和未定(unlocked)。任何时候,至多只有一个线程可以定该互斥量。试图对已经定的某一互斥量再次加,将可能阻塞线程或者报错失败,具体取决于加时使用的方法。

一旦线程定互斥量,随即成为该互斥量的所有者,只有所有者才能给互斥量解。一般情况下,对每一共享资源(可能由多个相关变量组成)会使用不同的互斥量,每一线程在访问同一资源时将采用如下协议:首先针对共享资源定互斥量,然后访问共享资源,最后对互斥量解即可。

简单来说就是如果多个线程相同时执行一个代码(临界区),只有一个线程能够持有互斥量,其他的线程将阻塞,这样也就保证了只有一个线程能够进入这段代码区域。

在这里插入图片描述

先来看看mutex的相关函数

    互斥量的类型 pthread_mutex_tint pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);- 初始化互斥量- 参数 :- mutex : 需要初始化的互斥量变量- attr : 互斥量相关的属性,NULL- restrict : C语言的修饰符,被修饰的指针,不能由另外的一个指针进行操作。pthread_mutex_t *restrict mutex = xxx;pthread_mutex_t * mutex1 = mutex;int pthread_mutex_destroy(pthread_mutex_t *mutex);- 释放互斥量的资源int pthread_mutex_lock(pthread_mutex_t *mutex);-,阻塞的,如果有一个线程加了,那么其他的线程只能阻塞等待int pthread_mutex_trylock(pthread_mutex_t *mutex);- 尝试加,如果加失败,不会阻塞,会直接返回。int pthread_mutex_unlock(pthread_mutex_t *mutex);-

所以刚刚的代码可以优化成下面加的形式。

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>// 全局变量,所有的线程都共享这一份资源。
int tickets = 1000;// 创建一个互斥量
pthread_mutex_t mutex;void * sellticket(void * arg) {// 卖票while(1) {// 加pthread_mutex_lock(&mutex);if(tickets > 0) {usleep(6000);printf("%ld 正在卖第 %d 张门票\n", pthread_self(), tickets);tickets--;}else {// 解pthread_mutex_unlock(&mutex);break;}// 解pthread_mutex_unlock(&mutex);}return NULL;
}int main() {// 初始化互斥量pthread_mutex_init(&mutex, NULL);// 创建3个子线程pthread_t tid1, tid2, tid3;pthread_create(&tid1, NULL, sellticket, NULL);pthread_create(&tid2, NULL, sellticket, NULL);pthread_create(&tid3, NULL, sellticket, NULL);// 回收子线程的资源,阻塞pthread_join(tid1, NULL);pthread_join(tid2, NULL);pthread_join(tid3, NULL);pthread_exit(NULL); // 退出主线程// 释放互斥量资源pthread_mutex_destroy(&mutex);return 0;
}

五、死

有时一个线程需要同时访问两个或更多不同的共享资源,而每个资源又都由不同的互斥量管理。当超过一个线程加同一组互斥量时,就有可能发生死

两个或两个以上的进程在执行过程中,因争夺共享资源而造成的一种互相等待的现象,若无外力作用它们都将无法推进下去。此时称系统处于死状态或系统产生了死

的几种场景:忘记释放、重复加(也就是一条加的语句连续有两行这种,会导致第二行就卡住了)、多线程多导致抢占资源。

比如下面这种情况,就是最常见的死情况示意图。

在这里插入图片描述

六、读写

当有一个线程已经持有互斥时,互斥将所有试图进入临界区的线程都阻塞住。但是考虑一种情形,当前持有互斥的线程只是要读访问共享资源,而同时有其它几个线程也想读取这个共享资源,但是由于互斥的排它性,所有其它线程都无法获取,也就无法读访问共享资源了,但是实际上多个线程同时读访问共享资源并不会导致问题。

在对数据的读写操作中,更多的是读操作,写操作较少,例如对数据库数据的读写应用。为了满足当前能够允许多个读出,但只允许一个写入的需求,线程提供了读写来实现。

读写的特点:
如果有其它线程读数据,则允许其它线程执行读操作,但不允许写操作。
如果有其它线程写数据,则其它线程都不允许读、写操作。
写是独占的,写的优先级高。

读写的类型 pthread_rwlock_t
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);
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);
/*案例:8个线程操作同一个全局变量。3个线程不定时写这个全局变量,5个线程不定时的读这个全局变量
*/#include <stdio.h>
#include <pthread.h>
#include <unistd.h>// 创建一个共享数据
int num = 1;
// pthread_mutex_t mutex;
pthread_rwlock_t rwlock;void * writeNum(void * arg) {while(1) {pthread_rwlock_wrlock(&rwlock);num++;printf("++write, tid : %ld, num : %d\n", pthread_self(), num);pthread_rwlock_unlock(&rwlock);usleep(100);}return NULL;
}void * readNum(void * arg) {while(1) {pthread_rwlock_rdlock(&rwlock);printf("===read, tid : %ld, num : %d\n", pthread_self(), num);pthread_rwlock_unlock(&rwlock);usleep(100);}return NULL;
}int main() {pthread_rwlock_init(&rwlock, NULL);// 创建3个写线程,5个读线程pthread_t wtids[3], rtids[5];for(int i = 0; i < 3; i++) {pthread_create(&wtids[i], NULL, writeNum, NULL);}for(int i = 0; i < 5; i++) {pthread_create(&rtids[i], NULL, readNum, NULL);}// 设置线程分离for(int i = 0; i < 3; i++) {pthread_detach(wtids[i]);}for(int i = 0; i < 5; i++) {pthread_detach(rtids[i]);}getchar();pthread_rwlock_destroy(&rwlock);pthread_exit(NULL);return 0;
}

运行之后可以看到num是非常有顺序的。

在这里插入图片描述

七、生产消费者模型

在这里插入图片描述

下面是一个粗略简单的生产者消费者demo模型。

但是下面的代码有一点问题,就是如果没有数据消费者还是一直while循环,那么就不太好,浪费了资源,所以就引入 条件变量

#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>// 创建一个互斥量
pthread_mutex_t mutex;struct Node{int num;struct Node *next;
};// 头结点
struct Node * head = NULL;void * producer(void * arg) {// 不断的创建新的节点,添加到链表中while(1) {pthread_mutex_lock(&mutex);struct Node * newNode = (struct Node *)malloc(sizeof(struct Node));newNode->next = head;head = newNode;newNode->num = rand() % 1000;printf("add node, num : %d, tid : %ld\n", newNode->num, pthread_self());pthread_mutex_unlock(&mutex);usleep(100);}return NULL;
}void * customer(void * arg) {while(1) {pthread_mutex_lock(&mutex);// 保存头结点的指针struct Node * tmp = head;// 判断是否有数据if(head != NULL) {// 有数据head = head->next;printf("del node, num : %d, tid : %ld\n", tmp->num, pthread_self());free(tmp);pthread_mutex_unlock(&mutex);usleep(100);} else {// 没有数据就释放pthread_mutex_unlock(&mutex);}}return  NULL;
}int main() {pthread_mutex_init(&mutex, NULL);// 创建5个生产者线程,和5个消费者线程pthread_t ptids[5], ctids[5];for(int i = 0; i < 5; i++) {pthread_create(&ptids[i], NULL, producer, NULL);pthread_create(&ctids[i], NULL, customer, NULL);}for(int i = 0; i < 5; i++) {pthread_detach(ptids[i]);pthread_detach(ctids[i]);}while(1) {sleep(10);}pthread_mutex_destroy(&mutex);pthread_exit(NULL);return 0;
}
条件变量的类型 pthread_cond_t
int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
int pthread_cond_destroy(pthread_cond_t *cond);
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);- 唤醒所有的等待的线程

下面是对上面demo引入条件变量的优化。

#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>// 创建一个互斥量
pthread_mutex_t mutex;
// 创建条件变量
pthread_cond_t cond;struct Node{int num;struct Node *next;
};// 头结点
struct Node * head = NULL;void * producer(void * arg) {// 不断的创建新的节点,添加到链表中while(1) {pthread_mutex_lock(&mutex);struct Node * newNode = (struct Node *)malloc(sizeof(struct Node));newNode->next = head;head = newNode;newNode->num = rand() % 1000;printf("add node, num : %d, tid : %ld\n", newNode->num, pthread_self());// 只要生产了一个,就通知消费者消费pthread_cond_signal(&cond);pthread_mutex_unlock(&mutex);usleep(100);}return NULL;
}void * customer(void * arg) {while(1) {pthread_mutex_lock(&mutex);// 保存头结点的指针struct Node * tmp = head;// 判断是否有数据if(head != NULL) {// 有数据head = head->next;printf("del node, num : %d, tid : %ld\n", tmp->num, pthread_self());free(tmp);pthread_mutex_unlock(&mutex);usleep(100);} else {// 没有数据,需要等待// 当这个函数调用阻塞的时候,会对互斥进行解,这样其他的生产者就可以继续生产,不会造成死。当不阻塞的时候,继续向下执行,会重新加。所以这也就是为什么后面还是要继续多一行代码进行解pthread_cond_wait(&cond, &mutex);pthread_mutex_unlock(&mutex);}}return  NULL;
}int main() {pthread_mutex_init(&mutex, NULL);pthread_cond_init(&cond, NULL);// 创建5个生产者线程,和5个消费者线程pthread_t ptids[5], ctids[5];for(int i = 0; i < 5; i++) {pthread_create(&ptids[i], NULL, producer, NULL);pthread_create(&ctids[i], NULL, customer, NULL);}for(int i = 0; i < 5; i++) {pthread_detach(ptids[i]);pthread_detach(ctids[i]);}while(1) {sleep(10);}pthread_mutex_destroy(&mutex);pthread_cond_destroy(&cond);pthread_exit(NULL);return 0;
}

接着我们来看看信号量(用于阻塞线程),但是不能保证线程的数据安全问题,如果要保证数据安全问题,需要跟互斥一起使用。

信号量的类型 sem_t
int sem_init(sem_t *sem, int pshared, unsigned int value);
int sem_destroy(sem_t *sem);
int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
int sem_post(sem_t *sem);
int sem_getvalue(sem_t *sem, int *sval);
    信号量的类型 sem_tint sem_init(sem_t *sem, int pshared, unsigned int value);- 初始化信号量- 参数:- sem : 信号量变量的地址- pshared : 0 用在线程间 ,非0 用在进程间- value : 信号量中的值int sem_destroy(sem_t *sem);- 释放资源int sem_wait(sem_t *sem);- 对信号量加,调用一次对信号量的值-1,如果值为0,就阻塞int sem_trywait(sem_t *sem);int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);int sem_post(sem_t *sem);- 对信号量解,调用一次对信号量的值+1int sem_getvalue(sem_t *sem, int *sval);sem_t psem;sem_t csem;init(psem, 0, 8);init(csem, 0, 0);producer() {sem_wait(&psem);sem_post(&csem)}customer() {sem_wait(&csem);sem_post(&psem)}
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
#include <semaphore.h>// 创建一个互斥量
pthread_mutex_t mutex;
// 创建两个信号量
sem_t psem;
sem_t csem;struct Node{int num;struct Node *next;
};// 头结点
struct Node * head = NULL;void * producer(void * arg) {// 不断的创建新的节点,添加到链表中while(1) {sem_wait(&psem);pthread_mutex_lock(&mutex);struct Node * newNode = (struct Node *)malloc(sizeof(struct Node));newNode->next = head;head = newNode;newNode->num = rand() % 1000;printf("add node, num : %d, tid : %ld\n", newNode->num, pthread_self());pthread_mutex_unlock(&mutex);sem_post(&csem);}return NULL;
}void * customer(void * arg) {while(1) {sem_wait(&csem);pthread_mutex_lock(&mutex);// 保存头结点的指针struct Node * tmp = head;head = head->next;printf("del node, num : %d, tid : %ld\n", tmp->num, pthread_self());free(tmp);pthread_mutex_unlock(&mutex);sem_post(&psem);}return  NULL;
}int main() {pthread_mutex_init(&mutex, NULL);sem_init(&psem, 0, 8);sem_init(&csem, 0, 0);// 创建5个生产者线程,和5个消费者线程pthread_t ptids[5], ctids[5];for(int i = 0; i < 5; i++) {pthread_create(&ptids[i], NULL, producer, NULL);pthread_create(&ctids[i], NULL, customer, NULL);}for(int i = 0; i < 5; i++) {pthread_detach(ptids[i]);pthread_detach(ctids[i]);}while(1) {sleep(10);}pthread_mutex_destroy(&mutex);pthread_exit(NULL);return 0;
}

http://www.ppmy.cn/devtools/155722.html

相关文章

深入解析 C++ 字符串处理:提取和分割的多种方法

在 C 编程中&#xff0c;字符串处理是一个常见的任务&#xff0c;尤其是在需要从字符串中提取特定数据时。本文将详细探讨如何使用 C 标准库中的工具&#xff08;如 std::istringstream 和 std::string 的成员函数&#xff09;来提取和分割字符串&#xff0c;并分析不同方法的适…

LeetCode 0598.区间加法 II:最小值

【LetMeFly】598.区间加法 II&#xff1a;最小值 力扣题目链接&#xff1a;https://leetcode.cn/problems/range-addition-ii/ 给你一个 m x n 的矩阵 M 和一个操作数组 op 。矩阵初始化时所有的单元格都为 0 。ops[i] [ai, bi] 意味着当所有的 0 < x < ai 和 0 < …

单片机串口打印printf函数显示内容(固件库开发)

1.hal_usart.c 文件 #include <stdio.h> #include "hal_usart.h" #include "stm32F10x.h"//**要根据 使用的是哪个串口 对应修改 串口号 eg&#xff1a;USART1** void USART_PUTC(char ch) {/* 等待数据寄存器为空 */while((USART1->SR & …

[LeetCode]day4 977.有序数组的平方

977. 有序数组的平方 - 力扣&#xff08;LeetCode&#xff09; 一.题目 给你一个按 非递减顺序 排序的整数数组 nums&#xff0c;返回 每个数字的平方 组成的新数组&#xff0c;要求也按 非递减顺序 排序。 示例 1&#xff1a; 输入&#xff1a;nums [-4,-1,0,3,10] 输出&a…

【工欲善其事】利用 DeepSeek 实现复杂 Git 操作:从原项目剥离出子版本树并同步到新的代码库中

文章目录 利用 DeepSeek 实现复杂 Git 操作1 背景介绍2 需求描述3 思路分析4 实现过程4.1 第一次需求确认4.2 第二次需求确认4.3 第三次需求确认4.4 V3 模型&#xff1a;中间结果的处理4.5 方案验证&#xff0c;首战告捷 5 总结复盘 利用 DeepSeek 实现复杂 Git 操作 1 背景介绍…

Windows socket之WSAEventSelect模型_wsaeventselect模型的socket编程和客户端应用的功能

WSAEVENT hEvent,LPWSANETWORKEVENTS lpNetworkEvents);该函数可以查找发生在套接字上的网络事件&#xff0c;并清除系统内部的网络事件记录&#xff0c;重置事件对象。s为发生网络事件的套接字句柄。hEvent为被重置的事件对象句柄&#xff08;可选)。lpNetworkEvents为指向WSA…

【回溯+剪枝】回溯算法的概念 全排列问题

文章目录 46. 全排列Ⅰ. 什么是回溯算法❓❓❓Ⅱ. 回溯算法的应用1、组合问题2、排列问题3、子集问题 Ⅲ. 解题思路&#xff1a;回溯 剪枝 46. 全排列 46. 全排列 ​ 给定一个不含重复数字的数组 nums &#xff0c;返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。 …

LeetCode 344: 反转字符串

LeetCode 344: 反转字符串 - C语言题解 这道题的目标是反转一个字符数组&#xff08;字符串&#xff09;。我们将通过双指针法来实现这一功能。 代码实现 #include <stdio.h>void reverseString(char* s, int sSize) {int left 0, right sSize - 1; // 定义左右指针…