多线程和线程同步复习

多线程和线程同步复习

  • 进程线程区别
  • 创建线程
  • 线程退出
  • 线程回收
    • 全局写法
    • 传参写法
  • 线程分离
  • 线程同步
    • 同步方式
  • 互斥锁
    • 互斥锁进行线程同步
  • 死锁
  • 读写锁
    • api细说
    • 读写锁进行线程同步
  • 条件变量
    • 生产者消费者案例
    • 问题解答
    • 加强版生产者消费者
  • 总结
  • 信号量
    • 信号量实现生产者消费者同步-->一个资源
    • 信号量实现生产者消费者同步-->多个资源

进程线程区别

  1. 进程是资源分配的最小单位,线程是操作系统调度执行的最小单位。
  2. 进程有自己独立的地址空间, 多个线程共用同一个地址空间
  3. 线程更加节省系统资源, 效率不仅可以保持的, 而且能够更高
    在一个地址空间中多个线程独享: 每个线程都有属于自己的栈区, 寄存器(内核中管理的)
    在一个地址空间中多个线程共享: 代码段, 堆区, 全局数据区, 打开的文件(文件描述符表)都是线程共享的
  4. 线程是程序的最小执行单位, 进程是操作系统中最小的资源分配单位
    每个进程对应一个虚拟地址空间,一个进程只能抢一个CPU时间片
    一个地址空间中可以划分出多个线程, 在有效的资源基础上, 能够抢更多的CPU时间片
  5. CPU的调度和切换: 线程的上下文切换比进程要快的多
  6. 线程更加廉价, 启动速度更快, 退出也快, 对系统资源的冲击小。

创建线程

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>// 子线程的处理代码
void* working(void* arg)
{printf("我是子线程, 线程ID: %ld\n", pthread_self());for(int i=0; i<9; ++i){printf("child == i: = %d\n", i);}return NULL;
}int main()
{// 1. 创建一个子线程pthread_t tid;pthread_create(&tid, NULL, working, NULL);printf("子线程创建成功, 线程ID: %ld\n", tid);// 2. 子线程不会执行下边的代码, 主线程执行printf("我是主线程, 线程ID: %ld\n", pthread_self());for(int i=0; i<3; ++i){printf("i = %d\n", i);}// 休息, 休息一会儿...sleep(1);// >>>>>>>>>>>>>>>>>>>> 让主线程休息一会给子线程运行return 0;
}

子线程被创建出来之后需要抢cpu时间片, 抢不到就不能运行,如果主线程退出了, 虚拟地址空间就被释放了, 子线程就一并被销毁了。
这里的解决方案,让子线程执行完毕, 主线程再退出, 可以在主线程中添加挂起函数 sleep();
在这里插入图片描述

线程退出

子线程退出这个地址空间是存在的不影响主线程,这个线程退出函数实际上是服务于主线程的。这个函数作用是让主线程退出后子线程继续执行,而不回收这块虚拟地址空间,当子线程退出后这块虚拟地址空间就会被操作系统回收。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>// 子线程的处理代码
void* working(void* arg)
{printf("我是子线程, 线程ID: %ld\n", pthread_self());for(int i=0; i<9; ++i){printf("child == i: = %d\n", i);}return NULL;
}int main()
{// 1. 创建一个子线程pthread_t tid;pthread_create(&tid, NULL, working, NULL);printf("子线程创建成功, 线程ID: %ld\n", tid);pthread_exit(NULL);return 0;
}

在这里插入图片描述
可以看出来主线程退出后是不影响子线程的执行

线程回收

#include <pthread.h>
// 这是一个阻塞函数, 子线程在运行这个函数就阻塞
// 子线程退出, 函数解除阻塞, 回收对应的子线程资源, 类似于回收进程使用的函数 wait()
int pthread_join(pthread_t thread, void **retval);

全局写法

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>struct Test
{int num;int age;
};
struct Test t;
// 子线程的处理代码
void* working(void* arg)
{for(int i=0; i<5; ++i){printf("子线程:i = %d\n", i);}printf("子线程ID: %ld\n", pthread_self());//    struct Test t;  >>>>>>>>>>> errt.num=100;t.age=66;pthread_exit(&t);return NULL;
}
int main()
{pthread_t tid;pthread_create(&tid, NULL, working, NULL);printf("主线程:%ld\n",pthread_self());void* ptr;pthread_join(tid,&ptr);struct Test* pt=(struct Test*)ptr;printf("num: %d,age = %d\n",pt->num,pt->age);return 0;
}

