多线程与线程同步(锁)
明明只有八核为什么能够同时运行100个程序?
实际上CPU会把一个单位事件分为多份,称之为CPU时间片;每个线程在运行时都需要抢占CPU时间片去运行。抢到为运行态,未抢到未就绪态。
线程概念
线程没有主次之分,我们称main函数处在的为主线程是便于区分;
在Linux内核看来只有进程而没有线程。线程是轻量级的进程(LWP:light weight process),在 Linux 环境下线程的本质仍是进程。
线程和进程之间的区别:
-
进程有自己独立的地址空间,多个线程共用同一个地址空间;
每启动一个进程都会有一个4G的虚拟内存空间;而是线程是公用同一个4G虚拟内存空间。
1)、线程更加节省系统资源,效率不仅可以保持的,而且能够更高。
2)、在一个地址空间中多个线程独享:每个线程都有属于自己的栈区(存放临时变量的区域),寄存器 (CPU内的资源,内核中管理的);
3)、在一个地址空间中多个线程共享:代码段,堆区(动态分配),全局数据区,打开的文件 (文件描述符表) 都是线程共享的。 -
线程是程序的最小执行单位,进程是操作系统中最小的资源分配单位
1)、每个进程对应一个虚拟地址空间,一个进程只能抢一个 CPU 时间片;
2)、一个地址空间中可以划分出多个线程,在有效的资源基础上,能够抢更多的 CPU 时间片。
-
CPU 的调度和切换:线程的上下文切换比进程要快的多
上下文切换:进程 / 线程分时复用 CPU 时间片,在切换之前会将上一个任务的状态进行保存(存储到寄存器中),下次切换回这个任务的时候,加载这个状态继续运行,任务从保存到再次加载这个过程就是一次上下文切换。 -
线程更加廉价,启动速度更快,退出也快,对系统资源的冲击小。
线程栈
线程栈的双重特性
- 线程的栈是每个线程独有的,保存其 运行状态和局部自动变量;
- 栈在线程开始的时候初始化,每个线程的栈相互独立,因此栈是thread safe 的。
但是线程的栈所属内存空间又是属于整个进程,这就是双重特性。
进程是资源单位,线程是调度单位
栈的 guard page
每个线程的栈会存在Guard page 保护页, 这种保护页是没有权限的。当一个线程进入了另一个线程的guardpage, 出现page falult 则程序崩溃。
线程函数调用的越深,用的临时变量越多,则栈的空间越大。飞到guard page时,直接page fault。
使得不会篡改别的线程的数据。
线程和进程的选择
1、如果程序a需要启动磁盘上的另外一个应用程序b的话,建议使用多进程;
2、一般情况下使用多线程;
3、如何控制线程个数?
CPU 使用率不高:可以分时复用 CPU 时间片,线程的个数 = 2 * CPU 核心数 (效率最高)。如IO操作等。
处理复杂的算法 :主要是 CPU 进行运算,压力大,线程的个数 = CPU 的核心数 (效率最高)。
线程函数
线程创建
每一个线程都有一个唯一的线程 ID,ID 类型为 pthread_t,这个 ID 是一个无符号长整形数,如果想要得到当前线程的线程 ID,可以调用如下函数:
pthread_t pthread_self(void); // 返回当前线程的线程ID
在一个进程中调用线程创建函数,就可得到一个子线程,和进程不同,需要给每一个创建出的线程指定一个处理函数,否则这个线程无法工作。
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
// Compile and link with -pthread, 线程库的名字叫pthread, 全名: libpthread.so libptread.a
- thread: 传出参数,是无符号长整形数,线程创建成功,会将线程 ID 写入到这个指针指向的内存中;
- attr: 线程的属性,一般情况下使用默认属性即可,写 NULL;
- start_routine: 函数指针,回调作用,创建出的子线程的处理动作,也就是该函数在子线程中执行;
- arg: 作为实参传递到 start_routine 指针指向的函数内部;建议通过自己设计一个结构体,通过往结构体里塞东西实现传递多个值;
- 返回值:线程创建成功返回 0,创建失败返回对应的错误号。
当报错缺少库的时候,需要手动连接库:
$ gcc pthread_create.c -lpthread
回调函数
回调函数满足这种格式:void* XXX(void* arg)
void* working(void* arg)
{
sleep(1);
printf("我是子线程, 线程ID: %ld\n", pthread_self());
for(int i=0; i<9; ++i)
{
if(i==6)
{
pthread_exit(NULL); // 直接退出子线程
}
printf("child == i: = %d\n", i);
}
return NULL;
}
需要注意:
如果主线程早于子线程执行完毕,那么子线程也会随着主线程一起死亡;
子线程被创建出来之后需要抢cpu时间片, 抢不到就不能运行,如果主线程退出了, 虚拟地址空间就被释放了, 子线程就一并被销毁了。但是如果某一个子线程退出了, 主线程仍在运行, 虚拟地址空间依旧存在。
线程退出
线程不分主次,任何的return都会导致整体的结束(整个内存空间的释放)。
在编写多线程程序的时候,如果想要让主线程退出,但不释放虚拟空间,我们就可以调用线程库中的线程退出函数,只要调用该函数当前线程就马上退出了,并且不会影响到其他线程(主线程通过pthread_exit,不会影响子进程)的正常运行,不管是在子线程或者主线程中都可以使用。
#include <pthread.h>
void pthread_exit(void *retval);
注意若不回收,会导致僵尸进程; 传出的是数据的指针 。
线程退出的时候携带的数据,当前子线程的主线程会得到该数据。如果不需要使用,指定为 NULL。
线程回收
主线程回收子线程资源(栈空间)
主线程重要任务是释放内核区资源
#include <pthread.h>
// 这是一个阻塞函数, 子线程在运行这个函数就阻塞
// 子线程退出, 函数解除阻塞, 回收对应的子线程资源, 类似于回收进程使用的函数 wait()
int pthread_join(pthread_t thread, void **retval);
只要子线程不退出,pthread_join一直阻塞;调用一次pthread_join只能等一个子线程结束;所以有时候需要循环等待子线程退出。
参数:
- thread: 要被回收的子线程的线程 ID
- retval: 二级指针,指向 一级指针的地址,是一个传出参数,这个地址中存储了 pthread_exit ()
传递出的数据,如果不需要这个参数,可以指定为 NULL
返回值:线程回收成功返回 0,回收失败返回错误号。
线程退出的栗子
struct Return_Value{}
void *callback(void *arg)
{
.......
static Return_Value ret;
thread_exit(&ret);
}
int main()
{
void * retval;
pthread_join(tid,&retval);
Return_Value *ret = (struct Return_Value )retval;
}
子线程将数据通过ret创出,主线程使用retval接受;
注意:
- 需要注意的是子线程结束后,线程栈被释放;所有传出参数不能放在线程的栈里;
- 所以数据可以放在堆区、主线程的栈空间或者其他数据段;
- 可以设计为static、全局,或者使用主线程的栈空间。
线程分离
一般情况下主线程退出时会释放子线程的资源,如果线程分离之后主线程退出那么就不会释放子线程的资源。
因为当使用pthread_join回收子线程资源时主线程会阻塞,子线程资源自动回收其占用的内核资源就被系统的其他进程接管并回收了。线程分离之后在主线程中使用 pthread_join() 就回收不到子线程资源了。
- 主线程退出不释放子线程资源;
- 主线程不回收子线程资源。
#include <pthread.h>
// 参数就子线程的线程ID, 主线程就可以和这个子线程分离了
int pthread_detach(pthread_t thread);
注意:pthread_detach于主线程中调用。
其他线程函数
线程取消
#include <pthread.h>
// 参数是子线程的线程ID
int pthread_cancel(pthread_t thread);
注意:
子线程不会立刻结束,当直接或间接使用系统调用才会被杀死。
线程同步
为了实现对数据的保护
假设有 4 个线程 A、B、C、D,当前一个线程 A 对内存中的共享资源进行访问的时候,其他线程 B, C, D 都不可以对这块内存进行操作,直到线程 A 对这块内存访问完毕为止,B,C,D 中的一个才能访问这块内存,剩余的两个需要继续阻塞等待,以此类推,直至所有的线程都对这块内存操作完毕。 线程对内存的这种访问方式就称之为线程同步,通过对概念的介绍,我们可以了解到所谓的同步并不是多个线程同时对内存进行访问,而是按照先后顺序依次进行的。
主要问题是,不做同步下可能使得缓存里的数据不一定来得及写入到内存中去。
在干某一件事的时候,让多个线程按照一定的顺序依次执行。
同步方式
就是加锁
互斥锁
互斥:只能被一个线程拥有
锁的原理:
pthread_mutex_lock(&mutex)本质上是一个阻塞函数;只有当线程可以取得线程锁时才不阻塞,这时才有权去抢占CPU时间片。 线程阻塞强制挂起,无法获得CPU时间片。
pthread_mutex_t mutex;
// 初始化互斥锁
锁的初始化有两种:
1、设置为全局变量,这样的话所有线程公用一把锁;
2、设置为局部的,那只能在该锁所在域使用。
// restrict: 是一个关键字, 用来修饰指针, 只有这个关键字修饰的指针可以访问指向的内存地址, 其他指针是不行的
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);
// 释放互斥锁资源
int pthread_mutex_destroy(pthread_mutex_t *mutex);
关键字:restrict ;使得有且只能通过mutex这个指针去访问。
// 修改互斥锁的状态, 将其设定为锁定状态, 这个状态被写入到参数 mutex 中
int pthread_mutex_lock(pthread_mutex_t *mutex);
这个函数被调用,首先会判断参数 mutex 互斥锁中的状态是不是锁定状态:
- 没有被锁定,是打开的,这个线程可以加锁成功,这个这个锁中会记录是哪个线程加锁成功了;
- 如果被锁定了,其他线程加锁就失败了,这些线程都会阻塞在这把锁上;
- 当这把锁被解开之后,这些阻塞在锁上的线程就解除阻塞了,并且这些线程是通过竞争的方式对这把锁加锁,没抢到锁的线程继续阻塞。
// 尝试加锁
int pthread_mutex_trylock(pthread_mutex_t *mutex);
尝试加锁,如果能加的上就加,加不到就不阻塞直接返回错误号,执行别的任务。
调用这个函数对互斥锁变量加锁还是有两种情况:
- 如果这把锁没有被锁定是打开的,线程加锁成功;
- 如果锁变量被锁住了,调用这个函数加锁的线程,不会被阻塞,加锁失败直接返回错误号;
死锁
1、注意不要重复加锁;
2、多个临界资源一般就有多少把锁,不看线程的个数;
如图是一种无解的形式:
A和B锁了各自资源,但是去访问别人的资源,那就会一直阻塞。
善用pthread_mutex_trylock上锁,避免死锁。
访问多个资源时,每一个线程尽量以相同的顺序去访问资源。
读写锁
这是一把锁,可以实现两种操作,锁定读和写。
使用读写锁锁定了读操作,需要先解锁才能去锁定写操作;一把锁不能同时锁定读操作又锁定写操作
pthread_rwlock_t rwlock;
因为通过一把读写锁可以锁定读或者写操作,下面介绍一下关于读写锁的特点:
- 使用读写锁的读锁锁定了临界区,线程对临界区的访问是并行的,读锁是共享的。
- 使用读写锁的写锁锁定了临界区,线程对临界区的访问是串行的,写锁是独占的。
- 使用读写锁分别对两个临界区加了读锁和写锁,两个线程要同时访问者两个临界区,访问写锁临界区的线程继续运行,访问读锁临界区的线程阻塞,因为写锁比读锁的优先级高。
注意:
1、读锁:允许多个线程同时读取一块资源;
2、写锁:有且仅有一个线程可以写一块资源;
3、多个线程同时读写:写的优先级高于读,写操作的线程优先执行写(如果有多个线程需要进行竞争);所有写任务完成后允许读。
#include <pthread.h>
pthread_rwlock_t rwlock;
// 初始化读写锁
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);
条件变量
虽然条件变量和互斥锁都能阻塞线程,但是二者的效果是不一样的,二者的区别如下:
- 假设有 A-Z 26 个线程,这 26 个线程共同访问同一把互斥锁,如果线程 A 加锁成功,那么其余 B-Z 线程访问互斥锁都阻塞,所有的线程只能依次访问临界区。
- 条件变量只有在满足指定条件下才会阻塞线程,如果条件不满足,允许多个线程同时进入临界区,同时读写临界资源,这种情况下还是会出现共享资源中数据的混乱。
- 所以一般和互斥锁联合使用。
定义
#include <pthread.h>
pthread_cond_t cond;
// 初始化
int pthread_cond_init(pthread_cond_t *restrict cond,
const pthread_condattr_t *restrict attr);
// 销毁释放资源
int pthread_cond_destroy(pthread_cond_t *cond);
参数:
- cond: 条件变量的地址
- attr: 条件变量属性,一般使用默认属性,指定为 NULL
cond内部包含着那些被阻塞线程的信息
调用 pthread_cond_wait
只要调用它,就被这个condition阻塞;因此cond包含着所有因他而阻塞的线程信息。
// 线程阻塞函数, 哪个线程调用这个函数, 哪个线程就会被阻塞
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
用于人为干预,阻塞当前线程;该函数于线程中被调用。
互斥锁mutex的作用:
- 在阻塞线程时候,如果线程已经对互斥锁 mutex (函数中的锁和线程中的必须是同一把锁)上锁,那么会将这把锁打开,这样做是为了避免死锁;
- 当线程解除阻塞的时候,函数内部会帮助这个线程将这个 mutex 互斥锁锁上,继续向下访问临界区。
// 表示的时间是从1971.1.1到某个时间点的时间, 总长度使用秒/纳秒表示
struct timespec {
time_t tv_sec; /* Seconds */
long tv_nsec; /* Nanoseconds [0 .. 999999999] */
};
// 将线程阻塞一定的时间长度, 时间到达之后, 线程就解除阻塞了
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);
唤醒之后会尝试加锁,如果加锁失败任然继续阻塞。
如果唤醒多个线程,多个线程会去抢锁,没能抢到的会重新阻塞,等待下次唤醒。
举个栗子
使用链表去存储生产者和消费者;生产者从头往后压入,消费者从尾往前删。
头文件,条件变量,互斥锁,链表初等基本要素:
#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;
主函数:
负责初始化和创建线程,5个生产者和5个消费者。
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;
}
生产者的回调函数:
void* producer(void* arg)
{
// 一直生产
while(1)
{
pthread_mutex_lock(&mutex);
// 创建一个链表的新节点
struct Node* pnew = (struct Node*)malloc(sizeof(struct Node));
// 节点初始化,随机设置number的值
pnew->number = rand() % 1000;
//将pnew插入头部,将pnew作为新的头,pnew的last为旧的头。
pnew->next = 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;
}
链表部分:
1、在所有之前存在 全局变量头部指针head ,值为NULL(表示到达尾部);
2、每次malloc分配新的链表成员pnew(在堆上,所有线程都可以通过指针访问);
3、将之前旧的head作为pnew下一个元素,而pnew成为新的head。
生产部分:
1、无论生产还是消费操作的是同一个链表,所以必须加锁;
2、pthread_cond_broadcast(&cond)唤醒所有被阻塞的消费者;
3、生产过程时不要阻塞的,只要拿到锁就可以生产;如果有其他判定需要也可以调用pthread_cond_wait(),但是由于判断条件不同且是相互唤醒,所以需要使用 完全不同的条件变量。
消费者的回调函数:
void* consumer(void* arg)
{
while(1)
{
pthread_mutex_lock(&mutex);
// 一直消费, 删除链表中的一个节点
while(head == NULL)
{
pthread_cond_wait(&cond, &mutex);
}
struct Node* pnode = head;
printf("--consumer: number: %d, tid = %ld\n", pnode->number, pthread_self());
//让pnode的下一个作为head,从而删除原来的头
head = pnode->next;
//需要把旧的head释放掉,正好struct Node* pnode = head;
free(pnode);
pthread_mutex_unlock(&mutex);
sleep(rand() % 3);
}
return NULL;
}
链表部分:
1、从头部取出数据,并从头部删除数据;
2、删除就是让旧head中的last成为新的head;但是记得释放旧head(pnode);
3、链表、head是被修改的全局变量,所以也需要上锁;
while(head == NULL)而不使用if的原因:
这是因为任何一个线程被唤醒竞争锁后,任然需要判断一下链表是否有数据。
内存释放:
释放一块内存时通过指向这块内存的 指针 实现的;正好struct Node* pnode = head 所以可以free(pnode)来释放旧的head;而且释放的时pnode 但pnode->next没有被释放,且被提前存起来了 head = pnode->next,所以pnode->next(新head)可以正常访问。
线程阻塞部分:
1、在此之前已经上锁;
2、while(head == NULL)判断为真当前五数据可读,调用pthread_mutex_unlock(&mutex)阻塞该线程;
3、由于pthread_mutex_unlock的特性,即使之前mutex已经上锁也会被自动解锁;而当被pthread_cond_broadcast唤醒时又会自动加上锁;
信号量
信号量主要负责通知这个线程是接着运行,还是要阻塞等待。但不能保证线程的安全,所以一般配合互斥锁。
如何判断这个线程是否该阻塞?
通过初始化的信号量进行增减,去判断是否该阻塞。信号量实际上时一个计数器,增减表示当前还有多少资源可用。
两种形式
存在两种形式:struct semaphore和sem_t sem
struct semaphore sem
操作函数:PV操作
struct semaphore {
spinlock_t lock;
unsigned int count;
struct list_head wait_list;
};
#include <linux/semaphore.h>
struct semaphore sem;
//当然,初始化也可以为2,3,4等等
sema_init(&sem,1);
struct semaphore成员含有一个等待队列。
P操作:down_
如果有一个任务想要获得已经被占用的信号量时,信号量会将其放入一个等待队列然后让其睡眠。
down_interruptible()
这个函数的功能就是获得信号量,如果得不到信号量就睡眠,此时没有信号打断,那么进入睡眠。
down_killable()
用来获取信号量,将信号量sem的计数器值减1,但它是可被致命信号杀死的,这一点与down()函数不同,down()不能被任何信号打断,也与down_interruptible()函数不同,down_interruptible()可被一般信号中断。当有另外的内核控制路径给这个因为竞争不到信号量而睡眠的进程发送了一个致命信号时(一般信号将不会响应),它收到信号后就会立即返回,而放弃继续获得信号量。
down_trylock()
即调用该函数的进程在不能获取信号量的情况下会立即返回,不会睡眠。
注意:
比如在使用down(),以阻塞形式且不接受外部信号时;如果输入外部信号会被存起来,当成功获取信号量时,优先执行外部信号再执行下面命令。
外部信号输入:Ctrl+c、Ctrl+z
V操作:up
当持有信号量的进程将信号释放后,处于等待队列中的一个任务将被唤醒(因为队列中可能不止一个任务),并让其获得信号量 。
信号量往往对应可使用资源的个数。
举个栗子
#include <linux/semaphore.h >
struct semaphore sem ;
//当然,初始化也可以为2,3,4等等
sema_init(&sem,1);
int hello_open (struct inode inode,struct file * filp )
{
if(down_interruptible(&sem))
return -ERESTARTSYS;
return 0 ;
}
int hello_release (struct inode inode,struct file * filp)
{
up(&sem)
return 0 ;
}
某一个线程执行到down_interruptible(&sem)时,信号量-1,如果此时>=0,该线程接着执行;如果<0,该线程阻塞并且进入等待队列排队。
某一个线程执行到up(&sem),信号量+1,这个时候如果等待队列有等待的,那么就会被唤醒,唤醒顺序是先进入等待队列的先唤醒。
sem_t sem
操作函数
#include <semaphore.h>
// 初始化信号量/信号灯
int sem_init(sem_t *sem, int pshared, unsigned int value);
// 资源释放, 线程销毁之后调用这个函数即可
// 参数 sem 就是 sem_init() 的第一个参数
int sem_destroy(sem_t *sem);
参数:
- sem:信号量变量地址
- pshared:
0:线程同步
非 0:进程同步
value:初始化当前信号量拥有的资源数(>=0),如果资源数为 0,线程就会被阻塞了。
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,如果资源数<0分别导致阻塞、不阻塞和延时阻塞。
int sem_post(sem_t *sem);
资源数+1,如果有并唤醒阻塞的线程。
int sem_getvalue(sem_t *sem, int *sval);
通过这个函数可以查看 sem 中现在拥有的资源个数,通过第二个参数 sval 将数据传出,也就是说第二个参数的作用和返回值是一样的。
双信号量控制
在生产者和消费者模式中,生产者和消费者相互通知对方,将对方的资源数+1;所以需要有两个信号量。
主函数
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <semaphore.h>
#include <pthread.h>
struct semaphore psem;
sem_t csem;
struct Node
{
int number;
struct Node* next;
};
pthread_mutex_t mutex;
// 设置为全局是为了所有的线程都可以调用
struct Node * head = NULL;
int main()
{
// 初始化条件变量
pthread_cond_init(&cond, NULL);
pthread_mutex_init(&mutex, NULL);
sema_init(&psem,5);
sem_init(&csem,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(sem_t *psem);
sem_destroy(sem_t *csem);
pthread_mutex_destroy(&mutex);
return 0;
}
生产者函数
void* producer(void* arg)
{
// 一直生产
while(1)
{
down_interruptible(&psem);
pthread_mutex_lock(&mutex);
// 创建一个链表的新节点
struct Node* pnew = (struct Node*)malloc(sizeof(struct Node));
// 节点初始化,随机设置number的值
pnew->number = rand() % 1000;
//将pnew插入头部,将pnew作为新的头,pnew的last为旧的头。
pnew->next = head;
head = pnew;
printf("+++producer, number = %d, tid = %ld\n", pnew->number, pthread_self());
pthread_mutex_unlock(&mutex);
//将消费者的资源数加1
int sem_post(sem_t *csem);
// 生产慢一点
sleep(rand() % 3);
}
return NULL;
}
消费者函数
void* consumer(void* arg)
{
while(1)
{
int sem_wait(sem_t *csem);
pthread_mutex_lock(&mutex);
struct Node* pnode = head;
printf("--consumer: number: %d, tid = %ld\n", pnode->number, pthread_self());
//让pnode的下一个作为head,从而删除原来的头
head = pnode->next;
//需要把旧的head释放掉,正好struct Node* pnode = head;
free(pnode);
pthread_mutex_unlock(&mutex);
//将生产者的资源数加1
up(&psem);
sleep(rand() % 3);
}
return NULL;
}
信号量和条件变量的区别
1、条件变量的特点:
- pthread_cond_wait会立刻阻塞调用这个函数的线程;等待由pthread_cond_signal或者pthread_cond_broadcast唤醒;
- 它需要配合互斥锁,且不能实现计数;
- pthread_cond_wait自动加解锁:在阻塞时会自动解开之前上的锁,在解除阻塞时会自动上锁;所以条件变量的调用时在加锁之后;
- 先加锁,再使用pthread_cond_wait;
- 条件变量就是阻塞实现的媒介,是一个标志,自身没有什么判断功能;它的 **阻塞和解除阻塞**全部依赖于pthread_cond_wait和pthread_cond_signal。
2、信号量的特点:
- 可以实现计数功能,可以 相互修改资源数。
区别:
- 条件变量 调用既阻塞;
- 信号量资源数 <0才阻塞。
- 条件变量唤醒之后竞争上岗;
- 信号量唤醒后按等待队列顺序上岗。
- 条件变量唤醒函数依赖互斥锁,唤醒的过程需要抢锁;
- 信号量唤醒 依赖资源数 。
- 由于pthread_cond_wait会自动解锁,所以 加锁可以是在条件变量之前;
- 由于up不会自动解锁,所以 加锁必须在在信号量之后。