【Linux】线程安全——补充|互斥、锁|同步、条件变量(上)

简介: 【Linux】线程安全——补充|互斥、锁|同步、条件变量(上)

> 作者:დ旧言~

> 座右铭:松树千年终是朽,槿花一日自为荣。

> 目标:理解【Linux】线程安全——补充|互斥、锁|同步、条件变量。

> 毒鸡汤:有些事情,总是不明白,所以我不会坚持。早安!

> 专栏选自:Linux初阶

> 望小伙伴们点赞👍收藏✨加关注哟💕💕



🌟前言

每建造一个建筑物时,我们都需要考虑这个建筑物的安全问题,为每一人的安全做个保证,在Linux下的线程其实也有安全隐患,那我们该如何维护线程的安全呢?本篇博客带大家了解Linux下的线程安全。


⭐主体

学习【Linux】线程安全——补充|互斥、锁|同步、条件变量咱们按照下面的图解:



🌙 知识补充


💫 线程的ID

概念:


pthread_create创建一个线程,产生一个线程ID存放在第一个参数之中,该线程ID与内核中的LWP并不是一回事。pthread_create函数第一个参数指向一块虚拟内存单元,该内存单元的地址就是新创建线程ID,这个ID是线程库的范畴,而内核中LWP是进程调度的范畴,轻量级进程是OS调度的最小单位,需要一个数值来表示该唯一线程。


Linux并不提供真正的线程,只提供了LWP,但是程序员用户不管LWP,只要线程。所以OS在OS与应用程序之间设计了一个原生线程库,pthread库,系统保存LWP,原生线程库可能存在多个线程,别人可以同时在用。OS只需要对内核执行流LWP进行管理,而提供用户使用的线程接口等其他数据则需要线程库自己来管理。所以线程库需要对线程管理“先描述,在组织”。


线程库实际上就是一个动态库:



进程运行时动态库加载到内存,然后通过页表映射到进程地址空间的共享区,这时候进程的所有线程都是能看到这个动态库的:



每个线程都有自己独立的栈:主线程采用的栈是进程地址空间中原生的栈,其他线程采用的是共享区中的栈,每个线程都有自己的struct pthread,包含了对应线程的属性,每个线程也有自己的线程局部存储(添加__thread可以将一个内置类型设置为线程局部存储),包含对应的线程被切换时的上下文。每一个新的线程在共享区都有一块区域对其描述,所以我们要找到一个用户级线程只需要找到该线程内存块的起始地址就可以获取到该线程的信息了:



线程函数起始是在库内部对线程属性进行操作,最后将要执行的代码交给对应的内核级LWP去执行。所以线程数据的管理本质在共享区。线程ID本质就是进程地址空间共享区上的一个虚拟地址:

#include <stdio.h>
#include <bits/pthreadtypes.h>
#include <pthread.h>
#include <unistd.h>
 
void* start_routine(void*args)
{
    while(true)
    {
        printf("new thread tid:%p\n",pthread_self());
        sleep(2);
    }
}
 
int main()
{
    pthread_t tid;
    pthread_create(&tid,nullptr,start_routine,nullptr);
    while(true)
    {
        printf("main thread tid:%p\n",pthread_self());
    }
    return 0;
}


💫 局部存储验证

给一个全局变量g_val,让一个线程进行++,其他线程会受到影响:

#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;
int g_val = 100;
void* start_routine(void*args)
{
    string name = static_cast<const char*>(args);
    while(true)
    {
        cout<<name<<" running ... "<<"g_val: "<<g_val<<"&g_val: "<<&g_val<<endl;
        sleep(1);
        ++g_val;
    }
}
int main()
{
    pthread_t tid;
    pthread_create(&tid,nullptr,start_routine,(void*)"thread 1");
    while(true)
    {
        printf("main thread g_val: %d &g_val: 0x%x\n",g_val,&g_val);
        sleep(1);
    }
    pthread_join(tid,nullptr);
    return 0;
}



此时在给全局变量g_val加上__thread:

从输出结果上看,g_val此时就不再是共享了,每个线程独有。添加__thread可以将一个内置类型设置为线程局部存储。每个线程都有一份,介于全局变量和局部变量之间线程特有属性。



💫 Thread.hpp——线程的封装

