【Linux】线程(二)
前言:上篇博客主要讲述了线程概念和线程控制两部分的内容,这篇博客主要讲述线程安全。
线程安全
概念:多个线程间对临界资源进行争抢访问而不会造成数据二义或逻辑混乱。
线程安全的实现
互斥:同一时间只有一个线程能够访问临界资源来保证数据操作安全性。
同步:通过条件的判断,实现对临界资源访问的时序合理性。
互斥的实现
通过黄牛抢票的例子体会线程安全的重要性以及认识互斥锁
// 操作共享变量会有问题的售票系统代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
int ticket = 100;
void *route(void *arg)
{
char *id = (char*)arg;
while ( 1 ) {
if ( ticket > 0 ) {
printf("%s sells ticket:%d\n", id, ticket);
usleep(1000);
ticket--;
} else {
break;
}
}
}
int main( void )
{
pthread_t t1, t2, t3, t4;
pthread_create(&t1, NULL, route, "thread 1");
pthread_create(&t2, NULL, route, "thread 2");
pthread_create(&t3, NULL, route, "thread 3");
pthread_create(&t4, NULL, route, "thread 4");
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_join(t3, NULL);
pthread_join(t4, NULL);
}
运行截图:
我们可以看到同一张票被三个线程都抢到了(黄牛卖的假票吗?哈哈哈)这是因为子函数中本来一个线程访问完,应该打印票数然后票数减少,但是打印完票数后usleep
休眠,票数没减少,下一个进程又进来访问,导致临街资源被多次访问,所以要引入互斥锁对临界资源进行管理。
互斥锁(互斥量)
互斥锁概念: 实质上是一个计数器,加互斥锁,系统会先访问互斥锁再对临界资源进行访问,互斥锁有‘0’和‘1’两种状态,默认状态为‘1’可以访问,当一个线程进入访问临界资源时,互斥锁会置‘0’,组织其他线程再来访问临界资源。当前线程访问完毕后,互斥锁又由‘0’到‘1’,允许其他线程进行访问。
互斥锁的接口及操作过程
1.pthread_mutex_t
互斥锁的变量类型
互斥锁的初始化:
始化互斥锁有两种方法:
方法1,静态分配:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
方法2,动态分配:
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const
pthread_mutexattr_t
*restrict attr); 参数: mutex:要初始化的互斥量 attr:NULL
销毁互斥锁
销毁互斥锁需要注意:
使用 PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥量不需要销毁
不要销毁一个已经加锁的互斥量
已经销毁的互斥量,要确保后面不会有线程再尝试加锁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
加锁和解锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex); 返回值:成功返回0,失败返回错误号
调用 pthread_ lock
时,可能会遇到以下情况:
1.互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功
2.发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread_ lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁。
代码实现:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <sched.h>
int ticket = 100;
pthread_mutex_t mutex;
void *route(void *arg) {
char *id = (char*)arg;
while ( 1 ) {
pthread_mutex_lock(&mutex);
if ( ticket > 0 ) {
usleep(1000);
printf("%s sells ticket:%d\n", id, ticket);
ticket--;
pthread_mutex_unlock(&mutex);
// sched_yield(); 放弃CPU
} else {
pthread_mutex_unlock(&mutex);
break;
}
}
}
int main( void )
{
pthread_t t1, t2, t3, t4;
pthread_mutex_init(&mutex, NULL);
pthread_create(&t1, NULL, route, "thread 1");
pthread_create(&t2, NULL, route, "thread 2");
pthread_create(&t3, NULL, route, "thread 3");
pthread_create(&t4, NULL, route, "thread 4");
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_join(t3, NULL);
pthread_join(t4, NULL);
pthread_mutex_destroy(&mutex);
}
运行截图:
死锁
什么是死锁?如何产生的?
概念:死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态。
产生:多个线程对所资源进行争抢访问,但因为推进顺序不当,导致互相等待,造成程序流程无法推进。
死锁四个必要条件
互斥条件:一个资源每次只能被一个执行流使用
请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放
不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺
循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系
避免死锁
破坏死锁的四个必要条件
加锁顺序一致
避免锁未释放的场景
资源一次性分配
同步
同步的实现:条件变量
条件变量提供了一个 线程等待和唤醒的功能
同步实现的函数及操作:
pthread_cond_t
条件变量类型
初始化
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)
等待条件满足:
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t
*restrict mutex); 参数: cond:要在这个条件变量上等待 mutex:互斥量,后面详细解释
唤醒等待:
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);
例子:吃面(没面等待,老板面好了,唤醒等待)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <sched.h>
int is_have_noodle = 0;
pthread_cond_t cond;
pthread_mutex_t mutex;
void *foodie(void* arg)
{
while(1){
pthread_mutex_lock(&mutex);
if(is_have_noodle == 0){
pthread_cond_wait(&cond, &mutex);
}
printf("eat noodles,delicious~~!\n");
is_have_noodle--;
pthread_mutex_unlock(&mutex);
}
return NULL;
}
void *chef(void* arg)
{
while(1){
pthread_mutex_lock(&mutex);
printf("cook noodlea~~\n");
is_have_noodle++;
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);
sleep(1);
}
return NULL;
}
int main()
{
pthread_t foodie_tid,chef_tid;
int ret = 0;
pthread_cond_init(&cond, NULL);
pthread_cond_init(&mutex, NULL);
ret = pthread_create(&foodie_tid,NULL,foodie,NULL);
if(ret!=0){
printf("thread create error\n");
return -1;
}
ret = pthread_create(&chef_tid,NULL,chef,NULL);
if(ret!=0){
printf("thread create error\n");
return -1;
}
pthread_join(foodie_tid,NULL);
pthread_join(foodie_tid,NULL);
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond);
return 0;
}
pthread_cond_wait(&cond, &mutex);
其实含有三步操作
代码运行截图:
为什么pthread_ cond_ wait 需要互斥量?
1.条件等待是线程间同步的一种手段,如果只有一个线程,条件不满足,一直等下去都不会满足,所以必须要有一个线程通过某些操作,改变共享变量,使原先不满足的条件变得满足,并且友好的通知等待在条件变量上的线程。
2.条件不会无缘无故的突然变得满足了,必然会牵扯到共享数据的变化。所以一定要用互斥锁来保护。没有互斥锁就无法安全的获取和修改共享数据。