【Linux(54),透彻分析源码

多执行流并发执行引起的数据竞争问题

if (tickets > 0)
{
    --tickets;
}

  1. 大部分情况,线程使用的数据都是局部变量,变量存储在线程栈空间内。这种情况,变量归属单个线程,其他线程无法访问这种变量。
  2. 很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互。比如,全局数据、堆空间数据。
  3. 多个线程并发的操作共享变量,会带来数据竞争,冲突以及数据不一致等问题。在不加保护的访问临界资源时即公共变量被多执行流同时访问和修改,会导致如同一编号的票被多个线程售出,某些线程售出了负数编号的票。除了多线程进程外,信号处理函数也是异步执行的(多执行流执行),同样存在数据竞争的问题。

并发运行问题

tickets > 0和–tickets操作并不是原子性操作,对应的操作:

  1. 将数据从内存加载到寄存器(当前线程的上下文中)
  2. 进行逻辑运算或算数运算
  3. 将数据写回内存

多核CPU允许多线程并行(同时)运行。在这三条步骤的其中任何一步,该线程都有可能被切换,切换前线程上下文会被保存。其他线程在执行时也对tickets进行了访问和修改。当原线程再次被CPU调度执行时,恢复上下文数据,此时的寄存器与内存就会发生数据不一致的错误。

2.pthread_mutex_t

为了解决数据竞争问题,可以使用互斥锁(Mutex)来保护对tickets变量的访问。互斥锁可以确保在同一时间只有一个线程能够访问临界区(对tickets变量的访问),从而避免数据竞争的发生。
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

pthread_mutex_t的源码

/\* Data structures for mutex handling. The structure of the attribute
 type is not exposed on purpose. \*/
typedef union
{
    struct \_\_pthread\_mutex\_s
    {
        int __lock;
        unsigned int __count;
        int __owner;
#ifdef \_\_x86\_64\_\_
        unsigned int __nusers;
#endif
        /\* KIND must stay at this position in the structure to maintain
 binary compatibility. \*/
        int __kind;
#ifdef \_\_x86\_64\_\_
        short __spins;
        short __elision;
        __pthread_list_t __list;
#define \_\_PTHREAD\_MUTEX\_HAVE\_PREV 1
/\* Mutex \_\_spins initializer used by PTHREAD\_MUTEX\_INITIALIZER. \*/
#define \_\_PTHREAD\_SPINS 0, 0
#else
        unsigned int __nusers;
        __extension__ union
        {
            struct
            {
                short __espins;
                short __elision;
#define \_\_spins \_\_elision\_data.\_\_espins
#define \_\_elision \_\_elision\_data.\_\_elision
#define \_\_PTHREAD\_SPINS \
 { \
 0, 0 \
 }
            } __elision_data;
            __pthread_slist_t __list;
        };
#endif
    } __data;
    char __size[__SIZEOF_PTHREAD_MUTEX_T];
    long int __align;
} pthread_mutex_t;

mutex的含义

在这里插入图片描述

初始化互斥量有两种方法:

方法1,静态分配:pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER。使用PTHREAD_ MUTEX_ INITIALIZER初始化的互斥量不需要销毁
方法2,动态分配:int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);.

互斥锁的基本使用方法

  1. 定义互斥锁变量:
    在使用互斥锁之前,先定义一个互斥锁变量。使用pthread_mutex_t类型来声明互斥锁变量,如:pthread_mutex_t mutex;
  2. 初始化互斥锁:在使用互斥锁之前,需要对互斥锁进行初始化。
    静态初始化:在定义互斥锁变量时,使用PTHREAD_MUTEX_INITIALIZER宏进行初始化。
    动态初始化:可以使用pthread_mutex_init函数来初始化互斥锁,例如pthread_mutex_init(&mutex, NULL);第一个参数是要初始化的互斥锁变量,第二个参数是互斥锁的属性,通常使用NULL表示使用默认属性;
  3. 对临界区加锁:在访问共享资源之前,需要加锁。使用pthread_mutex_lock函数来加锁,如:pthread_mutex_lock(&mutex);如果互斥锁已经被其他线程锁定,那么当前线程会被阻塞,直到互斥锁被解锁。
  4. 访问共享资源:在互斥锁被锁定的情况下,可以安全地串行访问共享资源。
  5. 解锁:在访问共享资源完成后,需要解锁互斥锁,以便其他线程可以继续访问共享资源。使用pthread_mutex_unlock函数来解锁,例如:pthread_mutex_unlock(&mutex);
  6. 销毁互斥锁:
    不再需要使用互斥锁时,需要将其销毁。使用pthread_mutex_destroy函数来销毁互斥锁,如:pthread_mutex_destroy(&mutex);
    静态初始化的互斥锁在程序结束时会自动被系统回收,无需手动销毁。
    不要销毁一个已经加锁的互斥量
    对于已经销毁的互斥量,要确保后面不会有线程再尝试加锁

3.pthread_mutex_lock()

在这里插入图片描述
在这里插入图片描述
在访问共享资源完成后,需要解锁互斥锁。否则,其他线程会被一直阻塞。需要特别注意break, goto等跳转语句跳过解锁函数。

被互斥锁锁定的临界区只能串行执行(互斥访问),虽然保证了多执行流访问临界资源的安全性,但是会在一定程度上降低程序的效率。

尽量保证被互斥锁锁定的代码都是访问临界资源的代码,不要将其他无关的操作也放入临界区中。因为相比并发或并行执行,临界区串行执行的效率较低。

调用pthread_ lock 时,可能会遇到以下情况

  1. 互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功
  2. 其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread_ lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁

4.time() or gettimeofday

在这里插入图片描述
在这里插入图片描述

详细解释

在 Linux 下,time() 和 gettimeofday() 函数都被用于获取当前的时间或时间戳,但它们之间有一些重要的区别。

time() 函数:
time() 函数返回从 Epoch(1970年1月1日 00:00:00 UTC)到当前时间的秒数。它的精度是秒级的。

函数原型:

c
#include <time.h> 
time_t time(time_t \*tloc);

如果 tloc 不是 NULL,则该函数也会将结果存储在 tloc 指向的内存位置。
如果成功,它返回自 Epoch 以来经过的秒数。
如果出错,它返回 -1。
示例:

c
#include <stdio.h> 
#include <time.h> 
  
int main() {  
    time_t current_time;  
    current_time = time(NULL);  
    if (current_time == -1) {  
        perror("time");  
        return 1;  
    }  
    printf("Current time since the Epoch: %ld\n", (long) current_time);  
    return 0;  
}

gettimeofday() 函数:
gettimeofday() 函数返回当前的时间,包括秒和微秒部分。因此,它的精度更高,可以达到微秒级。

函数原型:

c
#include <sys/time.h> 
int gettimeofday(struct timeval \*tv, struct timezone \*tz);

tv 是一个指向 timeval 结构的指针,该结构用于存储秒和微秒。
tz 是一个指向 timezone 结构的指针,但在现代系统中,它通常被设置为 NULL,因为时区信息可以通过其他方式获得。
如果成功,它返回 0。
如果出错,它返回 -1。
timeval 结构定义如下:

c
struct timeval {  
    time_t      tv_sec;     /\* seconds \*/  
    suseconds_t tv_usec;    /\* microseconds \*/  
};

示例:

c
#include <stdio.h> 
#include <sys/time.h> 
  
int main() {  
    struct timeval tv;  
    if (gettimeofday(&tv, NULL) == -1) {  
        perror("gettimeofday");  
        return 1;  
    }  
    printf("Current time: %ld.%06ld\n", (long)tv.tv_sec, (long)tv.tv_usec);  
    return 0;  
}

注意:

由于 gettimeofday() 使用了微秒级的精度,因此在某些系统上可能比其他时间函数(如 time())更慢。
在许多现代应用中,建议使用更高级的时间处理函数,如 clock_gettime(),它提供了更多的时钟选项和更高的精度。
在多线程环境中,time() 和 gettimeofday() 都不是线程安全的。如果需要线程安全的时间函数,应考虑使用 clock_gettime() 或其他同步机制。

