赞
踩
在学习管道的时候,管道是自带同步与互斥的。而在线程中,当多个线程没有加锁的情况下同时访问临界资源时会发生混乱。在举例之前,先了解几个概念。
大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程无法获得这种变量。但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互。多个线程并发的操作共享变量,会带来一些问题。
我们可以通过一个买票的例子,来看这块问题。
- int ticket = 2000;
- void *STicket(void *asg)
- {
- while (1)
- {
- if (ticket > 0)
- {
- usleep(100);
- printf("%s sang ticket:%d \n", (char *)asg, ticket--);
- }
- else
- {
- break;
- }
- }
- return NULL;
- }
-
- int main()
- {
- pthread_t t[4];
- int i;
- for (i = 0; i < 4; i++)
- {
- char *p = (char *)malloc(sizeof(char) * 64);
- sprintf(p, "pthread t%d", i);
- pthread_create(&t[i], NULL, STicket, (void *)p);
- }
- pthread_join(t[0], NULL);
- pthread_join(t[1], NULL);
- pthread_join(t[2], NULL);
- pthread_join(t[3], NULL);
-
- return 0;
- }

我们在运行结果中可以看到,票的数量本不可能出现负数的,但是在结果中出现了,那么这就是一个问题。
多个线程并发的访问同一块临界资源,我们用t1,t2,t3,t4,来表示四个线程。一开始票的数量有1000张。
《出现问题1》当t1首先访问到票时,判断票还有剩余,于是拿走一张票,票还剩999张。但是这些线程是并发执行的,有可能多个线程同时拿到票,且通过对票进行减减操作,那么这个票是重复了。
《出现问题2》当t3拿到票的时候,刚准备对票进行减减,时间片就到了,线程退出,那么在t3这个线程内把读取到的票的数量保存起来,当t3这个线程有运行时,先恢复上下文数据,然后对山下文数据中保存票的数量进行减减,当t3这个线程完成了操作后,把剩余票的数量进行更新,那么在t3没有运行前,票已经抢完了,但是t3它不知道,然后又把票的数量进行更新了,票又回来了,这个时候又出错了。出现负数的情况就是这样。
在我们判断票是否有剩余的时候,和对票减减的时候,并不是具有原子性的,因为这个时候,其他线程也在进行抢票,可能拿到重复的票。我们可以通过汇编来验证是否具有原子性。
- int main()
- {
- int a = 5;
- a--;
- return 0;
- }
--操作并不是原子性,而是对应了三条汇编:
想要解决上面的问题,需要做到三点:
而以上的三点本质就是加一把锁,在Linux上提供的这把锁叫做互斥量。
先要理解这个锁。当多个线程同时要执行临界区的代码,那么谁先申请到这把锁,谁就执行,其他的线程就开始进行等待,等待这把锁被释放,然后申请这把锁。
初始化互斥量有两中方法:
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
参数:mutex:要初始化的互斥量
attr:设置属性,一般设置NULL,用默认设置
返回值:成功返回0,错误返回错误号
功能:销毁互斥量 原型:int pthread_mutex_destroy(pthread_mutex_t *mutex); 参数:mutex:要销毁的互斥量 返回值:成功返回0,错误返回错误号
【注意】
互斥量加锁和解锁
- 功能:加锁
- 原型:int pthread_mutex_lock(pthread_mutex_t *mutex);
- 参数:mutex:要加锁的互斥量
- 返回值:成功返回0,错误返回错误号
-
- 功能:解锁
- 原型:int pthread_mutex_unlock(pthread_mutex_t *mutex);
- 参数:mutex:要解锁的互斥量
- 返回值:成功返回0,错误返回错误号
调用pthread_mutex_lock会遇到的情况
现在我们对之前的买票系统进行改进
- int ticket = 2000;
- pthread_mutex_t lock;
-
- void *STicket(void *asg)
- {
- while (1)
- {
- // 在执行临界区的代码前,先申请锁(加锁)
- pthread_mutex_lock(&lock);
- if (ticket > 0)
- {
- usleep(100);
- printf("%s sang ticket:%d \n", (char *)asg, ticket--);
- }
- else
- {
- // 当没有票的时候,也释放锁(解锁)
- pthread_mutex_unlock(&lock);
- break;
- }
- // 访问完了临界资源时,释放锁(解锁)
- pthread_mutex_unlock(&lock);
- }
- return NULL;
- }
-
- int main()
- {
- // 动态的初始化锁
- pthread_mutex_init(&lock, NULL);
- pthread_t t[4];
- int i;
- for (i = 0; i < 4; i++)
- {
- char *p = (char *)malloc(sizeof(char) * 64);
- sprintf(p, "pthread t%d", i);
- pthread_create(&t[i], NULL, STicket, (void *)p);
- }
- pthread_join(t[0], NULL);
- pthread_join(t[1], NULL);
- pthread_join(t[2], NULL);
- pthread_join(t[3], NULL);
- // 最后销毁锁
- pthread_mutex_destroy(&lock);
- return 0;
- }

1、一个线程拿到了锁,会不会被其他线程切换?
答:会被切换,当这个拿到锁的线程切换到了其他线程,其他线程依然没有锁,依然要等待,然而当拿到锁的线程又开始运行时,首先要先恢复上下文数据,这个线程依然是拿到锁的状态(这个线程是拿着锁被切走的),可以继续执行临界区的代码。
2、申请锁的过程是不是原子性的?
答:申请锁的原子性的,要么没有申请到锁,要么锁已经释放了,可以申请锁。
3、锁本身就是临界资源,那么谁来保护锁?
答:锁是来保护临界资源的,但是锁也是临界资源的呀。但是锁本身就具有原子性,申请锁的过程必须是原子性的。
概念
常见的线程不安全情况
常见的线程安全情况
常见的不可重入情况
常见的可重入情况
可重入与线程安全的联系
可重入与线程安全的区别
在上面的买票系统中,如果线程1的优先级非常高,那么会不会出现票都被线程A给抢完了。线程1申请锁后抢票完成,释放锁,释放完后线程A又申请到锁,如此往复,直到票买完了。按理说这样没有错,各凭本事买票嘛,但这样没有高效的让多个执行流使用这个资源,那么多执行流就没有意义了。线程同步就是来解决这个问题的。要申请锁的所有线程依次排队申请,使用完锁的线程去队尾排队,这样就防止了一个优先级高的线程抢完所以资源。
条件变量我们可以理解为:条件变量使我们可以睡眠等待某种条件的出现。
饥饿问题:多个执行流,在保证互斥地访问同一块资源时,该资源一直被同一个执行流访问,就会导致其他执行流形成饥饿,这种现象就做饥饿问题。
初始化条件变量
- 功能:初始化条件变量
- 原型:int pthread_cond_init(pthread_cond_t *restrict cond,
- const pthread_condattr_t *restrict attr);
- 参数:cond:要初始化的条件变量
- attr:条件变量的属性,设置NULL,使用默认的。
销毁条件变量
- 功能:释放条件变量
- 原型:int pthread_cond_destroy(pthread_cond_t *cond);
- 参数:cond:要销毁的条件变量
等待条件满足
- 功能:等待条件满足
- 原型: int pthread_cond_wait(pthread_cond_t *restrict cond,
- pthread_mutex_t *restrict mutex);
- 参数:cond:要在这个条件变量上等待
- mutex:互斥量,后面详细解释
唤醒等待
- 功能:唤醒等待队列中队头线程
- 原型:int pthread_cond_signal(pthread_cond_t *cond);
- 参数:cond:在这个条件变量上唤醒
-
- 功能:唤醒所以线程
- 原型 :int pthread_cond_broadcast(pthread_cond_t *cond);
- 参数:cond:在这个条件变量上唤醒
-
- 返回值:成功返回0;失败返回错误号
pthread_cond_t
我们设置条件变量的类型是pthread_cond_t。
- struct pthread_cond_t
- {
- int flag;//0表示没有钥匙,1表示有钥匙
- task_struct *queue;//等待队列
- }
简单的案例:
- // 定义锁
- pthread_mutex_t lock;
- // 定义条件变量
- pthread_cond_t cond;
- // 设置票的数量为6张
- int ticket = 6;
-
- void *RunRoute(void *arg)
- {
- // 分离自己,线程退出自动释放
- pthread_detach(pthread_self());
- while (true)
- {
- // 申请锁
- pthread_mutex_lock(&lock);
- // 等待条件变量
- pthread_cond_wait(&cond, &lock);
- if (ticket > 0)
- {
-
- std::cout << (char *)arg << "抢到了" << ticket << "号票" << std::endl;
- ticket--;
- }
- else
- {
- std::cout << "票卖完了" << std::endl;
- // 释放锁
- pthread_mutex_unlock(&lock);
- break;
- }
- // 释放锁
- pthread_mutex_unlock(&lock);
- }
- }
- int main()
- {
- // 初始化锁
- pthread_mutex_init(&lock, nullptr);
- // 初始化条件变量
- pthread_cond_init(&cond, nullptr);
- pthread_t t1, t2, t3;
- // 创建线程
- pthread_create(&t1, NULL, RunRoute, (void *)"thread t1");
- pthread_create(&t2, NULL, RunRoute, (void *)"thread t2");
- pthread_create(&t3, NULL, RunRoute, (void *)"thread t3");
- // 主线程控制其他线程
- while (true)
- {
- // 通过回车来唤醒等待的线程
- getchar();
- // 唤醒等待队列中队头的线程
- pthread_cond_signal(&cond);
- }
- return 0;
- }