我们如果想要跟C++一样使用,创建使用线程时,直接构造对象设置回调函数,对线程原生接口可以进行简单的封装:

mythread.cc:

#include <iostream>
#include <pthread.h>
#include <cassert>
#include <cstring>
#include <functional>
class Thread;
//上下文
class Context
{
public:
    Thread *this_;
    void *args_;
public:
    Context():this_(nullptr),args_(nullptr)
    {}
    ~Context()
    {}
};
class Thread
{
public:
    typedef std::function<void*(void*)> func_t;
    const int num = 1024;
public:
    Thread(func_t func,void*args,int number):func_(func),args_(args)
    {
        char buffer[num];
        snprintf(buffer,sizeof(buffer),"thread-%d",number);
        name_=buffer;
        Context*ctx = new Context();
        ctx->this_ = this;
        ctx->args_=args_;
        int n = pthread_create(&tid_,nullptr,start_routine,ctx);
        assert(n==0);
        (void)n;
    }
    static void*start_routine(void*args)
    {
        Context*ctx = static_cast<Context*>(args);
        void*ret = ctx->this_->run(ctx->args_);
        delete ctx;
        return ret;
    }
    void join()
    {
        int n = pthread_join(tid_,nullptr);
        assert(n==0);
        (void)n;
    }
    void*run(void*args)
    {
        return func_(args);
    }
 
    ~Thread()
    {}
 
private:
    std::string name_;
    pthread_t tid_;
    func_t func_;
    void* args_;
};


main.cc:

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <memory>
#include "mythread.cc"
 
using namespace std;
 
void* thread_run(void* args)
{
    std::string name = static_cast<const char *>(args);
    while(true)
    {
        cout << name << endl;
        sleep(1);
    }
}
int main()
{
    std::unique_ptr<Thread> thread1(new Thread(thread_run,(void*)"hellothread",1));
    std::unique_ptr<Thread> thread2(new Thread(thread_run,(void*)"COUTthread",2));
    std::unique_ptr<Thread> thread3(new Thread(thread_run,(void*)"PRINTthread",3));
 
    //thread1->start();
    //thread2->start();
    //thread3->start();
    thread1->join();
    thread2->join();
    thread3->join();
    return 0;
}



🌙 线程安全问题

线程安全:

在多个执行流中对同一个临界资源进行操作访问, 而不会造成数据二义性,也就是说, 在拥有共享数据的多条线程并行执行的程序中, 通过同步和互斥机制保证各个线程都可以正常且正确的执行, 不会出现数据污染等意外情况, 也就是保证了线程安全。


下面模拟抢票的过程,多个线程对共享资源tickets做–的过程:


#include<iostream>
#include<cstdio>
#include<cstring>
#include<unistd.h>
#include<pthread.h>
using namespace std;
 
int ticket = 30;//票数
 
void* route(void *arg){
    while(ticket > 0){
        usleep(100);
        --ticket;
        printf("第%d位黄牛抢到票, 还剩%d张\n", *(int*)arg, ticket);
    }
    return NULL;
}
int main(){
    pthread_t tid[5];
    int n[5] = {1,2,3,4,5};
    for(int i = 0; i < 5; ++i){
        int ret = pthread_create(&tid[i], NULL, route, n + i);
        if(ret){
            fprintf(stderr, "pthread_create:%s\n", strerror(ret));
        }
    }
    for(int i = 0; i < 5; ++i){
        pthread_join(tid[i], NULL);
    }
    return 0;
}



此时结果出现了负数,在现实生活中,抢票怎么可能出现负数


结果出现负数:


这是因为, 当多个线程同时对一个临界资源访问时, 就有很大的可能会出现数据二义性的问题, 就比如上面的例子, 当还有最后一张票时, 第一个黄牛的线程判断ticket > 0 进入循环抢票, 但此时如果ticket还没有减一, 这个线程就被挂起等待, 操作系统给CPU调度了其他的黄牛线程, 此时其他线程判断ticket > 0, 也就入了抢票循环, 这就出问题了, 同一张票, 本该抢完就结束的, 但这样一来就会出现上面例子问题发生, 此时线程是不安全的.  总结一下就是


  1. while判断后, 可以并发的切换到其他线程
  2. usleep模拟一段业务过程, 在这段过程中, 可能会并发多个线程也运行到这段代码
  3. --ticket本身就不是一个原子操作如果调试来看的话,  --ticket一个语句要对应三条汇编指令


而解决这种问题的办法就是加锁!(后面详细介绍这个内容)



【Linux】线程安全——补充|互斥、锁|同步、条件变量(下)    https://developer.aliyun.com/article/1565760

目录
相关文章
|
1月前
|
资源调度 Linux 调度
Linux C/C++之线程基础
这篇文章详细介绍了Linux下C/C++线程的基本概念、创建和管理线程的方法,以及线程同步的各种机制,并通过实例代码展示了线程同步技术的应用。
26 0
Linux C/C++之线程基础
|
1月前
|
安全 Linux
Linux线程(十一)线程互斥锁-条件变量详解
Linux线程(十一)线程互斥锁-条件变量详解
|
2月前
|
存储 Java 程序员
优化Java多线程应用:是创建Thread对象直接调用start()方法?还是用个变量调用?
这篇文章探讨了Java中两种创建和启动线程的方法,并分析了它们的区别。作者建议直接调用 `Thread` 对象的 `start()` 方法,而非保持强引用,以避免内存泄漏、简化线程生命周期管理,并减少不必要的线程控制。文章详细解释了这种方法在使用 `ThreadLocal` 时的优势,并提供了代码示例。作者洛小豆,文章来源于稀土掘金。
|
2月前
|
存储 Ubuntu Linux
C语言 多线程编程(1) 初识线程和条件变量
本文档详细介绍了多线程的概念、相关命令及线程的操作方法。首先解释了线程的定义及其与进程的关系,接着对比了线程与进程的区别。随后介绍了如何在 Linux 系统中使用 `pidstat`、`top` 和 `ps` 命令查看线程信息。文档还探讨了多进程和多线程模式各自的优缺点及适用场景,并详细讲解了如何使用 POSIX 线程库创建、退出、等待和取消线程。此外,还介绍了线程分离的概念和方法,并提供了多个示例代码帮助理解。最后,深入探讨了线程间的通讯机制、互斥锁和条件变量的使用,通过具体示例展示了如何实现生产者与消费者的同步模型。
|
3月前
|
Linux API 调度
重温Linux内核:互斥和同步
本文全面回顾了Linux内核中的互斥和同步机制,包括中断屏蔽、原子变量、自旋锁、读写锁、顺序锁、信号量、互斥量、RCU机制以及完成量等,提供了它们的定义、实现原理、API用法和使用时的注意事项。
55 0
|
1月前
|
存储 消息中间件 资源调度
C++ 多线程之初识多线程
这篇文章介绍了C++多线程的基本概念,包括进程和线程的定义、并发的实现方式,以及如何在C++中创建和管理线程,包括使用`std::thread`库、线程的join和detach方法,并通过示例代码展示了如何创建和使用多线程。
40 1
C++ 多线程之初识多线程
|
20天前
|
Java 开发者
在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口
【10月更文挑战第20天】在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口。本文揭示了这两种方式的微妙差异和潜在陷阱,帮助你更好地理解和选择适合项目需求的线程创建方式。
15 3
|
20天前
|
Java 开发者
在Java多线程编程中,选择合适的线程创建方法至关重要
【10月更文挑战第20天】在Java多线程编程中,选择合适的线程创建方法至关重要。本文通过案例分析,探讨了继承Thread类和实现Runnable接口两种方法的优缺点及适用场景,帮助开发者做出明智的选择。
14 2
|
20天前
|
Java
Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口
【10月更文挑战第20天】《JAVA多线程深度解析:线程的创建之路》介绍了Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口。文章详细讲解了每种方式的实现方法、优缺点及适用场景,帮助读者更好地理解和掌握多线程编程技术,为复杂任务的高效处理奠定基础。
27 2
|
20天前
|
Java 开发者
Java多线程初学者指南:介绍通过继承Thread类与实现Runnable接口两种方式创建线程的方法及其优缺点
【10月更文挑战第20天】Java多线程初学者指南:介绍通过继承Thread类与实现Runnable接口两种方式创建线程的方法及其优缺点,重点解析为何实现Runnable接口更具灵活性、资源共享及易于管理的优势。
27 1