初识Linux · 系统编程done
初识Linux · 系统编程done
本文作为Linux系统编程的收尾工作,介绍的是些零碎的概念,比如死锁,可重入函数,自旋锁,读写锁等,其中死锁概念要重要些,对于自旋锁,读写锁来说都没有那么重要,所以咱们了解一下即可。
那么废话不多说,我们直接进入第一个主题,死锁。
死锁的概念为:
死锁是指两个或两个以上的线程互相申请对方的资源,同时不释放自己的资源导致的一种互相等待的情况。
我们可以举个简单的例子,A持有1块钱,B持有一块钱,A和B想要买商品C,价值为2块钱,此时A申请B的一块钱,B申请A的一块钱,但是它们都不想释放自己的一块钱,此时A等B给一块钱,B等A给它一块钱,结果就互相等待。
那么从上面的例子,我们不妨总结一下死锁构成的原因:
互斥条件:至少有一个资源只能被某种执行流持有
请求和保持:一个执行流在申请资源的时候,仍然保持自己的资源
不可剥夺条件:已经分配给一个执行流的资源不能被强行剥夺,只能由执行流自己释放
环路等待条件:存在一个回路,每个执行流都在等待其他执行流持有的资源。
以上是死锁形成的四个条件,那么我们想要破坏死锁,肯定就要从这四个条件里面破坏,当然了,也有算法是用来检测死锁的,例如银行家算法,资源分配算法,我们这里不做介绍。
对于第一个条件:
互斥条件指的是资源每次只能被一个进程(或线程)使用。要破坏这个条件,可以将独占资源改造成共享资源,允许多个进程同时使用。例如,操作系统可以采用SPOOLing(Simultaneous Peripheral Operati On-Line,即同时外围操作联机)技术,将独占设备在逻辑上改造成共享设备。但需要注意的是,并不是所有的资源都可以改造成可共享使用的资源,并且为了系统安全,很多地方还必须保护这种互斥性。因此,很多时候都无法破坏互斥条件。
对于第二个条件:
- 预先静态分配:进程在运行前一次性申请完它所需要的全部资源,在它的资源未满足前,不让它投入运行。一旦投入运行后,这些资源就一直归它所有,该进程就不会再请求别的任何资源了。但这种方法会导致资源利用率极低,并且可能导致某些进程饥饿。
- 释放已占用资源:当进程请求新的资源得不到满足时,它必须立即释放保持的所有资源,待以后需要时再重新申请。但这种方法可能导致前一阶段工作的失效,并且反复地申请和释放资源会增加系统开销,降低系统吞吐量。
对于第三个条件:
当某个进程需要的资源被其他进程所占用的时候,可以由操作系统协助,将想要的资源强行剥夺。这种方式一般需要考虑各进程的优先级,并且实现起来比较复杂。此外,释放已获得的资源也可能造成前一阶段工作的失效,因此这种方法一般只适用于易保存和恢复状态的资源,如CPU
对于第四个条件:
给系统中的所有资源类型进行排序编号,每个进程只能按递增顺序申请资源。即进程申请了序号为n的资源后,下次只能申请序号为n+1或以上资源。如果进程后面又想申请序号低的资源,那就必须把现在拥有的序号为该资源及其以上的资源全部释放。
对于死锁的解决方法有很多种,我们应该根据实际情况具体分析具体判断。
这里就了解一下可重入函数和线程安全的联系和区别即可:
可重入函数和线程安全的联系:
函数是可重入的,那就是线程安全的 函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题 如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的
如果函数是可重入的,说明没有临界资源,那么就不会出现多个执行流访问一个数据的情况,反之同理,对于一个函数如果有全局变量,那么多个执行流都能访问,实际上就是第二个情况。
可重入函数和线程安全的区别:
可重入函数是线程安全函数的一种 线程安全不一定是可重入的,而可重入函数则一定是线程安全的。 如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的。
可重入函数通常是线程安全的,因为它们被设计为在多线程环境中安全地执行。但是,并不是所有线程安全的代码或函数都是可重入的。例如,一个使用全局变量但通过互斥锁保护的函数可能是线程安全的,但如果它在持有锁的同时调用了另一个可能也持有相同锁的不可重入函数,那么它就不是可重入的。
对于线程安全来说,STL中的几乎所有函数都不是可重入函数。
对于读写锁来说,同样存在21原则,即一个交易场所,两个角,三个关系,其中我们从名字来看,角分别是读者和写者,那么对于交易场所我们不妨看作是黑板报,写者写黑板报,读者读黑板报,那么对于关系呢?
如果关系和生产消费模型一样的,那么读写锁应该就没有存在的意义了吧?
读写锁 VS 生产消费模型
它们的一个本质区别就是,消费者是真真实实的要拿数据的,读者对于数据只是阅读,并不会做出任何处理,仅仅是读取。
三种关系
对于读者和读者来说,它们有关系吗?你读你的,我读我的,我们毫无关系。
对于写者和写者来说,它们的关系是一目了然的,我写的时候你不能写,你写的时候我不能写,这是一种典型的互斥关系。
对于读写和写者来说,我写的时候你不能读,万一你读取到的信息是不完整的,上法庭告我怎么办?因为当时本来就还没有写完,这是一种互斥关系。可是当我写完了,叫读者一声,读者就来读取了,这是一种典型的同步关系。
对于读者和写者来说,使用的锁也不是典型的互斥锁,因为读者之间是不需要加锁的,它们使用的锁是pthread_rwlock_init,pthread_rwlock_init,pthread_rwlock_rdlock,pthread_rwlock_wrlock,对于它们来说摧毁的函数都是pthread_rwlock_unlock。
函数的原型分别为:
代码语言:javascript代码运行次数:0运行复制int pthread_rwlock_init(pthread_rwlock_t *rwlock, ct pthread_rwlockattr_t *attr);
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
那么这里给一份示例代码:
代码语言:javascript代码运行次数:0运行复制// 共享资源
int shared_data = 0;
// 读写锁
pthread_rwlock_t rwlock;
// 读者线程函数
void *Reader(void *arg)
{
//sleep(1); //读者优先,一旦读者进入&&读者很多,写者基本就很难进入了
int number = *(int *)arg;
while (true)
{
pthread_rwlock_rdlock(&rwlock); // 读者加锁
std::cout << "读者-" << number << " 正在读取数据, 数据是: " << shared_data << std::endl;
sleep(1); // 模拟读取操作
pthread_rwlock_unlock(&rwlock); // 解锁
}
delete (int*)arg;
}
// 写者线程函数
void *Writer(void *arg)
{
int number = *(int *)arg;
while (true)
{
pthread_rwlock_wrlock(&rwlock); // 写者加锁
shared_data = rand() % 100; // 修改共享数据
std::cout << "写者- " << number << " 正在写入. 新的数据是: " << shared_data << std::endl;
sleep(2); // 模拟写入操作
pthread_rwlock_unlock(&rwlock); // 解锁
}
delete (int*)arg;
}
int main()
{
srand(time(nullptr)^getpid());
pthread_rwlock_init(&rwlock, ULL); // 初始化读写锁
// 可以更高读写数量配比,观察现象
ct int reader_num = 2;
ct int writer_num = 2;
ct int total = reader_num + writer_num;
pthread_t threads[total]; // 假设读者和写者数量相等
// 创建读者线程
for (int i = 0; i < reader_num; ++i)
{
int *id = new int(i);
pthread_create(&threads[i], ULL, Reader, id);
}
// 创建写者线程
for (int i = reader_num; i < total; ++i)
{
int *id = new int(i - reader_num);
pthread_create(&threads[i], ULL, Writer, id);
}
// 等待所有线程完成
for (int i = 0; i < total; ++i)
{
pthread_join(threads[i], ULL);
}
pthread_rwlock_destroy(&rwlock); // 销毁读写锁
return 0;
}
读写模型分为读者优先和写者优先,一般默认的都是读者优先。
对于自旋锁来说,它的原理和互斥锁几乎一样:
自旋锁通常使用一个共享的标志位(如一个布尔值)来表示锁的状态。当标志位为 true 时,表示锁已被某个线程占用;当标志位为false时,表示锁可用。当一个线程尝 试获取自旋锁时,它会不断检查标志位: • 如果标志位为false,表示锁可用,线程将设置标志位为true,表示自己占用了 锁,并进入临界区。 • 如果标志位为true(即锁已被其他线程占用),线程会在一个循环中不断自旋等 待,直到锁被释放。
对于资源来说,分为临界资源和非临界资源,我们平常几乎没有关心临界资源的执行时间问题,我们假设这么一个场景,执行流AB,A持有了锁,B自然应该挂起等待,那么B怎么挂起的你别管,反正数据是被cpu寄存器存储了,那么如果B挂起的时间只有1ms,A执行的时间有1秒,那么这个挂起无所谓,没有大消耗。
可是如果执行时间只有1ms,挂起的时间有1秒呢?多个执行流被挂起,此时cpu的切片的时间多了,执行的时间那么短,其他执行流就浪费了许多时间,如果执行流能够不挂起,一直查询呢?
此时,自旋锁就出来了,也就是执行流选择不挂起,一直轮询查看锁的状态,如果锁的状态是被占用,那么执行流就一直循环查看,直到锁被释放。
这种锁就叫做自旋锁。
但是实际上,自旋锁应用的场景十分少,虽然它减去了系统调度开销,减少了时间成本,但是它的缺点十分明显,如果持有锁的线程出问题了,那么其他所有的执行流都轮询锁,此时cpu资源肯定是被浪费了,而所有线程都在检测锁的状态无法进入临界区,此时引发的问题是活锁:
活锁的定义为: 活锁指的是任务或执行者没有被阻塞,但由于某些条件没有满足,导致它们一直重复尝试、失败、再尝试、再失败的状态。处于活锁的实体是在不断地改变状态,即所谓的“活”。
而因为原理部分自旋锁和互斥锁几乎一样,接口其实也差不了多少,所以直接给函数原型了:
代码语言:javascript代码运行次数:0运行复制int pthread_spin_init(pthread_spinlock_t *lock, int pshared);
int pthread_spin_destroy(pthread_spinlock_t *lock);
int pthread_spin_lock(pthread_spinlock_t *lock);
int pthread_spin_unlock(pthread_spinlock_t *lock);
给一段测试代码,对于自旋锁我们是人为感知不到它正在循环查询的,加上自旋锁应用的场景有临界区执行时间非常非常短,以及一下极其特殊的场景,所以就不过多介绍了:
代码语言:javascript代码运行次数:0运行复制#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
int ticket = 1000;
//pthread_spinlock_t lock;
void *route(void *arg)
{
char *id = (char *)arg;
while (1)
{
//pthread_spin_lock(&lock);
if (ticket > 0)
{
usleep(1000);
printf("%s sells ticket:%d\n", id, ticket);
ticket--;
//pthread_spin_unlock(&lock);
}
else
{
//pthread_spin_unlock(&lock);
break;
}
}
return nullptr;
}
int main(void)
{
//pthread_spin_init(&lock, PTHREAD_PROCESS_PRIVATE);
pthread_t t1, t2, t, t4;
pthread_create(&t1, ULL, route, (void *)"thread 1");
pthread_create(&t2, ULL, route, (void *)"thread 2");
pthread_create(&t, ULL, route, (void *)"thread ");
pthread_create(&t4, ULL, route, (void *)"thread 4");
pthread_join(t1, ULL);
pthread_join(t2, ULL);
pthread_join(t, ULL);
pthread_join(t4, ULL);
//pthread_spin_destroy(&lock);
}
以上,或者说初识Linux系统编程的文章就更新完了,大部分文章介绍的不是那么清楚的,博主会重制Linux系统编程的,算是对博主自己的一个交代啦,敬请期待!!
感谢阅读!
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。 原始发表:2024-12-20,如有侵权请联系 cloudcommunity@tencent 删除linux编程函数系统线程#感谢您对电脑配置推荐网 - 最新i3 i5 i7组装电脑配置单推荐报价格的认可,转载请说明来源于"电脑配置推荐网 - 最新i3 i5 i7组装电脑配置单推荐报价格
推荐阅读
留言与评论(共有 14 条评论) |
本站网友 商业地产策划案 | 19分钟前 发表 |
而可重入函数则一定是线程安全的 | |
本站网友 南非首都是哪里 | 4分钟前 发表 |
表示锁可用 | |
本站网友 国产玻尿酸价格 | 11分钟前 发表 |
对于死锁的解决方法有很多种 | |
本站网友 防辐射服哪个品牌好 | 16分钟前 发表 |
比如死锁 | |
本站网友 锤锤 | 19分钟前 发表 |
执行流AB | |
本站网友 新东方网站 | 9分钟前 发表 |
自旋锁就出来了 | |
本站网友 柯桥玉兰花园 | 26分钟前 发表 |
ULL); //pthread_spin_destroy(&lock); }以上 | |
本站网友 莲塘租房 | 20分钟前 发表 |
如果进程后面又想申请序号低的资源 | |
本站网友 天津一汽卡罗拉 | 4分钟前 发表 |
写者写黑板报 | |
本站网友 年终奖个税计算器 | 3分钟前 发表 |
对于资源来说 | |
本站网友 我有 | 14分钟前 发表 |
以及一下极其特殊的场景 | |
本站网友 榆林市政府 | 5分钟前 发表 |
但需要注意的是 | |
本站网友 签名代码 | 15分钟前 发表 |
反正数据是被cpu寄存器存储了 |