目录
互斥锁
互斥锁接口:
pthread_mutex_t // 互斥量的类型
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
- 功能:初始化互斥量
- 参数 :
- mutex : 需要初始化的互斥量变量
- attr : 互斥量相关的属性,NULL(一般使用默认)
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);
- 解锁
案例描述,使用多线程实现用户买票问题: 有3个窗口同时卖票,一共是100张票,卖完即止。
int tickets = 100;
void * mysellfunc(){
while (tickets >0)
{
usleep(6000);
printf("%ld 正在卖第 %d 张门票\n" , pthread_self() , tickets);
tickets--;
}
return NULL;
}
int main(){
pthread_t tid1 , tid2 ,tid3;
pthread_create(&tid1 , NULL , mysellfunc , NULL);
pthread_create(&tid2 , NULL , mysellfunc , NULL);
pthread_create(&tid3 , NULL , mysellfunc , NULL);
pthread_detach(tid1);
pthread_detach(tid2);
pthread_detach(tid3);
pthread_exit(NULL);
return 0;
}
显然,没有进行线程同步的情况下,会出现非常混乱的情况:
实现方面,显然,临界区(线程共享变量)即各线程对票数的售卖操作,因此对此操作需要加锁:
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
int tickets = 1000;
pthread_mutex_t mutex; //创建互斥量类型
void * mysellfunc(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); // 初始化互斥量
pthread_t tid1 , tid2 ,tid3;
pthread_create(&tid1 , NULL , mysellfunc , NULL);
pthread_create(&tid2 , NULL , mysellfunc , NULL);
pthread_create(&tid3 , NULL , mysellfunc , NULL);
pthread_detach(tid1);
pthread_detach(tid2);
pthread_detach(tid3);
pthread_exit(NULL);
pthread_mutex_destroy(&mutex); // 释放互斥量
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);
- 等待函数,调用了该函数,线程会阻塞在这里。
- 注意一下,当运行到该函数并处于阻塞状态时,会先对持有的互斥锁进行解锁,从而不影响其他线程拿互斥锁。当不阻塞时(也就是被sinal函数唤醒后),会重新加锁,继续向下执行。
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);
- 唤醒所有的等待的线程
先看看什么是生产者消费者模型:
上图表明生产者生产满了以后,要通知消费者去消费;同样的,消费者消费完后 ,也会通知生产者去生产, 如果需要实现以上的机制,就可以用到条件变量这种线程同步的机制。
直接上实例:
1、先创建五个生产者线程,五个消费者线程,并进行互斥锁和条件变量的初始化:
struct ListNode{
int val;
struct ListNode *next;
}*head;
head = NULL;
int main(){
pthread_cond_init(&cond , NULL);
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 , producerfunc , NULL);
pthread_create(&ctids[i] , NULL , customerfunc , NULL);
}
for(int i = 0 ; i<5 ;i++)
{
pthread_detach(ptids[i]);
pthread_detach(ctids[i]);
}
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond);
pthread_exit(NULL);
return 0;
}
2、以链表结点作为线程生产和消费的对象,
生产者线程函数生产了一个结点就调用signal函数通知消费者线程进行消费(也就是删除结点):
void * producerfunc(void * arg){
while (1) //生产者不断生产结点
{
pthread_mutex_lock(&mutex);
struct ListNode *newNode = (struct ListNode*)malloc(sizeof(struct ListNode));
newNode ->next = head;
head = newNode;
newNode ->val = rand()%1000;
printf("add Node, num: %d , tid : %ld\n" , newNode->val , pthread_self());
//只要生产了一个就通知 消费者消费(signal函数)
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);
usleep(100);
}
return NULL;
}
消费者线程函数:
void * customerfunc(void * arg){ //消费者不断的消费结点(删除)
while (1)
{
pthread_mutex_lock(&mutex);
struct ListNode *cur = head;
if(head != NULL) //判断是否有数据 (不等于空表示有数据,通知消费者去消费数据)
{
head = head ->next;
printf("delete node num: %d , tid: %ld\n", cur->val, pthread_self());
free(cur);
pthread_mutex_unlock(&mutex);
usleep(100);
}
else
{
//没有数据需要等待(wait函数)
pthread_cond_wait(&cond, &mutex);
//该函数有两个参数,第二个参数即需要配合使用的锁,这个锁用来解开唤醒这个条件变量的所控制的线程。
//补充一下,当这个函数调用阻塞的时候,会对互斥锁进行解锁,当不阻塞的时候,继续向下执行,会重新加锁。
pthread_mutex_unlock(&mutex);
}
}
return NULL;
}
完整代码:
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
/创建条件变量
pthread_cond_t cond;
pthread_mutex_t mutex;
struct ListNode{
int val;
struct ListNode *next;
};
struct ListNode *head = NULL;
void * producerfunc(void * arg){ //生产者不断的生产结点
while (1)
{
pthread_mutex_lock(&mutex);
struct ListNode *newNode = (struct ListNode*)malloc(sizeof(struct ListNode));
newNode ->next = head;
head = newNode;
newNode ->val = rand()%1000;
printf("add Node, num: %d , tid : %ld\n" , newNode->val , pthread_self());
/只要生产了一个就通知消费者 消费
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);
usleep(100);
}
return NULL;
}
void * customerfunc(void * arg){ //消费者不断的消费结点(删除)
while (1)
{
pthread_mutex_lock(&mutex);
struct ListNode *cur = head;
if(head != NULL) //判断是否有数据 (不等于空表示有数据,通知消费者去消费数据)
{
head = head ->next;
printf("delete node num: %d , tid: %ld\n", cur->val, pthread_self());
free(cur);
pthread_mutex_unlock(&mutex);
usleep(100);
}
else
{
/ 没有数据需要等待
pthread_cond_wait(&cond, &mutex);
当这个函数调用阻塞的时候,会对互斥锁进行解锁,当不阻塞的时候,继续向下执行,会重新加锁。
pthread_mutex_unlock(&mutex);
}
}
return NULL;
}
int main(){
//main函数初始化条件变量
pthread_cond_init(&cond , NULL);
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 , producerfunc , NULL);
pthread_create(&ctids[i] , NULL , customerfunc , NULL);
}
for(int i = 0 ; i<5 ;i++)
{
pthread_detach(ptids[i]);
pthread_detach(ctids[i]);
}
pthread_mutex_destroy(&mutex);
//释放条件变量
pthread_cond_destroy(&cond);
pthread_exit(NULL);
return 0;
}
执行结果如下,可以看到,一个生产者线程生产了一个结点后,会通知一个消费者线程进行消费:
信号量
信号量是一种计数器,用于控制对共享资源的访问。实现的目标与条件变量是相似的,都是用于阻塞线程的存在。主要不同的是,信号量有“信号灯”的概念,灯只有两种状态,要么是亮的,要么是灭的。这也就对应了二进制信号量的 0 ,1 两种状态。 灯亮了代表资源可用,灯灭了代表资源不可用。
使用信号量其实只是限定了访问资源的次数。如果信号量里的值是0,则会阻塞进程,直到信号灯大于0为止。例如初始化信号灯的值是5,那么调用一次就会-1 ,为0时则会阻塞进程,不允许调用。
来看看信号量的接口:
信号量的类型 sem_t
int 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);
- 对信号量解锁,调用一次对信号量的值+1
int 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); 会对8进行-1,变成7
sem_post(&csem); 生产了一个内容要通知消费者去消费,因此要 +1
}
customer() {
sem_wait(&csem);
sem_post(&psem)
}
具体实现:
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
#include<semaphore.h>
/// 创建两个信号量
sem_t psem;
sem_t csem;
///
pthread_mutex_t mutex;
struct ListNode{
int val;
struct ListNode *next;
};
struct ListNode *head = NULL;
void * producerfunc(void * arg){ //生产者不断的生产结点
while (1)
{
/// 每次生产-1,可以知道在这个循环里生产了8个链表节点,生产者进来先消费一个信号灯
sem_wait(&psem);
///
pthread_mutex_lock(&mutex);
struct ListNode *newNode = (struct ListNode*)malloc(sizeof(struct ListNode));
newNode ->next = head;
head = newNode;
newNode ->val = rand()%1000;
printf("add Node, num: %d , tid : %ld\n" , newNode->val , pthread_self());
pthread_mutex_unlock(&mutex);
/// 当上面的减到0时候,证明发送给消费者的就是8了,表示他可以消费8个
sem_post(&csem);
///
}
return NULL;
}
void * customerfunc(void * arg){ //消费者不断的消费结点(删除)
while (1)
{
/// 代码到这里 csem的值已经是8了,证明生产者给他生产了8个链表结点可以消费
sem_wait(&csem);
///
pthread_mutex_lock(&mutex);
struct ListNode *cur = head;
head = head ->next;
printf("delete node num: %d , tid: %ld\n", cur->val, pthread_self());
free(cur);
pthread_mutex_unlock(&mutex);
///
sem_post(&psem);
///
}
return NULL;
}
int main(){
/// 初始化两个信号量
sem_init(&psem , 0 , 8);
sem_init(&csem , 0 , 0);
///
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 , producerfunc , NULL);
pthread_create(&ctids[i] , NULL , customerfunc , 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;
}
读写锁
考虑一种情形,当前持有互斥锁的线程只是要读访问共享资源,而同时有其它几个线程也想读取这个共享资源,但是由于互斥锁的排它性,所有其它线程都无法获取锁,也就无法读访问共享资源了,但是实际上多个线程同时读访问共享资源并不会导致问题。
在对数据的读写操作中,更多的是读操作,写操作较少,例如对数据库数据的读写应用为了满足当前能够允许多个读出,但只允许一个写入的需求,线程提供了读写锁来实现读写锁的特点:
- 如果有其它线程读数据,则允许其它线程执行读操作,但不允许写操作。
- 如果有其它线程写数据,则其它线程都不允许读、写操作。
- 写是独占的,写的优先级高。
直接看案例,案例描述,8个线程操作同一个全局变量:3个线程不定时写这个全局变量,5个线程不定时的读这个全局变量。
读写锁接口:
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_unlock(pthread_rwlock_t *rwlock);
//上读 OR 写锁
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);
1、先创建读写锁:
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
int nums = 0 ; //全局变量
pthread_rwlock_t rwlock;
int main(){
// 初始化读写锁
pthread_rwlock_init(&rwlock , NULL);
pthread_t wtids[3] , rtids[5];
//创建读, 写线程
for(int i = 0 ; i<3 ; i++)
{
pthread_create(&wtids[i] , NULL , myWrite , NULL); //写线程池
}
for(int i = 0 ; i<5; i++)
{
pthread_create(&rtids[i] , NULL , myRead , NULL); //读线程池
}
// 设置线程分离
for(int i = 0 ; i<3 ; i++)
{
pthread_detach(wtids[i]);
}
for(int i = 0 ; i<5; i++)
{
pthread_detach(rtids[i]);
}
pthread_exit(NULL);
// 释放读写锁
pthread_rwlock_destroy(&rwlock);
return 0;
}
2、初始化工作完毕后,开始编写读写线程的执行函数
写:
void * myWrite(void *arg){
while (1)
{
// 写操作加写锁
pthread_rwlock_wrlock(&rwlock);
nums++;
printf("++write , tid: %ld, nums: %d\n" , pthread_self() , nums);
pthread_rwlock_unlock(&rwlock);
usleep(100);
}
return NULL;
}
读:
void * myRead(void *arg){
while (1)
{
//读操作加读锁
pthread_rwlock_rdlock(&rwlock);
printf("==read , tid: %ld, nums: %d\n" , pthread_self() , nums);
pthread_rwlock_unlock(&rwlock);
usleep(100);
}
return NULL;
}
整体代码:
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
int nums = 0 ;
pthread_rwlock_t rwlock;
void * myWrite(void *arg){
while (1)
{
// 写操作加写锁
pthread_rwlock_wrlock(&rwlock);
nums++;
printf("++write , tid: %ld, nums: %d\n" , pthread_self() , nums);
pthread_rwlock_unlock(&rwlock);
usleep(100);
}
return NULL;
}
void * myRead(void *arg){
while (1)
{
// 读操作加读锁
pthread_rwlock_rdlock(&rwlock);
printf("==read , tid: %ld, nums: %d\n" , pthread_self() , nums);
pthread_rwlock_unlock(&rwlock);
usleep(100);
}
return NULL;
}
int main(){
// 初始化读写锁
pthread_rwlock_init(&rwlock , NULL);
pthread_t wtids[3] , rtids[5];
// 创建读, 写线程
for(int i = 0 ; i<3 ; i++)
{
pthread_create(&wtids[i] , NULL , myWrite , NULL); //写线程池
}
for(int i = 0 ; i<5; i++)
{
pthread_create(&rtids[i] , NULL , myRead , NULL); //读线程池
}
// 设置线程分离
for(int i = 0 ; i<3 ; i++)
{
pthread_detach(wtids[i]);
}
for(int i = 0 ; i<5; i++)
{
pthread_detach(rtids[i]);
}
// 退出主线程
pthread_exit(NULL);
// 释放读写锁
pthread_rwlock_destroy(&rwlock);
return 0;
}
执行结果: