【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;
}
相关文章:
【C++高并发服务器WebServer】-9:多线程开发
本文目录 一、线程概述1.1 线程和进程的区别1.2 线程之间共享和非共享资源1.3 NPTL 二、线程操作2.1 pthread_create2.2 pthread_exit2.3 pthread_join2.4 pthread_detach2.5 patch_cancel2.6 pthread_attr 三、实战demo四、线程同步五、死锁六、读写锁七、生产消费者模型 一、…...
全解:Redis RDB持久化和AOF持久化
🧑 博主简介:CSDN博客专家,历代文学网(PC端可以访问:https://literature.sinhy.com/#/literature?__c1000,移动端可微信小程序搜索“历代文学”)总架构师,15年工作经验,…...
视频外绘技术总结:Be-Your-Outpainter、Follow-Your-Canvas、M3DDM
Diffusion Models专栏文章汇总:入门与实战 前言:视频Inpaint的技术很火,但是OutPaint却热度不高,这篇博客总结比较经典的几篇视频Outpaint技术。其实Outpaint在runway等工具上很火,可是学术界对此关注比较少,博主从这三年的顶会中找到了最具代表性的三篇论文解读。 目录 …...
LLM - 大模型 ScallingLaws 的设计 100B 预训练方案(PLM) 教程(5)
欢迎关注我的CSDN:https://spike.blog.csdn.net/ 本文地址:https://spike.blog.csdn.net/article/details/145356022 免责声明:本文来源于个人知识与公开资料,仅用于学术交流,欢迎讨论,不支持转载。 Scaling Laws (缩放法则) 是大模型领域中,用于描述 模型性能(Loss) 与…...
Python帝王學集成-母稿
引用:【【全748集】这绝对是2024最全最细的Python全套教学视频,七天看完编程技术猛涨!别再走弯路了,从零基础小白到Python全栈这一套就够了!-哔哩哔哩】 https://b23.tv/lHPI3XV 语法基础 Python解释器与pycharm编辑器安装 - 定义:Python解释器负责将Python代码转换为计…...
MySQL数据导入与导出
在现代软件开发中,数据管理是一个重要的核心环节,而数据库则是进行数据管理的主要工具。MySQL 作为一款开源的关系型数据库管理系统,被广泛应用于企业和个人开发项目中。对于学习编程的初学者或是自学者来说,掌握 MySQL 的基本操作尤为重要,尤其是数据的导入与导出功能。这…...
微服务面试题:概览
🧑 博主简介:CSDN博客专家,历代文学网(PC端可以访问:https://literature.sinhy.com/#/?__c1000,移动端可微信小程序搜索“历代文学”)总架构师,15年工作经验,精通Java编…...
每日一题 430. 扁平化多级双向链表
430. 扁平化多级双向链表 简单 /*class Solution { public:Node* flatten(Node* head) {Node* tail nullptr;return dfs(head);}Node* dfs(Node* head){Node* cur head;while(cur ! nullptr){if(cur->child ! nullptr){Node* curChild getTail(cur->child);Node* te…...
冯诺依曼系统及操作系统
目录 一.冯诺依曼体系结构 二.操作系统 三.系统调用和库函数概念 一.冯诺依曼体系结构 我们常见的计算机,如笔记本。我们不常见的计算机,如服务器,大部分都遵守冯诺依曼体系 截至目前,我们所认识的计算机,都是由一…...
双指针(典型算法思想)——OJ例题算法解析思路
目录 一、283. 移动零 - 力扣(LeetCode) 1. 问题分析 2. 算法思路 3. 代码逐行解析 4. 示例运行 5. 时间复杂度与空间复杂度 6. 总结 二、1089. 复写零 - 力扣(LeetCode) 1. 问题分析 2. 算法思路 3. 代码逐行解析 4. …...
大数据Hadoop入门1
目录 相关资料 第一部分 1.课程内容大纲和学习目标 2.数据分析和企业数据分析方向 3.数据分析基本流程步骤 4.大数据时代 5.分布式和集群 6.Linux操作系统概述 7.VMware虚拟机概念与安装 8.centos操作系统的虚拟机导入 9.VMware虚拟机常规使用、快照 第二部分 1.课…...
Ubuntu-手动安装 SBT
文章目录 前言Ubuntu-手动安装 SBT1. SBT是什么?1.1. SBT 的特点1.2. SBT 的基本功能1.3. SBT 的常用命令 2. 安装2.1. 下载2.2. 解压 sbt 二进制包2.3. 确认 sbt 可执行文件的位置2.4. 设置执行权限2.5. 创建符号链接2.6. 更新 PATH 环境变量2.7. 验证 sbt 安装 前言 如果您觉…...
人工智能:农业领域的变革力量
在当今科技飞速发展的时代,人工智能正以前所未有的态势渗透进各个领域,农业也不例外。想象一下,未来的农田里,农民不再是弯腰劳作的形象,而是坐在高科技的“智能农场”里,悠闲地喝着咖啡,指挥着…...
Qt——界面优化
一.QSS 1.背景 在网页前端开发领域中, CSS 是⼀个至关重要的部分。 描述了⼀个网页的 "样式"。 从而起到对网页美化的作用。 所谓样式,包括不限于大小,位置,颜色,背景,间距,字体等等…...
Qt Designer and Python: Build Your GUI
1.install pyside6 2.pyside6-designer.exe 发送到桌面快捷方式 在Python安装的所在 Scripts 文件夹下找到此文件。如C:\Program Files\Python312\Scripts 3. 打开pyside6-designer 设计UI 4.保存为simple.ui 文件,再转成py文件 用代码执行 pyside6-uic.exe simpl…...
HarmonyOS DevEco Studio模拟器点击运行没有反应的解决方法
HarmonyOS DevEco Studio模拟器点击运行没有反应的解决方法 翻遍了CSDN,试了所有办法都没办法,最后偶然间竟然解决了 解决方法其实很简单:本地模拟器下载路径下面不能有中文。。。。。 切换正确路径以后,成功运行,哦…...
java中的算数运算符
1.java中的加法是“”。 简单数字的相加对于byte.short.char.int类型数字相加时进行整形提升至int,对于数据类型大于int的long.float.double数据类型有参与计算时,需要进行整形提升至最高的数据类型。 有字符串类型的相加,将数字视为字符串进行字符串的…...
【数据结构】二叉树
二叉树 1. 树型结构(了解)1.1 概念1.2 概念(重要)1.3 树的表示形式(了解)1.4 树的应用 2. 二叉树(重点)2.1 概念2.2 两种特殊的二叉树2.3 二叉树的性质2.4 二叉树的存储2.5 二叉树的…...
websocket实现
由于安卓资源管理器展示的路径不尽相同,各种软件保存文件的位置也不一定一样.对于普通用户上传文件时,查找文件可能是一个麻烦的事情.后来想到了一个办法,使用pc端进行辅助上传. 文章目录 实现思路1.0 实现定义web与客户端通信数据类型和数据格式web端websocket实现web端对客户…...
AI软件外包需要注意什么 外包开发AI软件的关键因素是什么 如何选择AI外包开发语言
1. 定义目标与需求 首先,要明确你希望AI智能体做什么。是自动化任务、数据分析、自然语言处理,还是其他功能?明确目标可以帮助你选择合适的技术和方法。 2. 选择开发平台与工具 开发AI智能体的软件时,你需要选择适合的编程语言、…...
电梯系统的UML文档12
5.2.1 DoorControl 的状态图 图 19: DoorControl 的状态图 5.2.2 DriveControl 的状态图 图 20: DriveControl 的状态图 5.2.3 LanternControl 的状态图 图 21: LanternControl 的状态图 5.2.4 HallButtonControl 的状态图 图 22: HallButtonControl 的状态图 5.2.5 CarB…...
【华为路由的arp配置】
华为路由的arp配置 ARP:IP地址与MAC地址的映射。 R1: g0/0/0:10.1.1.254/24 g0/0/1:10.1.2.254/24 PC1: 10.1.1.1/16 PC2: 10.1.1.2/16 PC3: 10.1.2.3/16 动态ARP 查看PC1的arp表,可以看到,列表为空。 查看R1的arp表 在PC3上ping命令测…...
Web 代理、爬行器和爬虫
目录 Web 在线网页代理服务器的使用方法Web 在线网页代理服务器使用流程详解注意事项 Web 请求和响应中的代理方式Web 开发中的请求方法借助代理进行文件下载的示例 Web 服务器请求代理方式代理、网关和隧道的概念参考文献说明 爬虫的工作原理及案例网络爬虫概述爬虫工作原理 W…...
node 爬虫开发内存处理 zp_stoken 作为案例分析
声明: 本文章中所有内容仅供学习交流使用,不用于其他任何目的,抓包内容、敏感网址、数据接口等均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关! 前言 主要说3种我们补环境过后如果用…...
【Samba】Ubuntu20.04 Windows 共享文件夹
【Samba】Ubuntu20.04 Windows 共享文件夹 前言整体思路检查 Ubuntu 端 和 Windows 网络通信是否正常创建共享文件夹安装并配置 Samba 服务器安装 Samba 服务器创建 Samba 用户编辑 Samba 配置文件重启 Samba 服务器 在 Windows 端 访问 Ubuntu 的共享文件夹 前言 本文基于 Ub…...
windows下本地部署安装hadoop+scala+spark-【不需要虚拟机】
注意版本依赖【本实验版本如下】 Hadoop 3.1.1 spark 2.3.2 scala 2.11 1.依赖环境 1.1 java 安装java并配置环境变量【如果未安装搜索其他教程】 环境验证如下: C:\Users\wangning>java -version java version "1.8.0_261" Java(TM) SE Runti…...
GitHub 仓库的 Archived 功能详解:中英双语
GitHub 仓库的 Archived 功能详解 一、什么是 GitHub 仓库的 “Archived” 功能? 在 GitHub 上,“Archived” 是一个专门用于标记仓库状态的功能。当仓库被归档后,它变为只读模式,所有的功能如提交代码、创建 issue 和 pull req…...
银行卡三要素验证接口:方便快捷地实现银行卡核验功能
银行卡三要素验证API:防止欺诈交易的有力武器 随着互联网的发展,电子支付方式也越来越普及。在支付过程中,银行卡是最常用的支付工具之一。然而,在一些支付场景中,需要对用户的银行卡信息进行验证,以确保支…...
Banana JS,一个严格子集 JavaScript 的解释器
项目地址:https://github.com/shajunxing/banana-js 特色 我的目标是剔除我在实践中总结的JavaScript语言的没用的和模棱两可的部分,只保留我喜欢和需要的,创建一个最小的语法解释器。只支持 JSON 兼容的数据类型和函数,函数是第…...
引领未来科技潮流:Web3 前沿发展趋势
随着技术不断发展,我们正站在一个全新的互联网时代的门槛上,Web3的出现正在重新定义互联网的构架和运作方式。Web3,作为互联网的下一代发展趋势,其核心思想是去中心化、开放与用户主权。与现有的Web2.0相比,Web3更加注…...
OpenCV:在图像中添加高斯噪声、胡椒噪声
目录 在图像中添加高斯噪声 高斯噪声的特性 添加高斯噪声的实现 给图像添加胡椒噪声 实现胡椒噪声的步骤 相关阅读 OpenCV:图像处理中的低通滤波-CSDN博客 OpenCV:高通滤波之索贝尔、沙尔和拉普拉斯-CSDN博客 OpenCV:图像滤波、卷积与…...
在深度Linux (Deepin) 20中安装Nvidia驱动
文章创作不易,麻烦大家点赞关注收藏一键三连。 在Deepin上面跑Tensorflow, pytorch等人工智能框架不是一件容易的事情。特别是如果你要使用GPU,就得有nvidia的驱动。默认情况下Deepin系统自带的是nouveau开源驱动。这是没办法用tensorflow的。下面内容是…...
PC端实现PDF预览(支持后端返回文件流 || 返回文件URL)
一、使用插件 插件名称:vue-office/pdf 版本:2.0.2 安装插件:npm i vue-office/pdf^2.0.2 1、“vue-office/pdf”: “^2.0.2”, 2、 npm i vue-office/pdf^2.0.2 二、代码实现 // 引入组件 (在需要使用的页面中直接引入&#x…...
【ESP32】ESP-IDF开发 | WiFi开发 | UDP用户数据报协议 + UDP客户端和服务器例程
1. 简介 UDP协议(User Datagram Protocol),全称用户数据报协议,它是一种面向非连接的协议,面向非连接指的是在正式通信前不必与对方先建立连接, 不管对方状态就直接发送。至于对方是否可以接收到这些数据内…...
OpenAI的真正对手?DeepSeek-R1如何用强化学习重构LLM能力边界——DeepSeek-R1论文精读
2025年1月20日,DeepSeek-R1 发布,并同步开源模型权重。截至目前,DeepSeek 发布的 iOS 应用甚至超越了 ChatGPT 的官方应用,直接登顶 AppStore。 DeepSeek-R1 一经发布,各种资讯已经铺天盖地,那就让我们一起…...
es数据同步
Logstash 是 Elastic 技术栈中的一个技术,它是一个数据采集引擎,可以从数据库采集数据到 ES 中。可以通过设置 自增 ID 主键 或 更新时间 来控制数据的自动同步: 自增 ID 主键:Logstatsh 会有定时任务,如果发现有主键…...
【JavaScript笔记】01- 原型及原型链(面试高频内容)
前言 JavaScript作为前端入门三件套之一,也是前端求职的必会知识,重要性不言而喻。 这个系列分享个人学习JavaScript的记录,和大家一起学习讨论。 下面介绍关于原型&原型链的相关重要知识点。 1、构造函数创建对象 function Student(…...
【Python】第五弹---深入理解函数:从基础到进阶的全面解析
✨个人主页: 熬夜学编程的小林 💗系列专栏: 【C语言详解】 【数据结构详解】【C详解】【Linux系统编程】【MySQL】【Python】 目录 1、函数 1.1、函数是什么 1.2、语法格式 1.3、函数参数 1.4、函数返回值 1.5、变量作用域 1.6、函数…...
动态规划DP 数字三角形模型(模型分析+例题分析+C++代码实现)(数字三角形、摘花生、最低通行费用、方格取数、传纸条)
总体概览 数字三角形 原题链接 AcWing 898.数字三角形 题目描述 给定一个如下图所示的数字三角形,从顶部出发,在每一结点可以选择移动至其左下方的结点或移动至其右下方的结点,一直走到底层,要求找出一条路径,使路…...
2025 最新flutter面试总结
目录 1.Dart是值传递还是引用传递? 2.Flutter 是单引擎还是双引擎 3. StatelessWidget 和 StatefulWidget 在 Flutter 中有什么区别? 4.简述Dart语音特性 5. Navigator 是什么?在 Flutter 中 Routes 是什么? 6、Dart 是不是…...
Java后端之AOP
AOP:面向切面编程,本质是面向特定方法编程 引入依赖: <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency>示例:记录…...
JS中对数组的操作哪些会改变原数组哪些不会?今天你一定要记下!
JavaScript 数组方法:变更原数组与不变更原数组的区别 在 JavaScript 中,数组是非常常见且重要的数据结构。作为开发者,我们常常需要使用数组方法来处理数组数据。但是,数组的不同方法会以不同的方式影响原数组,它们可…...
ubuntu x64下交叉编译ffmpeg到目标架构为aarch架构的系统
参考链接 https://blog.csdn.net/qq_46396470/article/details/137794498...
Java进阶(二):Java设计模式
目录 设计模式 一.建模语言 二.类之间的关系 1.依赖关系 2.关联关系 3.聚合关系 4.组合关系 5.继承关系 6.实现关系 三.面向对象设计原则 单一职责原则 开闭原则 里氏替换原则 依赖倒置 接口隔离原则 迪米特原则 组合/聚合(关联关系)复用原则 四.23种设计模式…...
python学opencv|读取图像(四十二)使用cv2.add()函数实现多图像叠加
【1】引言 前序学习过程中,掌握了灰度图像和彩色图像的掩模操作: python学opencv|读取图像(九)用numpy创建黑白相间灰度图_numpy生成全黑图片-CSDN博客 python学opencv|读取图像(四十)掩模:三…...
DIY QMK量子键盘
最近放假了,趁这个空余在做一个分支项目,一款机械键盘,量子键盘取自固件名称QMK(Quantum Mechanical Keyboard)。 键盘作为计算机或其他电子设备的重要输入设备之一,通过将按键的物理动作转换为数字信号&am…...
【公式】卢布贬值风险:义乌到俄罗斯贸易的汇率陷阱
卢布贬值风险:义乌到俄罗斯贸易的汇率陷阱 具体实例与推演 假设一位中国义乌的商人,计划出口一批价值100万人民币的商品到俄罗斯。最初的汇率是1人民币兑换100卢布。 初始状态: 商品价值:100万人民币初始汇率:1人民币…...
1月27(信息差)
🌍喜大普奔,适用于 VS Code 的 GitHub Copilot 全新免费版本正式推出,GitHub 全球开发者突破1.5亿 🎄Kimi深夜炸场:满血版多模态o1级推理模型!OpenAI外全球首次!Jim Fan:同天两款国…...
Linux常见问题解决方法--1
常见安全工具、设备 工具 端口及漏洞扫描:Namp、Masscan 抓包:Wireshark,Burpsuite、Fiddler、HttpCanary Web自动化安全扫描:Nessus、Awvs、Appscan、Xray 信息收集:Oneforall、hole 漏洞利用:MSF、…...
Python 数据清洗与处理常用方法全解析
在数据处理与分析过程中,缺失值、重复值、异常值等问题是常见的挑战。本文总结了多种数据清洗与处理方法:缺失值处理包括删除缺失值、固定值填充、前后向填充以及删除缺失率高的列;重复值处理通过删除或标记重复项解决数据冗余问题࿱…...