在这里插入图片描述
pthread_join(tid, &ptr); 的作用是等待子线程 tid 执行完毕,并将子线程通过 pthread_exit 返回的指针值保存到 ptr 中。
在这里插入图片描述

传参写法

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>struct Test
{int num;int age;
};// 子线程的处理代码
void* working(void* arg)
{for(int i=0; i<5; ++i){printf("子线程:i = %d\n", i);}printf("子线程ID: %ld\n", pthread_self());struct Test *t=(struct Test*)arg;t->num=100;t->age=66;pthread_exit(&t);return NULL;
}int main()
{pthread_t tid;struct Test t;pthread_create(&tid, NULL, working, &t);// >>>> 把主线程的栈空间给了子线程printf("主线程:%ld\n",pthread_self());void* ptr;pthread_join(tid,&ptr);printf("num: %d,age = %d\n",t.num,t.age);// >>>>>直接打印即可return 0;
}

pthread_join 只是等待线程 tid 执行完成,并获取线程 tid 的返回值。此时并没有释放或销毁传递给线程的参数 t

线程分离

在某些情况下,程序中的主线程有属于自己的业务处理流程,如果让主线程负责子线程的资源回收,调用pthread_join()只要子线程不退出主线程就会一直被阻塞,主要线程的任务也就不能被执行了。
调用这个函数之后指定的子线程就可以和主线程分离,当子线程退出的时候,其占用的内核资源就被系统的其他进程接管并回收了
当主线程退出的时候如果子线程还在工作直接就没了,因为虚拟地址空间被释放了

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>// 子线程的处理代码
void* working(void* arg)
{printf("我是子线程, 线程ID: %ld\n", pthread_self());for(int i=0; i<9; ++i){printf("child == i: = %d\n", i);}return NULL;
}int main()
{// 1. 创建一个子线程pthread_t tid;pthread_create(&tid, NULL, working, NULL);printf("子线程创建成功, 线程ID: %ld\n", tid);// 2. 子线程不会执行下边的代码, 主线程执行printf("我是主线程, 线程ID: %ld\n", pthread_self());for(int i=0; i<3; ++i){printf("i = %d\n", i);}// 设置子线程和主线程分离pthread_detach(tid);// 让主线程自己退出即可pthread_exit(NULL);return 0;
}

在这里插入图片描述
子线程实际上被操作系统回收了

线程同步

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

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <pthread.h>#define MAX 50
// 全局变量
int number;// 线程处理函数
void* funcA_num(void* arg)
{for(int i=0; i<MAX; ++i){int cur = number;cur++;usleep(10);// >>>>>>>>>> 微秒number = cur;printf("Thread A, id = %lu, number = %d\n", pthread_self(), number);}return NULL;
}void* funcB_num(void* arg)
{for(int i=0; i<MAX; ++i){int cur = number;cur++;number = cur;printf("Thread B, id = %lu, number = %d\n", pthread_self(), number);usleep(5);}return NULL;
}int main(int argc, const char* argv[])
{pthread_t p1, p2;// 创建两个子线程pthread_create(&p1, NULL, funcA_num, NULL);pthread_create(&p2, NULL, funcB_num, NULL);// 阻塞,资源回收pthread_join(p1, NULL);pthread_join(p2, NULL);return 0;
}

这里创建了两个线程 funcA_num 和 funcB_num,它们分别对共享全局变量 number 进行加一操作并输出结果。然而,由于缺少对 number 的同步处理,会导致线程竞争,从而出现不一致的结果。这是因为在多线程环境中,number 的读写操作并不是原子性的,所以多个线程可能会对 number 进行相互覆盖的操作,导致不正确的输出结果。
具体解释:如果线程A执行这个过程期间就失去了CPU时间片,线程A被挂起了最新的数据没能更新到物理内存。线程B变成运行态之后从物理内存读数据,很显然它没有拿到最新数据,只能基于旧的数据往后数,然后失去CPU时间片挂起。线程A得到CPU时间片变成运行态,第一件事儿就是将上次没更新到内存的数据更新到内存,但是这样会导致线程B已经更新到内存的数据被覆盖,活儿白干了,最终导致有些数据会被重复数很多次。

同步方式

对于多个线程访问共享资源出现数据混乱的问题,需要进行线程同步。常用的线程同步方式有四种:互斥锁、读写锁、条件变量、信号量。所谓的共享资源就是多个线程共同访问的变量,这些变量通常为全局数据区变量或者堆区变量,这些变量对应的共享资源也被称之为临界资源。
通过锁机制能保证临界区代码最多只能同时有一个线程访问,这样并行访问就变为串行访问了。

互斥锁

一般情况下,每一个共享资源对应一个把互斥锁,锁的个数和线程的个数无关。

// 初始化互斥锁
// *******restrict: 是一个关键字, 用来修饰指针, 只有这个关键字修饰的指针可以访问指向的内存地址, 其他指针是不行的********
int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);
// 释放互斥锁资源            
int pthread_mutex_destroy(pthread_mutex_t *mutex);
// 尝试加锁
int pthread_mutex_trylock(pthread_mutex_t *mutex);