看运行结果,t1、t2、t3线程轮流抢票。
pthread_cond_wait为什么需要互斥量
看上面的代码。在pthread_cond_wait函数最后一个参数是互斥量。
看代码,当一个线程申请到锁时,就开始执行pthread_cond_wait进行等待,在等待的过程中,该线程的锁会被释放,等线程被唤醒的时候,该线程的锁又会回到手上。
条件变量使用规范
- pthread_mutex_lock(&lock);
- while (条件为假)
- pthread_cond_wait(&cond, &lock);
- //修改条件
- pthread_mutex_unlock(&mutex);
- pthread_mutex_lock(&lock);
- //设置条件为真
- pthread_cond_signal(&cond);
- pthread_mutex_unlock(&lock);
死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态。
自己也可以让自己死锁。
- pthread_mutex_t lock;
- int count = 10;
- void *Rounit(void *asg)
- {
- while (1)
- {
- pthread_mutex_lock(&lock);
- pthread_mutex_lock(&lock);
- if (count > 0)
- {
- count--;
- }
- else
- {
- pthread_mutex_unlock(&lock);
- break;
- }
- pthread_mutex_unlock(&lock);
- }
- }
-
- int main()
- {
- pthread_t t1;
- pthread_mutex_init(&lock, NULL);
- pthread_create(&t1, NULL, Rounit, NULL);
- pthread_join(t1, NULL);
- pthread_mutex_destroy(&lock);
- return 0;
- }

这会行成死锁,因为线程申请了两次锁,第二次申请锁的时候锁已经在你手上了,但因为你还要等待锁被释放,所以一直等待,形成了死锁。
POSIX信号量和SystemV信号量作用相同,都是用于同步操作,达到无冲突的访问共享资源目的。 但POSIX可以用于线程间同步。
使用信号量要包semaphore.h头文件
- 功能:初始化信号量
- 原型:int sem_init(sem_t *sem, int pshared, unsigned int value);
- 参数:sem:信号量
- pshared:0表示线程间共享,非零表示进程间共享
- value:信号量初始值
-
- 功能:销毁信号量
- 原型:int sem_destroy(sem_t *sem);
- 参数:sem:信号量
-
- 功能:等待信号量,会将信号量的值减1(P操作)
- 原型:int sem_wait(sem_t *sem);
- 参数:sem:信号量
-
- 功能:发布信号量,表示资源使用完毕,可以归还资源了。将信号量值加1(V操作)
- 原型:int sem_post(sem_t *sem);
- 参数:sem:信号量

写个二元信号量来熟悉这些函数。二元信号量的使用类似锁。
二元信号量: value为1,当 value经过P操作变成0时,线程要等待 value又变成1。
- class Sem
- {
- public:
- Sem()
- {
- sem_init(&sem, 0, 1);
- }
- ~Sem()
- {
- sem_destroy(&sem);
- }
- void P()
- {
- sem_wait(&sem);
- }
- void V()
- {
- sem_post(&sem);
- }
-
- private:
- sem_t sem;
- };
- Sem sem;
- int tickets = 10;
- void *GetTickets(void *asg)
- {
- while (true)
- {
- sleep(1);
- sem.P();
- if (tickets > 0)
- {
- std::cout << (char *)asg << "抢到了" << tickets << "号票" << std::endl;
- tickets--;
- sem.V();
- }
- else
- {
- break;
- }
- }
- }
-
- int main()
- {
- pthread_t t1, t2;
- pthread_create(&t1, nullptr, GetTickets, (void *)"thread 1");
- pthread_create(&t2, nullptr, GetTickets, (void *)"thread 2");
-
- pthread_join(t1, nullptr);
- pthread_join(t2, nullptr);
-
- return 0;
- }

Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。