目录
一、线程同步
多线程中重要的内容。
案例:使用多线程卖票的案例,有3个窗口,100张票;
微观角度:并发执行;宏观角度:并行执行;
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
void * sellticket(void * arg)
{
int tickets=100;
//卖票;
while(tickets>0)
{
// usleep(7000);
printf("%ld 正在卖第 %d 张票;\n",pthread_self(),tickets);
tickets--;
}
return NULL;
}
int main()
{
//创建三个子线程;
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);//退出主线程;
return 0;
}
结果分析:
三个线程各自卖了100张票, 但是需求是三个窗口一共100张,所以,结果并不是我们预想的那样。因为tickets作为局部变量,在函数里面,每个线程都有自己的局部变量,这样就有三个tickets。
修改为全局变量:
//全局变量,所有的线程卖100张;
int tickets=100;
void * sellticket(void * arg)
{
//卖票;
while(tickets>0)
{
printf("%ld 正在卖第 %d 张票;\n",pthread_self(),tickets);
tickets--;
}
return NULL;
}
运行结果:
我们发现还是有问题,比如说第100张票卖了好几遍,这显然是不正确的。我们在程序中加上延时,这种现象会更加明显:
void * sellticket(void * arg)
{
//卖票;
while(tickets>0)
{
usleep(7000);
printf("%ld 正在卖第 %d 张票;\n",pthread_self(),tickets);
tickets--;
}
return NULL;
}
运行结果:
根据上面我们甚至发现出现了零票、负数票的现象,这是为什么呢?
线程同步有关的问题,三个线程并发执行,抢占CPU资源,谁抢到就谁执行,这样会导致,同一个票数,ABC都进入卖票的程序,在票数没减之前,其他线程就会出票。
多个线程对同一个资源做了同时的处理。那么如何处理这种问题?
对于共享数据的操作同一个时间只能有一个线程进行操作。
线程的主要优势在于,能够通过全局变量来共享信息,不过这种便捷的共享是有代价的:必须确保多个线程不会同时修改同一变量,或者某一线程不会读取正在有其他进程修改的变量。
临界区:访问某一共享资源的代码片段,并且这段代码的执行应为原子操作,也就是同时访问同一共享资源的其他线程不应终端该片段的执行。
线程同步:即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作,其他线程才能对该线程的内存地址进行操作,而其他线程则处于等待状态。
线程同步能降低代码的执行效率,但是这是必须的,因为这得保证代码的安全性;
二、互斥量
2.1概述
为何解决线程数据的安全问题;
为了避免线程更新共享变量出现问题,可以使用互斥量(mutex),来确保同时只有一个线程来访问某项共享资源。可以使用互斥量来保证任意共享资源的原子访问。
互斥量有两种状态:已锁定和未锁定。
任何时候,只有有一个线程可以锁定该互斥量。试图对已经锁定的某一互斥量再次进行加锁,将可能阻塞线程或者报错,具体取决于加锁时使用的方法;
一旦线程锁定互斥量,随即称为该互斥量的所有者,只有所有者才能给互斥量解锁。一百清凉下,对每一共享资源(可能由多个相关变量组成)会使用不同的互斥量,每一个线程在访问同一资源时将采用如下协议:
针对共享资源锁定互斥量;
访问共享资源;
对互斥量解锁;
如果多个新城试图执行这个代码(一个临界区),事实上只有一个线程能够持有该互斥量(其他线程将遭到阻塞),即同时只有一个线程能够进入这段代码区域:
2.2 互斥量相关操作函数
◼ 互斥量的类型 pthread_mutex_t
类似于变量的类型,使用互斥量需要先定义一个互斥量;
◼ int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);初始化互斥量:
参数:mutex:需要初始化的互斥量;
attr:互斥量相关属性-NULL;
restrict:C语言修饰符。被修饰指针不能由另外的指针操作;
◼ 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);解锁。
我们在前面的卖票程序中,出现的问题就是,三个线程抢占CPU资源,都进入临界区进行卖票,这就会出现一票多卖以及负票这种现象。为了解决这个问题,我们提出来加锁这种方法,就是在一个线程进入临界区时,加锁,不让别的线程进来,它卖,卖完后再解锁,允许其他线程进入。
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
//全局变量,所有的线程卖100张;
int tickets=100;
//创建一个互斥量
pthread_mutex_t mutex;
void * sellticket(void * arg)
{
//加锁
//卖票;
while(1)
{
pthread_mutex_lock(&mutex);
if(tickets>0)
{
printf("%ld 正在卖第 %d 张票;\n",pthread_self(),tickets);
tickets--;
}
else
{
pthread_mutex_unlock(&mutex);
usleep(7000);
break;
}
pthread_mutex_unlock(&mutex);
usleep(7000);
}
//解锁
return NULL;
}
int main()
{
//初始化互斥量
pthread_mutex_init(&mutex,NULL);
//创建三个子线程;
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_mutex_destroy(&mutex);
pthread_exit(NULL);//退出主线程;
return 0;
}
运行结果:
交替卖票,三个线程交替完成卖票;
void * sellticket(void * arg)
{
while(1)
{
pthread_mutex_lock(&mutex);
if(tickets>0)
{
printf("%ld 正在卖第 %d 张票;\n",pthread_self(),tickets);
tickets--;
}
else
{
pthread_mutex_unlock(&mutex);
usleep(7000);
break;
}
pthread_mutex_unlock(&mutex);
usleep(7000);
}
return NULL;
}
注意:加锁和解锁的位置,以及加延时的位置,加延迟的作用是,防止一个线程抢占到CPU资源后,一直把票给卖完了,加上延时后,线程就算卖票,抢占上一个CPU资源,一次也卖不了太多的票。
三、死锁
有时,一个线程需要同时访问两个或者更多不同的共享资源,而每个共享资源又都由不同的互斥量进行管理,当超过一个线程加锁同一组互斥量时,就有可能发生死锁。
两个或者连个以上的进程执行过程中,因为争夺共享资源造成的一种互相等待的现象,若无外力作用,他们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁。
死锁的几种场景:
忘记释放锁;
重复加锁;
多线程多锁,抢占锁资源;
3.1忘记释放锁
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
//全局变量,所有的线程卖100张;
int tickets=100;
//创建一个互斥量
pthread_mutex_t mutex;
void * sellticket(void * arg)
{
//加锁
//卖票;
while(1)
{
pthread_mutex_lock(&mutex);
if(tickets>0)
{
printf("%ld 正在卖第 %d 张票;\n",pthread_self(),tickets);
tickets--;
}
else
{
pthread_mutex_unlock(&mutex);
usleep(7000);
break;
}
usleep(7000);
}
return NULL;
}
int main()
{
//初始化互斥量
pthread_mutex_init(&mutex,NULL);
//创建三个子线程;
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_mutex_destroy(&mutex);
pthread_exit(NULL);//退出主线程;
return 0;
}
运行结果:
多线程抢占共享资源区,一个进程进入,加锁,出来,第二次无法进入以及其他进程也无法进入,造成死锁现象;
3.2重复加锁
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
//全局变量,所有的线程卖100张;
int tickets=100;
//创建一个互斥量
pthread_mutex_t mutex;
void * sellticket(void * arg)
{
//加锁
//卖票;
while(1)
{
pthread_mutex_lock(&mutex);
pthread_mutex_lock(&mutex);
if(tickets>0)
{
printf("%ld 正在卖第 %d 张票;\n",pthread_self(),tickets);
tickets--;
}
else
{
pthread_mutex_unlock(&mutex);
usleep(7000);
break;
}
pthread_mutex_unlock(&mutex);
pthread_mutex_unlock(&mutex);
usleep(7000);
}
return NULL;
}
int main()
{
//初始化互斥量
pthread_mutex_init(&mutex,NULL);
//创建三个子线程;
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_mutex_destroy(&mutex);
pthread_exit(NULL);//退出主线程;
return 0;
}
运行结果:
多线程抢占共享资源区,一个进程进入,进入第一道锁,第二道锁无法进入因为已经这道锁已经被加过了,没能解锁,无法再加,造成死锁现象。
3.3多线程多锁
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
//创建两个互斥量
pthread_mutex_t mutex1,mutex2;
void * workA(void *arg)
{
pthread_mutex_lock(&mutex1);
sleep(1);
pthread_mutex_lock(&mutex2);
printf("workA...\n");
pthread_mutex_unlock(&mutex2);
pthread_mutex_unlock(&mutex1);
return NULL;
}
void * workB(void *arg)
{
pthread_mutex_lock(&mutex2);
sleep(1);
pthread_mutex_lock(&mutex1);
printf("workB...\n");
pthread_mutex_unlock(&mutex1);
pthread_mutex_unlock(&mutex2);
return NULL;
}
运行结果:
线程A运行A区共享资源,线程B运行B区共享资源,两个区各有两把锁A区a锁将线程A锁住,B区b锁将线程B锁住,线程A在A区无法被B锁锁住,线程B无法在B区被A锁锁住,造成死锁。
四、读写锁
问题:当有一个线程已经持有互斥锁时,互斥锁将所有试图进入临界区的线程都阻塞住。但是考虑一种情形,当前持有互斥锁的线程只是要读访问共享资源,而同时有其它几个线程也想读取这个共享资源,但是由于互斥锁的排它性,所有其它线程都无法获取锁,也就无法读访问共享资源了,但是实际上多个线程同时读访问共享资源并不会导致问题。
解决:在对数据的读写操作中,更多的是读操作,写操作较少,例如对数据库数据的读写应用。为了满足当前能够允许多个读出,但只允许一个写入的需求,线程提供了读写锁来实现。
读写锁的特点:
1、 如果有其它线程读数据,则允许其它线程执行读操作,但不允许写操作。
2、 如果有其它线程写数据,则其它线程都不允许读、写操作。
3、 写是独占的,写的优先级高。
◼ 读写锁的类型 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()
{//创建3个写线程,5个读线程;
pthread_t wtid[3],rtid[5];
int i;
// pthread_mutex_init(&mutex,NULL);
pthread_rwlock_init(&rwlock,NULL);
for(i=0;i<3;i++)
{
pthread_create(&wtid[i],NULL,writenum,NULL);
}
for(i=0;i<5;i++)
{
pthread_create(&rtid[i],NULL,readnum,NULL);
}
//设置线程分离
for(i=0;i<3;i++)
{
pthread_detach(wtid[i]);
}
for(i=0;i<5;i++)
{
pthread_detach(rtid[i]);
}
pthread_rwlock_destroy(&rwlock);
pthread_exit(NULL);
return 0;
}
运行结果:
利用读写锁实现,写线程具有较高权限,读线程可以并行执行,不会发生阻塞,写线程会具有阻塞行为,整体效率要高效于互斥锁。
五、生产者消费者模型
生产者消费者模型:
模型含有三个对象:生产者、消费者以及容器;
由生产者生产产品,消费者使用产品,且数量都是多个的,每个生产者与消费者用一个线程实现,那么容器中的产品就是数据。当容器满后生产者会阻塞等待消费者进行消费。当容器为空后,消费者会堵塞等待生产者生产。
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
struct Node
{
int num;
struct Node * next;
};
//头结点
struct Node* head=NULL;
void * producter(void * arg)
{
//不断创建新的结点,添加在链表中;
while(1)
{
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());
usleep(100);
}
return NULL;
}
void * customer(void * arg)
{
while(1)
{
//保存头结点的指针;
struct Node* temp=head;
head=head->next;
printf("del node num:%d,tid:%ld\n",temp->num,pthread_self());
free(temp);
usleep(100);
}
return NULL;
}
int main()
{
int i;
//创建5个生产者线程,和5个消费者线程;
pthread_t ptids[5],ctids[5];
for(i=0;i<5;i++)
{
pthread_create(&ptids[i],NULL,producter,NULL);
pthread_create(&ctids[i],NULL,customer,NULL);
}
for(i=0;i<5;i++)
{
pthread_detach(ptids[i]);
pthread_detach(ctids[i]);
}
while(1)
{
sleep(1);
}
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)
#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 * producter(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* temp=head;
if(head!=NULL)
{
//有数据;
head=head->next;
printf("del node num:%d tid:%ld\n",temp->num,pthread_self());
free(temp);
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);
int i;
//创建5个生产者线程,和5个消费者线程;
pthread_t ptids[5],ctids[5];
for(i=0;i<5;i++)
{
pthread_create(&ptids[i],NULL,producter,NULL);
pthread_create(&ctids[i],NULL,customer,NULL);
}
for(i=0;i<5;i++)
{
pthread_detach(ptids[i]);
pthread_detach(ctids[i]);
}
while(1)
{
sleep(1);
}
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond);
pthread_exit(NULL);
return 0;
}
运行结果:
加上互斥锁,作用在于线程同步时,共享资源区数据的安全性。
加上条件变量,其意义在于当产品为0时,不使用条件变量,消费者处于无意义的缓冲,此时,只执行加锁和解锁,直到时间片用完才发生线程切换,浪费CPU资源。而加入条件变量,一旦发现缓冲区是空的,会立刻从运行态进入阻塞态,此时一定会发生线程切换,把CPU资源让出来。互斥锁和条件变量都可以实现线程同步,但是使用条件变量效率更高。
七、信号量
信号量也称之为信号灯,它跟条件变量的作用是一致的,能起到阻塞线程的作用;灯亮表示资源可用,灯灭表示资源不可用。
◼ 信号量的类型 sem_t
◼ int sem_init(sem_t *sem, int pshared, unsigned int value);初始化;
头文件: #include <semaphore.h>
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); 获取post的值;
利用信号量完成生产者消费者的模型:
sem_t psem;//创建一个生产者信号量
sem_t csem;//创建一个消费者信号量
init(psem,0,8);//初始化生产者信号量,其信号量的初值设置为8;
init(csem,0,0);//初始化消费者信号量,其信号量的初值设置为0;
producer()
{
sem_wait(&psem);//对生产者信号量加锁,调用一次,对生产者信号量的值减1,如果值为0就阻塞。
sem_pose(&csem);//调用一次,对消费信号量的值+1;
}
customer()
{
sem_wait(&csem);对消费者信号量加锁,调用一次,对消费者信号量的值减1,如果值为0就阻塞。
sem_pose(&psem);//调用一次,对生产者信号量的值+1;
}
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
#include <semaphore.h>
//创建一个互斥量
pthread_mutex_t mutex;
//创建两个信号量
sem_t psem,csem;
struct Node
{
int num;
struct Node * next;
};
//头结点
struct Node* head=NULL;
void * producter(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);
usleep(100);
}
return NULL;
}
void * customer(void * arg)
{
while(1)
{
sem_wait(&csem);
//保存头结点的指针;
pthread_mutex_lock(&mutex);
struct Node* temp=head;
//有数据;
head=head->next;
printf("del node num:%d tid:%ld\n",temp->num,pthread_self());
free(temp);
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);
int i;
//创建5个生产者线程,和5个消费者线程;
pthread_t ptids[5],ctids[5];
for(i=0;i<5;i++)
{
pthread_create(&ptids[i],NULL,producter,NULL);
pthread_create(&ctids[i],NULL,customer,NULL);
}
for(i=0;i<5;i++)
{
pthread_detach(ptids[i]);
pthread_detach(ctids[i]);
}
while(1)
{
sleep(1);
}
pthread_mutex_destroy(&mutex);
pthread_exit(NULL);
return 0;
}
运行结果:
本次我们讲述了线程的同步、互斥问题以及与互斥相关的死锁、读写锁以及举了相关案例——生产者消费者模型,并结合条件变量以及信息量完善了案例的实现。
创做不易,感谢大家多多支持!!!