互斥锁进行线程同步

加锁和解锁之间放的是临界区,加锁的是临界区的资源

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <pthread.h>#define MAX 50
// 全局变量
int number=0;
pthread_mutex_t mutex;// 线程处理函数
void* funcA_num(void* arg)
{for(int i=0; i<MAX; ++i){pthread_mutex_lock(&mutex);int cur = number;cur++;usleep(10);number = cur;pthread_mutex_unlock(&mutex);printf("Thread A, id = %lu, number = %d\n", pthread_self(), number);}return NULL;
}void* funcB_num(void* arg)
{for(int i=0; i<MAX; ++i){pthread_mutex_lock(&mutex);int cur = number;cur++;number = cur;pthread_mutex_unlock(&mutex);printf("Thread B, id = %lu, number = %d\n", pthread_self(), number);usleep(5);}return NULL;
}int main(int argc, const char* argv[])
{pthread_t p1, p2;pthread_mutex_init(&mutex,NULL);// 创建两个子线程pthread_create(&p1, NULL, funcA_num, NULL);pthread_create(&p2, NULL, funcB_num, NULL);// 阻塞,资源回收pthread_join(p1, NULL);pthread_join(p2, NULL);pthread_mutex_destroy(&mutex);return 0;
}

死锁

加锁之后忘记解锁
重复加锁, 造成死锁
在程序中有多个共享资源, 因此有很多把锁,随意加锁,导致相互被阻塞

读写锁

之所以称其为读写锁,是因为这把锁既可以锁定读操作,也可以锁定写操作
使用读写锁锁定了读操作,需要先解锁才能去锁定写操作,反之亦然
特性:
使用读写锁的读锁锁定了临界区,线程对临界区的访问是并行的,读锁是共享的。
使用读写锁的写锁锁定了临界区,线程对临界区的访问是串行的,写锁是独占的。
使用读写锁分别对两个临界区加了读锁和写锁,两个线程要同时访问者两个临界区,访问写锁临界区的线程继续运行,访问读锁临界区的线程阻塞,因为写锁比读锁的优先级高。 >>>>>>>>>>>>>>>> 场景:读进程大于写进程数的时候

api细说

// 在程序中对读写锁加读锁, 锁定的是读操作
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);//调用这个函数,如果读写锁是打开的,那么加锁成功;**如果读写锁已经锁定了读操作**,**调用这个函数依然可以加锁成功,因为读锁是共享的**;如果读写锁已经锁定了写操作,调用这个函数的线程会被阻塞。// 在程序中对读写锁加写锁, 锁定的是写操作
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
//调用这个函数,如果读写锁是打开的,那么加锁成功;**如果读写锁已经锁定了读操作或者锁定了写操作,调用这个函数的线程会被阻塞。**

读写锁进行线程同步

8个线程操作同一个全局变量,3个线程不定时写同一全局资源,5个线程不定时读同一全局资源。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>// 全局变量
int number = 0;// 定义读写锁
pthread_rwlock_t rwlock;// 写的线程的处理函数
void* writeNum(void* arg)
{while(1){pthread_rwlock_wrlock(&rwlock);int cur = number;cur ++;number = cur;printf("++写操作完毕, number : %d, tid = %ld\n", number, pthread_self());pthread_rwlock_unlock(&rwlock);// 添加sleep目的是要看到多个线程交替工作sleep(rand() % 10);}return NULL;
}// 读线程的处理函数
// 多个线程可以如果处理动作相同, 可以使用相同的处理函数
// 每个线程中的栈资源是独享
void* readNum(void* arg)
{while(1){pthread_rwlock_rdlock(&rwlock);printf("--全局变量number = %d, tid = %ld\n", number, pthread_self());pthread_rwlock_unlock(&rwlock);sleep(rand() % 10);}return NULL;
}int main()
{// 初始化读写锁pthread_rwlock_init(&rwlock, NULL);// 3个写线程, 5个读的线程pthread_t wtid[3];pthread_t rtid[5];for(int i=0; i<3; ++i){pthread_create(&wtid[i], NULL, writeNum, NULL);}for(int i=0; i<5; ++i){pthread_create(&rtid[i], NULL, readNum, NULL);}// 释放资源for(int i=0; i<3; ++i){pthread_join(wtid[i], NULL);}for(int i=0; i<5; ++i){pthread_join(rtid[i], NULL);}// 销毁读写锁pthread_rwlock_destroy(&rwlock);return 0;
}

在这里插入图片描述

条件变量

严格意义上来说,条件变量的主要作用不是处理线程同步, 而是进行线程的阻塞。如果在多线程程序中只使用条件变量无法实现线程的同步, 必须要配合互斥锁来使用。虽然条件变量和互斥锁都能阻塞线程,但是二者的效果是不一样的,二者的区别如下:
条件变量只有在满足指定条件下才会阻塞线程,如果条件不满足,多个线程可以同时进入临界区,同时读写临界资源,这种情况下还是会出现共享资源中数据的混乱。

生产者消费者案例

#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){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);// 生产了任务, 通知消费者消费pthread_cond_broadcast(&cond);// 生产慢一点sleep(rand() % 3);}return NULL;
}
// 消费者的回调函数
void* consumer(void* arg)
{while(1){pthread_mutex_lock(&mutex);// 一直消费, 删除链表中的一个节点while(head == NULL)   // 这样写有bug//   while(head == NULL){// 任务队列, 也就是链表中已经没有节点可以消费了// 消费者线程需要阻塞// 线程加互斥锁成功, 但是线程阻塞在这行代码上, 锁还没解开// 其他线程在访问这把锁的时候也会阻塞, 生产者也会阻塞 ==> 死锁// 这函数会自动将线程拥有的锁解开 >>>>>>>>>>>>>>>>> pthread_cond_wait(&cond, &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);sleep(rand() % 3);}return NULL;
}int main()
{// 初始化条件变量pthread_cond_init(&cond, NULL);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);}// 销毁条件变量pthread_cond_destroy(&cond);pthread_mutex_destroy(&mutex);return 0;
}

问题解答

pthread_cond_wait(&cond, &mutex) 会在执行时释放锁,这是关键所在。
其它被这个消费者互斥锁阻塞的线程再这之后就会开始抢锁
消费者抢锁的操作是再pthread_cond_wait函数内部实现的
案例分析:前一个消费者消费完了链表为空,假设后面还有一个消费者被这个消费者阻塞后,此时生产者生产数据并调用 pthread_cond_broadcast(&cond) 唤醒所有等待的线程,由于前面一个消费者又一次消费完了,那个阻塞的消费者没有进行循环判断就接着消费就会发生段错误。

加强版生产者消费者

生产者设置上限,消费者设置下限,并且用两个条件变量

#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_producer; // 生产者的条件变量
pthread_cond_t cond_consumer; // 消费者的条件变量
// 互斥锁变量
pthread_mutex_t mutex;
// 指向头结点的指针
struct Node * head = NULL;
// 生产者生产的上限
#define MAX_PRODUCE 5
// 生产者生产的计数器
int produced_count = 0;// 生产者的回调函数
void* producer(void* arg)
{while (1) {pthread_mutex_lock(&mutex);// 如果已经生产了 5 个任务,则停止生产if (produced_count >= MAX_PRODUCE) {pthread_mutex_unlock(&mutex);break; // 达到生产上限,退出生产}// 创建一个链表的新节点struct Node* pnew = (struct Node*)malloc(sizeof(struct Node));// 节点初始化pnew->number = rand() % 1000;// 节点的连接, 添加到链表的头部, 新节点就新的头结点pnew->next = head;// head指针前移head = pnew;produced_count++;  // 更新生产计数器printf("+++producer, number = %d, tid = %ld\n", pnew->number, pthread_self());// 生产了任务, 通知消费者消费pthread_cond_signal(&cond_consumer);pthread_mutex_unlock(&mutex);// 生产慢一点sleep(rand() % 3);}return NULL;
}// 消费者的回调函数
void* consumer(void* arg)
{while (1) {pthread_mutex_lock(&mutex);// 一直消费, 删除链表中的一个节点while (head == NULL) {// 如果链表为空, 消费者线程等待生产者的通知pthread_cond_wait(&cond_consumer, &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);sleep(rand() % 3);}return NULL;
}int main()
{// 初始化条件变量pthread_cond_init(&cond_producer, NULL);pthread_cond_init(&cond_consumer, NULL);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);}// 销毁条件变量pthread_cond_destroy(&cond_producer);pthread_cond_destroy(&cond_consumer);pthread_mutex_destroy(&mutex);return 0;
}

总结

不使用条件变量的生产者-消费者模型和使用条件变量的生产者-消费者模型
资源占用:使用条件变量的模型更高效,避免了不必要的 CPU 占用。
延迟:条件变量能更快地响应条件变化,不会因轮询间隔导致延迟。
代码复杂度:使用条件变量需要额外的代码来管理条件的等待和唤醒,但能提高性能。

信号量

信号量用在多线程多任务同步的,一个线程完成了某一个动作就通过信号量告诉别的线程,别的线程再进行某些动作。

信号量实现生产者消费者同步–>一个资源

在这里插入图片描述

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>
#include <semaphore.h>
// 链表的节点
struct Node
{int number;struct Node* next;
};
//信号量
sem_t semp;
sem_t semc;struct Node * head = NULL;void* producer(void* arg)
{while(1){sem_wait(&semp);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(&semc);sleep(rand() % 3);}return NULL;
}void* consumer(void* arg)
{while(1){sem_wait(&semc);struct Node* pnode = head;printf("--consumer: number: %d, tid = %ld\n", pnode->number, pthread_self());head  = pnode->next;free(pnode);sem_post(&semp);sleep(rand() %3);}return NULL;
}
int main()
{//生产者sem_init(&semp,0,1);//资源只有一个,虽然线程很多但是都是被阻塞的//消费者 --> 资源初始化为0 sem_init(&semc,0,0);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(&semp);sem_destroy(&semc);return 0;
}

在这里插入图片描述

信号量实现生产者消费者同步–>多个资源

假设资源数是>1的,此时如果多个生产者同时对链表进行添加数据这样是有问题的!
因此要通过锁来让这些线程线性执行
在这里插入图片描述
上面这种情况消费者是不能够通知消费者去生产的,此时生产者由于消费者都没通知所有生产者也阻塞了,这样的话就没有任何一个线程在工作–>问题大了!

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>
#include <semaphore.h>
// 链表的节点
struct Node
{int number;struct Node* next;
};
//信号量
sem_t semp;
sem_t semc;
pthread_mutex_t mutex;struct Node * head = NULL;void* producer(void* arg)
{while(1){sem_wait(&semp);pthread_mutex_lock(&mutex);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());pthread_mutex_lock(&mutex);sem_post(&semc);sleep(rand() % 3);}return NULL;
}void* consumer(void* arg)
{while(1){sem_wait(&semc);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_lock(&mutex);sem_post(&semp);sleep(rand() %3);}return NULL;
}int main()
{//生产者sem_init(&semp,0,5);//资源只有一个,虽然线程很多但是都是被阻塞的//消费者 --> 资源初始化为0 sem_init(&semc,0,0);pthread_mutex_init(&mutex, NULL);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(&semp);sem_destroy(&semc);pthread_mutex_destroy(&mutex);return 0;
}

在这里插入图片描述
在生产者-消费者模型中会出现“解锁和通知消费者之间有生产者加锁”的情况

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/469505.html

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

相关文章

WSL 2 中 FastReport 与 FastCube 的设置方法与优化策略

软件开发人员长期以来一直在思考这个问题&#xff1a;“我们如何才能直接在 Windows 中运行 Linux 应用程序&#xff0c;而无需使用单独的虚拟机&#xff1f;” WSL 技术为这个问题提供了一个可能的答案。WSL 的历史始于 2016 年。当时&#xff0c;其实现涉及使用 Windows 内核…

【前端】HTML标签汇总

目录 展示用户信息的标签 1.文本标签 span 2.标题标签 h1~h6 3.竖着布局的标签 div 4.段落标签 p 5.超链接标签 a 5.1跳转至网上的资源 5.2锚点 6.列表标签 6.1有序列表 ol 6.2无序列表 ul 7.图片标签 img 7.1相对路径 7.1.1兄弟关系 7.1.2叔侄关系 7.1.3表兄弟…

海外云手机在出海业务中的优势有哪些?

随着互联网技术的快速发展&#xff0c;海外云手机已在出海电商、海外媒体推广和游戏行业都拥有广泛的应用。对于国内的出海电商企业来说&#xff0c;短视频引流和社交平台推广是带来有效流量的重要手段。借助云手机&#xff0c;企业能够更高效地在新兴社交平台上推广产品和品牌…

电脑提示xinput1_3.dll丢失怎么办?游戏DLL修复方法详解

xinput1_3.dll 是一个动态链接库&#xff08;DLL&#xff09;文件&#xff0c;它在Windows操作系统中扮演着重要的角色&#xff0c;特别是在处理游戏控制器和其他输入设备的交互方面。这个文件是Microsoft DirectX软件包的一部分&#xff0c;DirectX是微软公司开发的一个多媒体…

Spring资源加载模块,原来XML就这,活该被注解踩在脚下 手写Spring第六篇了

这一篇让我想起来学习 Spring 的时&#xff0c;被 XML 支配的恐惧。明明是写Java&#xff0c;为啥要搞个XML呢&#xff1f;大佬们永远不知道&#xff0c;我认为最难的是 XML 头&#xff0c;但凡 Spring 用 JSON来做配置文件&#xff0c;Java 界都有可能再诞生一个扛把子。 <…

讲讲关于SNMP与智能PDU插座

什么是SNMP 简单网络管理协议 (SNMP) 是一种应用层协议&#xff0c;主要用于网络管理中的设备监控和控制。通过 SNMP&#xff0c;网络管理员可以从管理站远程访问网络中的设备&#xff0c;获取设备的状态信息、配置参数&#xff0c;甚至控制设备的行为。SNMP 被广泛应用于 TCP/…

丹摩征文活动 | Kolors入门:从安装到全面活用的对比指南

文章目录 1 图像生成模型 Kolors2 部署流程3 部署服务3.1 安装 Anaconda3.2 Kolors 库下载3.3 创建虚拟环境 4 生成图片 1 图像生成模型 Kolors Kolors是由快手团队开发的大规模文本到图像生成模型&#xff0c;以其独特的潜在扩散技术而闻名。 Kolors通过在数十亿对文本和图像…

【go从零单排】通道select、通道timeout、Non-Blocking Channel Operations非阻塞通道操作

&#x1f308;Don’t worry , just coding! 内耗与overthinking只会削弱你的精力&#xff0c;虚度你的光阴&#xff0c;每天迈出一小步&#xff0c;回头时发现已经走了很远。 &#x1f4d7;概念 select 语句是 Go 的一种控制结构&#xff0c;用于等待多个通道操作。它类似于 s…

信息安全工程师(83)Windows操作系统安全分析与防护

一、Windows操作系统安全分析 系统漏洞&#xff1a; Windows操作系统由于其复杂性和广泛使用&#xff0c;可能存在一些已知或未知的漏洞。这些漏洞可能会被黑客利用&#xff0c;进行恶意攻击。微软会定期发布系统更新和补丁&#xff0c;以修复这些漏洞&#xff0c;提高系统的安…

计算机网络常见面试题(一):TCP/IP五层模型、TCP三次握手、四次挥手,TCP传输可靠性保障、ARQ协议

文章目录 一、TCP/IP五层模型&#xff08;重要&#xff09;二、应用层常见的协议三、TCP与UDP3.1 TCP、UDP的区别&#xff08;重要&#xff09;3.2 运行于TCP、UDP上的协议3.3 TCP的三次握手、四次挥手3.3.1 TCP的三次握手3.3.2 TCP的四次挥手3.3.3 随机生成序列号的原因 四、T…

BFD8122防爆轻便移动工作灯

BFD8122防爆轻便移动工作灯 适用范围&#xff1a; 适用于炼油、化工、油田等易燃易爆场所小范围施工、检修、抢险应急照明。 结构特性 高亮度&#xff0c;灯具光通量&#xff1e;4000lm&#xff0c;6米中心照度&#xff08;聚光&#xff09;&#xff1e;1000lx&#xff0c;…

天地图入门|标注|移动飞行|缩放,商用地图替换

“天地图”是国家测绘地理信息局建设的地理信息综合服务网站。集成了来自国家、省、市&#xff08;县&#xff09;各级测绘地理信息部门&#xff0c;以及相关政府部门、企事业单位 、社会团体、公众的地理信息公共服务资源&#xff0c;如果做的项目是政府部门、企事业单位尽量选…

分布式----Ceph部署(上)

目录 一、存储基础 1.1 单机存储设备 1.2 单机存储的问题 1.3 商业存储解决方案 1.4 分布式存储&#xff08;软件定义的存储 SDS&#xff09; 1.5 分布式存储的类型 二、Ceph 简介 三、Ceph 优势 四、Ceph 架构 五、Ceph 核心组件 #Pool中数据保存方式支持两种类型&…

linux详解,基本网络枚举

基本网络枚举 一、基本网络工具 ifconfig ifconfig是一个用于配置和显示网络接口信息的命令行工具。它可以显示网络接口的P地址、子网掩码、MC地址等信息&#xff0c;还可以用于启动、停止或配置网络接口。 ip ip也是用于查看和管理网络接口的命令。 它提供了比ifconfig更…

组件间通信(组件间传递数据)

组件间通信(组件间传递数据) 在 Vue.js 中&#xff0c;组件间通信是开发者需要经常处理的任务&#xff0c;特别是在构建具有多层次组件的复杂应用时。根据组件之间的关系和数据流的复杂程度&#xff0c;可以采用不同的通信方式。以下是常用的几种组件间通信方式&#xff1a; …

深度学习-图像评分实验(TensorFlow框架运用、读取处理图片、模型建构)

目录 0、实验准备 ①实验环境 ②需要下载的安装包 ③注意事项&#xff08;很关键&#xff0c;否则后面内容看不懂&#xff09; ④容易出现的问题 1、查看数据并读取数据。 2、PIL库里的Image包进行读取&#xff08;.resize更改图片尺寸&#xff0c;并将原始数据归一化处…

全球碳循环数据集(2000-2023)包括总初级生产力、生态系统净碳交换和生态系统呼吸变量

全球碳循环数据集&#xff08;2000-2023&#xff09; 数据介绍 PFTs_XGB FLUX 是一个基于 XGBOOST 机器学习模型的全球碳循环数据集。该数据集通过对全球植被功能类型&#xff08;PFTs&#xff09;的分类&#xff0c;结合了 FLUXNET、AmeriFlux 和 ICOS 通量站点的现场观测数据…

前端代码分析题(选择题、分析题)——this指向、原型链分析

this指向 普通函数&#xff1a;this 的指向由调用方式决定&#xff0c;可以是全局对象、调用该函数的对象&#xff0c;或者显式指定的对象。箭头函数&#xff1a;this 的指向在定义时确定&#xff0c;始终继承自外层函数作用域的 this&#xff0c;不会被调用方式影响。 var obj…

【SpringBoot】18 上传文件到数据库(Thymeleaf + MySQL)

Git仓库 https://gitee.com/Lin_DH/system 介绍 使用 Thymeleaf 写的页面&#xff0c;将&#xff08;txt、jpg、png&#xff09;格式文件上传到 MySQL 数据库中。 依赖 pom.xml <!-- https://mvnrepository.com/artifact/com.mysql/mysql-connector-j --><depende…

手动搭建 Ghost 博客

操作场景 Ghost 是使用 Node.js 语言编写的开源博客平台&#xff0c;您可使用 Ghost 快速搭建博客&#xff0c;简化在线出版过程。本文档介绍如何在腾讯云云服务器&#xff08;CVM&#xff09;上手动搭建 Ghost 个人网站。 进行 Ghost 网站搭建&#xff0c;您需要熟悉 Linux …