一、线程的互斥
当线程1和2都对全局变量进行读写操作时,会产生资源竞争问题。因此需要通过控制使得当前线程在走完读算写以后其他线程再走,不能再调度其他线程,其他休眠。该过程叫互斥。
1. 互斥的概念:
在多线程中对临界资源的排他性访问。临界资源即公共操作的东西(可为变量、设备)排他性访问即多线程在同一时刻只能有一个线程进行读或写操作。
互斥锁使用步骤:
作用:保证临界资源的访问控制(锁在系统中本质是结构体)
定义互斥锁 ——>初始化锁 ——>加锁 ——>解锁 ——>销毁
(1)向系统申请锁mutex(在pcb块)约定1没锁,0锁了
(2)初始化锁
(3)尝试使用该资源,申请到后用的时候加锁(其它线程要使用该资源尝试解锁,解不开锁则进入休眠等待状态)
(4)用完后解锁释放资源,系统通知,两个线程再次同时竞争资源
(5)不需要互斥操作后销毁锁
2.互斥锁相关函数
1)定义锁:pthread_mutex_t mutex
(1)pthread_mutex_t :互斥锁类型
(2) mutex:互斥锁变量(创建在pcb块中的东西) 也称内核对象(在内核中被定义的)
2)初始化锁函数:pthread_mutex_init():将已经定义好的互斥锁初始化。
int pthread_mutex_init(pthread_mutex_t *mutex,const pthread_mutexattr_t *attr);
参数:mutex 要初始化的互斥锁
atrr 初始化的值,一般是NULL表示默认锁
返回值:成功 0;失败 非零
3)加锁:pthread_mutex_lock():用指定的互斥锁开始加锁代码
int pthread_mutex_lock(pthread_mutex_t *mutex);
参数: mutex 用来给代码加锁的互斥锁
返回值:成功 0;失败 非零
注意: 加锁后的代码到解锁部分的代码属于原子操作(汇编角度来说多个语句一把走完);在加锁期间其他进程/线程都不能操作该部分代码;如果该函数在执行的时候,mutex已经被其他部分使用则代码阻塞。
4)解锁:pthread_mutex_unlock():将指定的互斥锁解锁。
int pthread_mutex_unlock(pthread_mutex_t *mutex);
参数:用来解锁的互斥锁
返回值:成功 0;失败 非零
注意:解锁之后代码不再排他访问,一般加锁解锁同时出现;
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
int count =0;
pthread_mutex_t mutex;
int main(int argc, char **argv)
{
int i = 5000;
while(i--)
{
pthread_mutex_lock(&mutex);
int tmp = count;
printf("%d\n",tmp +1);
count = tmp + 1;
pthread_mutex_unlock(&mutex);
}
system("pause");
return 0;
}
5)销毁:pthread_mutex_destroy():使用互斥锁完毕后需要销毁互斥锁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
参数:mutex 要销毁的互斥锁
返回值:成功 0; 失败 非零
6)非阻塞锁:pthread_mutex_trylock():
类似加锁函数效果,唯一区别就是不阻塞(稍后再试),非阻塞锁,CPU占有率较高。
int pthread_mutex_trylock(pthread_mutex_t *mutex);
参数:mutex 用来加锁的互斥锁
返回值:成功 0(操作临界资源);非0(稍后再试)
注:
(1)上互斥锁的地方不并发;
(2)被保护的临界资源要尽可能小(不要大递归、不要sleep);
(3)同一个线程不能对同一把锁重复上锁,会造成死锁。
示例代码:模拟银行办理业务,开放3个窗口,10人办理业务。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
int WIN = 3;
pthread_mutex_t mutex;
void *th (void *arg)
{
while(1)
{
pthread_mutex_lock(&mutex);
if(WIN > 0)
{
WIN --;
printf("get win\n");
pthread_mutex_unlock(&mutex);
int n = rand()%5 + 1;
sleep(n);
pthread_mutex_lock(&mutex);
WIN ++;
printf("relese win\n");
pthread_mutex_unlock(&mutex);
break;
}
else
{
pthread_mutex_unlock(&mutex);
}
}
return NULL;
}
int main(int argc, char **argv)
{
pthread_t tid[10] = {0};
int i;
pthread_mutex_init(&mutex,NULL);
for(i = 0 ; i < 10 ; ++i)
{
pthread_create(&tid[i],NULL,th,NULL);
}
for (i = 0; i < 10; ++i)
{
pthread_join(tid[i],NULL);
}
pthread_mutex_destroy(&mutex);
system("pause");
return 0;
}
二、线程的同步
1.概念:
有一定先后顺序的对资源的排他性访问;因为互斥锁可以控制排他访问但没有次序。
同步:互斥的基础上加上顺序。
信号量:其本质是结构体,也是锁的一种。
使用步骤:信号量的定义 ——>信号量的初始化 ——>信号量的PV操作——>信号量的销毁
(1)信号量的定义(申请信号量与同步线程的个数相同)
(2)初始化信号量(信号量状态初值:1该走就走;0等待)
(3)执行体操作
(4)释放的是下一个信号量
信号量的PV操作
(1)P :申请资源——申请一个二值信号量(值不是1就是0)
(2)V :释放资源——释放一个二值信号量
信号量的分类
(1)无名信号量 ——>线程间通信
(2)有名信号量 ——>进程间通信
2.信号量相关函数
1)定义信号量:sem_t sem
sem_t sem;
信号量的类型 信号量的变量
2)初始化信号量:sem_init():将已经定义好的信号量赋值
信号量的初值决定了谁先走谁后走。
int sem_init(sem_t *sem, int pshared, unsigned int value);
参数:sem 要初始化的信号量
pshared = 0 (表示线程间使用信号量);
pshared != 0 (表示进程间使用信号量);
value 信号量的初始值,一般无名信号量都是二值信号量(0 1) :
0 表示红灯,进程暂停阻塞;
1 表示绿灯,进程可以通过执行;
返回值:成功 0;失败 -1
3)申请信号量:sem_wait()
判断当前sem信号量是否有资源可用。如果sem有资源(==1),则申请该资源,程序继续运行;如果sem没有资源(==0),则线程阻塞等待,一旦有资源则自动申请资源并继续运行程序。
int sem_wait(sem_t *sem);
参数:sem 要判断的信号量资源;
返回值:成功 0 ;失败 -1
4)释放信号量:sem_post()
函数可以将指定的sem信号量资源释放;并默认执行,sem = sem+1;线程在该函数上不会阻塞。
int sem_post(sem_t *sem);
参数:sem 要释放资源的信号量;
返回值:成功 0;失败 -1
5)销毁:sem_destroy():使用完毕将指定的信号量销毁
int sem_destroy(sem_t *sem);
参数:sem要销毁的信号量;
返回值:成功 0;失败 -1
6)信号量的两种用法
(1)二值信号量
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
#include <semaphore.h>
sem_t sem_H,sem_W;
void *th1(void *arg)
{
int i = 10;
while(i--)
{
sem_wait(&sem_H);
printf("hello ");
fflush(stdout);
sem_post(&sem_W);
}
return NULL;
}
void *th2(void *arg)
{
int i = 10;
while(i--)
{
sem_wait(&sem_W);
printf("world\n");
sem_post(&sem_H);
sleep(1);
}
return NULL;
}
int main(int argc, char **argv)
{
pthread_t tid1,tid2;
sem_init(&sem_H,0,1);
sem_init(&sem_W,0,0);
pthread_create(&tid1,NULL,th1,NULL);
pthread_create(&tid2,NULL,th2,NULL);
pthread_join(tid1,NULL);
pthread_join(tid2,NULL);
sem_destroy(&sem_H);
sem_destroy(&sem_W);
}
(2)计数信号量
#include <pthread.h>
#include <semaphore.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
sem_t sem_WIN;
void *th(void *arg) {
sem_wait(&sem_WIN);
printf("get win\n");
int n = rand() % 5 + 1;
sleep(n);
printf("relese win\n");
sem_post(&sem_WIN);
return NULL;
}
int main(int argc, char **argv) {
pthread_t tid[10] = {0};
int i = 0;
sem_init(&sem_WIN, 0, 3);
for (i = 0; i < 10; i++) {
pthread_create(&tid[i], NULL, th, NULL);
}
for (i = 0; i < 10; i++) {
pthread_join(tid[i], NULL);
}
sem_destroy(&sem_WIN);
system("pause");
return 0;
}
三、死锁问题
1.死锁定义:
死锁是指多个进程或线程在竞争资源时,由于互相等待对方释放资源而陷入无限阻塞的状态。(归根结底是逻辑错误导致的)
2.产生死锁的原因:
(1) 因为系统资源不足。
(2) 进程运行推进的顺序不合适。
(3) 资源分配不当等。
如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁。其次,进程运行推进顺序与速度不同,也可能产生死锁。
3.产生死锁的四个必要条件:
(1) 互斥条件:一个资源每次只能被一个进程或线程使用。(若死锁可先把阻塞锁换成非阻塞锁找出错误后改回)
(2) 请求与保持条件:一个进程或线程因请求资源而阻塞时,对已获得的资源保持不放。
(3) 不剥夺条件:进或线程程已获得的资源,在末使用完之前,不能强行剥夺。
(4) 循环等待条件:若干进程或线程之间形成一种头尾相接的循环等待资源关系.