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

在这里插入图片描述

本文目录

  • 一、线程概述
    • 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.mzph.cn/web/67321.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

14-6-1C++STL的list

(一&#xff09;list容器的基本概念 list容器简介&#xff1a; 1.list是一个双向链表容器&#xff0c;可高效地进行插入删除元素 2.list不可以随机存取元素&#xff0c;所以不支持at.(pos)函数与[ ]操作符 &#xff08;二&#xff09;list容器头部和尾部的操作 list对象的默…

在sortablejs的拖拽排序情况下阻止input拖拽事件

如题 问题 在vue3的elementPlus的table中&#xff0c;通过sortablejs添加了行拖拽功能&#xff0c;但是在行内会有输入框&#xff0c;此时拖拽输入框会触发sortablejs的拖拽功能 解决 基于这个现象&#xff0c;我怀疑是由于拖拽事件未绑定而冒泡到后面的行上从而导致的拖拽…

21.Word:小赵-毕业论文排版❗【39】

目录 题目​ NO1.2 NO3.4 NO5.6 NO7.8.9 NO10.11.12 题目 NO1.2 自己的论文当中接收老师的修改&#xff1a;审阅→比较→源文档&#xff1a;考生文件夹&#xff1a;Word.docx→修订的文档&#xff1a;考生文件夹&#xff1a;教师修改→确定→接收→接收所有修订将合并之…

leetcode_链表 876.链表的中间节点

876.链表的中间节点 给你单链表的头结点 head &#xff0c;请你找出并返回链表的中间结点。如果有两个中间结点&#xff0c;则返回第二个中间结点。思路&#xff1a;快慢指针&#xff0c;创建两个指针fast和slow&#xff0c;fast指针每次移动两步&#xff0c;slow指针每次移动…

深度学习 DAY3:NLP发展史及早期的前馈神经网络(ANN)及多任务学习

NLP发展史 NLP发展脉络简要梳理如下&#xff1a; 2001 - Neural language models&#xff08;神经语言模型&#xff09; 2008 - Multi-task learning&#xff08;多任务学习&#xff09; 2013 - Word embeddings&#xff08;词嵌入&#xff09; 2013 - Neural networks for NL…

全面了解 Web3 AIGC 和 AI Agent 的创新先锋 MelodAI

不管是在传统领域还是 Crypto&#xff0c;AI 都是公认的最有前景的赛道。随着数字内容需求的爆炸式增长和技术的快速迭代&#xff0c;Web3 AIGC&#xff08;AI生成内容&#xff09;和 AI Agent&#xff08;人工智能代理&#xff09;正成为两大关键赛道。 AIGC 通过 AI 技术生成…

54.数字翻译成字符串的可能性|Marscode AI刷题

1.题目 问题描述 小M获得了一个任务&#xff0c;需要将数字翻译成字符串。翻译规则是&#xff1a;0对应"a"&#xff0c;1对应"b"&#xff0c;依此类推直到25对应"z"。一个数字可能有多种翻译方法。小M需要一个程序来计算一个数字有多少种不同的…

FileReader使用

FileReader : 读取文件内容的api&#xff0c;&#xff0c;&#xff0c;在前端处理上传的文件&#xff0c;&#xff0c;比如预览图片 readAsDataURL(file) &#xff1a; 读取为base64编码的 data urlreadAsText() &#xff1a; 读取为文本readAsArrayBuffer() : 读取为二进制 …

RabbitMQ5-死信队列

目录 死信的概念 死信的来源 死信实战 死信之TTl 死信之最大长度 死信之消息被拒 死信的概念 死信&#xff0c;顾名思义就是无法被消费的消息&#xff0c;一般来说&#xff0c;producer 将消息投递到 broker 或直接到queue 里了&#xff0c;consumer 从 queue 取出消息进…

JavaScript系列(48)-- 3D渲染引擎实现详解

JavaScript 3D渲染引擎实现详解 &#x1f3ae; 今天&#xff0c;让我们深入探讨JavaScript的3D渲染引擎实现。通过WebGL和现代JavaScript技术&#xff0c;我们可以构建一个功能完整的3D渲染系统。 3D渲染基础概念 &#x1f31f; &#x1f4a1; 小知识&#xff1a;3D渲染引擎的…

10JavaWeb——SpringBootWeb案例01

前面我们已经讲解了Web前端开发的基础知识&#xff0c;也讲解了Web后端开发的基础(HTTP协议、请求响应)&#xff0c;并且也讲解了数据库MySQL&#xff0c;以及通过Mybatis框架如何来完成数据库的基本操作。 那接下来&#xff0c;我们就通过一个案例&#xff0c;来将前端开发、后…

【面试题】 Java 三年工作经验(2025)

问题列表 为什么选择 spring boot 框架&#xff0c;它与 Spring 有什么区别&#xff1f;spring mvc 的执行流程是什么&#xff1f;如何实现 spring 的 IOC 过程&#xff0c;会用到什么技术&#xff1f;spring boot 的自动化配置的原理是什么&#xff1f;如何理解 spring boot 中…

JAVA 接口、抽象类的关系和用处 详细解析

接口 - Java教程 - 廖雪峰的官方网站 一个 抽象类 如果实现了一个接口&#xff0c;可以只选择实现接口中的 部分方法&#xff08;所有的方法都要有&#xff0c;可以一部分已经写具体&#xff0c;另一部分继续保留抽象&#xff09;&#xff0c;原因在于&#xff1a; 抽象类本身…

ResNeSt: Split-Attention Networks论文学习笔记

这张图展示了一个名为“Split-Attention”的神经网络结构&#xff0c;该结构在一个基数组&#xff08;cardinal group&#xff09;内进行操作。基数组通常指的是在神经网络中处理的一组特征或通道。图中展示了如何通过一系列操作来实现对输入特征的注意力机制。 以下是图中各部…

数据收集后台服务概要设计

为了帮助大家设计一个数据指标汇总的后端应用&#xff0c;我将提供一个概要设计和表设计的建议。这个设计将基于常见的数据收集需求&#xff0c;假设你需要收集、存储和汇总来自不同数据源的指标数据。 1. 概要设计 1.1 系统架构 数据收集层&#xff1a;负责从不同数据源&am…

探秘 TCP TLP:从背景到实现

回家的路上还讨论了个关于 TCP TLP 的问题&#xff0c;闲着无事缕一缕。本文内容参考自 Tail Loss Probe (TLP): An Algorithm for Fast Recovery of Tail Losses 以及 Linux 内核源码。 TLP&#xff0c;先说缘由。自 TCP 引入 Fast retrans 机制就是为了尽力避免 RTO&#xf…

设计模式Python版 原型模式

文章目录 前言一、原型模式二、原型模式示例三、原型管理器 前言 GOF设计模式分三大类&#xff1a; 创建型模式&#xff1a;关注对象的创建过程&#xff0c;包括单例模式、简单工厂模式、工厂方法模式、抽象工厂模式、原型模式和建造者模式。结构型模式&#xff1a;关注类和对…

一文大白话讲清楚webpack进阶——5——dev-server原理及其作用

文章目录 一文大白话讲清楚webpack进阶——5——dev-server原理及其作用1. webpack的作用2. dev-server的作用3. dev-server的原理3.1 啥是webpack-dev-middleware3.2 HMR 一文大白话讲清楚webpack进阶——5——dev-server原理及其作用 1. webpack的作用 webpack的作用我们之…

【第十天】零基础入门刷题Python-算法篇-数据结构与算法的介绍-两种常见的字符串算法(持续更新)

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、Python数据结构与算法的详细介绍1.Python中的常用的字符串算法2.字符串算法3.详细的字符串算法1&#xff09;KMP算法2&#xff09;Rabin-Karp算法 总结 前言…

Writing an Efficient Vulkan Renderer

本文出自GPU Zen 2。 Vulkan 是一个新的显式跨平台图形 API。它引入了许多新概念&#xff0c;即使是经验丰富的图形程序员也可能不熟悉。Vulkan 的主要目标是性能——然而&#xff0c;获得良好的性能需要深入了解这些概念及其高效应用方法&#xff0c;以及特定驱动程序实现的实…