5.锁的相关问题

linux下,mutex的工作原理是什么?它是如何保护临界区的?

在Linux下,互斥锁(mutex)是一种同步机制,用于确保在任何给定时刻只有一个线程或进程可以进入临界区,从而避免数据竞态条件和不一致性。以下是关于mutex工作原理、如何保护临界区以及如何让线程变安全的详细解释:

互斥锁的工作原理:

互斥锁基于原子操作和硬件支持来实现其同步机制。原子操作是一种不可分割的操作,即使在多核处理器上也是如此。Linux内核利用现代处理器提供的硬件指令(如“比较并交换”CMPXCHG指令)来支持原子操作,确保在执行操作期间不会被中断或其他线程干扰。

互斥锁内部有一个计数器(通常称为互斥量),其值只能为0或1。当线程需要进入临界区时,它会检查互斥锁的状态。如果计数器的值为0,表示当前没有线程占用临界区,该线程可以获取互斥锁并进入临界区。如果计数器的值为1,则表示临界区已被其他线程占用,该线程将被阻塞,直到互斥锁被释放。

保护临界区:

临界区是访问临界资源的代码段,这些资源一次只允许一个线程或进程访问。通过使用互斥锁,可以确保在任何时候只有一个线程能够进入临界区并执行相关操作。当线程进入临界区之前,它会尝试获取互斥锁。如果成功获取到锁,线程将执行临界区内的代码;如果锁已被其他线程占用,则当前线程将被阻塞,直到锁被释放。这种方式有效地保护了临界区,避免了多个线程同时访问临界资源导致的竞态条件。

线程安全:

线程安全是指多个线程访问某个方法或资源时,不管这些线程如何交替执行,每次运行的结果都与单线程运行的结果一致。线程安全问题通常由全局变量和静态变量引起。当多个线程同时对全局变量或静态变量进行写操作时,就可能导致数据不一致或其他问题。

通过使用互斥锁,可以确保对临界资源的互斥访问,从而实现线程安全。当线程需要访问临界资源时,它必须先获取互斥锁。这确保了在同一时间只有一个线程能够访问临界资源,从而避免了数据竞态条件。一旦线程完成对临界资源的访问,它会释放互斥锁,允许其他线程进入临界区。通过这种方式,互斥锁有效地保证了线程安全。

总之,Linux下的互斥锁通过原子操作、硬件支持和内核态调用实现其工作原理,保护临界区并确保线程安全。它是多线程编程中重要的同步机制之一,用于解决多线程环境中的竞态条件和数据不一致性问题。

加锁就是串行执行了吗?

是的!执行临界区代码一定是串行的!

结合Linux下的线程互斥与同步,简要介绍什么叫做原子性

在Linux下,线程互斥与同步是并发编程中的关键概念,它们共同确保多个线程在访问共享资源时的正确性和一致性。而原子性(atomicity)则是这些概念中的一个重要属性。

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Linux运维工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Linux运维全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Linux运维知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加VX:vip1024b (备注Linux运维获取)
img

最后的话

最近很多小伙伴找我要Linux学习资料,于是我翻箱倒柜,整理了一些优质资源,涵盖视频、电子书、PPT等共享给大家!

资料预览

给大家整理的视频资料:

给大家整理的电子书资料:

如果本文对你有帮助,欢迎点赞、收藏、转发给朋友,让我有持续创作的动力!

一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
img

最后的话

最近很多小伙伴找我要Linux学习资料,于是我翻箱倒柜,整理了一些优质资源,涵盖视频、电子书、PPT等共享给大家!

资料预览

给大家整理的视频资料:

[外链图片转存中…(img-UtFUge3k-1712587529799)]

给大家整理的电子书资料:

[外链图片转存中…(img-gTTYR1kQ-1712587529800)]

如果本文对你有帮助,欢迎点赞、收藏、转发给朋友,让我有持续创作的动力!

一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
[外链图片转存中…(img-jelldi6q-1712587529800)]

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值