『 Linux 』线程的资源共享,分离,以及多线程并发导致资源竞争

5 篇文章 0 订阅
4 篇文章 0 订阅


多线程的创建

请添加图片描述

可以在主线程中使用循环调用pthread_create()来创建一批线程(多线程);

一批线程在被创建后需要在主线程中使用数据结构进行管理;

#define NUM 5  // 定义线程数量


class threadData {
  // 设计了一个类用来封装线程的基本信息
 public:
  threadData(const pthread_t tid) : tid_(tid) {
    // 构造函数,初始化线程数据
    // 不在主线程中调用构造函数是为了在线程内部自行对属性进行初始化
    // 如果在主线程中直接调用构造函数进行初始化则无法正确获取其tid
    threadname_ = "Thread_" + std::to_string(tid % 1000);  // 简短线程名
  }

 public:
  pthread_t tid_;           // 线程ID
  std::string threadname_;  // 线程名称
};

void *threadRoutine(void *args) {
  pthread_t tid = pthread_self();  // 获取当前线程ID
  threadData self_data(tid);       // 创建线程数据对象
  printf("I am %s , the tid : %p\n", self_data.threadname_.c_str(),
         (void *)tid);  // 输出线程信息
  sleep(2);             // 模拟线程执行
  return nullptr;
}

int main() {
  std::vector<pthread_t> tids;  // 用于管理线程的tid

  // 创建线程
  for (size_t i = 0; i < NUM; ++i) {
    pthread_t tid;                                          // 线程ID
    pthread_create(&tid, nullptr, threadRoutine, nullptr);  // 创建线程并启动
    tids.push_back(
        tid);  // 将线程tid存放至STL容器中,方便主线程后期对这些线程进行管理
  }

  // 等待所有线程完成
  for (size_t i = 0; i < tids.size(); ++i) {
    pthread_join(tids[i], nullptr);  // 循环调用 pthread_join 等待线程
  }
    
  sleep(2);
  return 0;
}

在该段代码中设计了一个类,用来封装线程的基本属性,线程在创建后实例化该对象对自身基本属性进行管理描述;

若是主线程需要控制线程的描述对象可使用全局容器,当线程被创建成功后线程用new实例化出对应的对象并且将对象添加至该STL容器中使主线程方便进行控制;

使用#define定义了一个数值为5的宏NUM常量作为一批线程的线程个数;

主线程中使用了vector容器用来保存线程的tid以保证后期可以对线程进行join等待;

线程打印完对应信息后sleep(3)后退出;

主线程循环调用pthread_join()对线程进行等待,等待时第二个参数传递nullptr表示对该线程不关心,不需要知道该线程的具体结果;

等待完后主线程sleep(2)随后退出进程;

可在新窗口中使用shell脚本观察运行情况;

$ while :; 
do ps -aL | head -1 && ps -aL |  grep mythread ; 
echo "------------------------------------" ; 
sleep 1 ;
done

运行结果为:

# 程序所在会话
$ ./mythread 
I am Thread_936 , the tid : 0x7f632e633700
I am Thread_344 , the tid : 0x7f632f635700
I am Thread_640 , the tid : 0x7f632ee34700
I am Thread_232 , the tid : 0x7f632de32700
I am Thread_528 , the tid : 0x7f632d631700

# shell 脚本所在会话
  PID   LWP TTY          TIME CMD
13078 13078 pts/0    00:00:00 mythread
13078 13079 pts/0    00:00:00 mythread
13078 13080 pts/0    00:00:00 mythread
13078 13081 pts/0    00:00:00 mythread
13078 13082 pts/0    00:00:00 mythread
13078 13083 pts/0    00:00:00 mythread
------------------------------------
  PID   LWP TTY          TIME CMD
13078 13078 pts/0    00:00:00 mythread
13078 13079 pts/0    00:00:00 mythread
13078 13080 pts/0    00:00:00 mythread
13078 13081 pts/0    00:00:00 mythread
13078 13082 pts/0    00:00:00 mythread
13078 13083 pts/0    00:00:00 mythread
------------------------------------
  PID   LWP TTY          TIME CMD
13078 13078 pts/0    00:00:00 mythread
------------------------------------
  PID   LWP TTY          TIME CMD
13078 13078 pts/0    00:00:00 mythread
------------------------------------
  PID   LWP TTY          TIME CMD
------------------------------------

结果为创建了一批线程(5个)后线程打印自身信息而后在2s后退出;

主线程回收了子线程后过2s后退出,进程结束退出;


线程的资源共享

请添加图片描述

线程是进程中的一条执行流,本质上线程共享着进程的大部分资源,这些资源包括进程的进程地址空间,堆,全局变量和静态变量,代码段等等;

线程也需要有自己独立的数据,这些数据包括线程ID,线程特定数据,栈空间,寄存器内容等等;

在Linux中的线程是使用第三方pthread线程库维护的,其被动态链接在共享区中,当使用pthread库创建一个新的线程时,该线程中所保存的数据与Linux内核中的轻量型进程TCB结构体构成的才是一个线程的整体;

线程共享同一个进程的进程地址空间,除了栈;

  • 全局数据

    线程共享一个进程的全局数据,当一个全局数据被定义时,所有的线程可以共享该全局数据,包括访问与修改;

    以上文代码为例进行修改;

    #define NUM 5  
    
    int test_num = 0;  // 添加一个全局变量
    
    class threadData {
     public:
      threadData(const pthread_t tid) : tid_(tid) {
        threadname_ = "Thread_" + std::to_string(tid % 1000);
      }
    
     public:
      pthread_t tid_;          
      std::string threadname_;  
    };
    
    void *threadRoutine(void *args) {
      pthread_t tid = pthread_self();  
      threadData self_data(tid);       
      while (1) {
        test_num++;
        printf("I am %s , the tid : %p , the test_num = %d\n",
               self_data.threadname_.c_str(), (void *)tid,
               test_num);  // 输出线程信息 并输出test_num 数据
        sleep(1);
      }  
      return nullptr;
    }
    
    int main() {
      std::vector<pthread_t> tids;  
      for (size_t i = 0; i < NUM; ++i) {
        pthread_t tid;                                         
        pthread_create(&tid, nullptr, threadRoutine, nullptr);  
        tids.push_back(
            tid);  
      }
    
      for (size_t i = 0; i < tids.size(); ++i) {
        pthread_join(tids[i], nullptr);  
      }
    
      sleep(2);
    
      return 0;
    }
    

    在代码中添加了一个int类型的全局数据test_num并在线程中循环++同时打印出数据当前情况;

    运行结果为:

    $ ./mythread 
    I am Thread_56 , the tid : 0x7f9a292dc700 , the test_num = 1
    I am Thread_944 , the tid : 0x7f9a27ad9700 , the test_num = 2
    I am Thread_760 , the tid : 0x7f9a29add700 , the test_num = 3
    I am Thread_352 , the tid : 0x7f9a28adb700 , the test_num = 4
    I am Thread_648 , the tid : 0x7f9a282da700 , the test_num = 5
    I am Thread_56 , the tid : 0x7f9a292dc700 , the test_num = 6
    I am Thread_760 , the tid : 0x7f9a29add700 , the test_num = 7
    I am Thread_944 , the tid : 0x7f9a27ad9700 , the test_num = 9
    I am Thread_648 , the tid : 0x7f9a282da700 , the test_num = 10
    I am Thread_352 , the tid : 0x7f9a28adb700 , the test_num = 8
    ...
    

    结果为每个线程的每次所读取到的数据都不同,原因是全局数据可被统一进程中的线程共享;

    一般情况下在进程地址空间中除了栈以外其他数据都是被共享的,包括已初始化区,未初始化区,共享区,堆区,内核空间等;


线程的独立数据区

请添加图片描述

线程作为互相独立的执行流,其必定需要具有自己独立的数据区以确保线程的独立性和线程安全性;

通常线程的独立数据区有struct pthread结构体,线程局部存储,线程栈等等;

这些数据都被维护在共享区中的pthread线程库中;

  • struct pthread

    该结构体为pthread线程库用于维护管理一个线程的结构体,其大致结构为如下:

    // 简化
    struct pthread {
        pthread_t tid;                 // 线程ID
        void *stack;                   // 线程栈指针
        size_t stack_size;             // 线程栈大小
        pthread_attr_t attr;           // 线程属性
        int state;                     // 线程状态
        sigset_t sigmask;              // 信号掩码
        void *tls;                     // 线程局部存储
        int cancel_state;              // 取消状态
        int cancel_type;               // 取消类型
        int detached;                  // 分离状态标志
        pthread_mutex_t *mutexes;      // 线程互斥锁
        pthread_cond_t *cond;          // 条件变量
        int errno;                     // 错误代码
    };
    
  • 线程栈

    线程在进程中属于互相独立的执行流;

    线程的执行是由层次结构的,每个线程都有自己的调用栈,栈中的每一帧对应一个函数调用,当函数调用另一个函数时将会在当前栈帧的顶部创建一个新的栈帧;

    栈是每个线程的基础数据结构,用于管理函数调用和局部变量,每个线程都有自己的栈用于记录函数调用的层次和函数的局部变量;

    当线程调用一个函数时将会在其栈上分配一个新的栈帧,当函数返回时这个栈帧会被释放;

    可对最初的代码进行修改从而验证:

    #define NUM 5  // 需要创建一批线程的个数
    
    class threadData {
     public:
      threadData(const pthread_t tid) : tid_(tid) {
        threadname_ = "Thread_" + std::to_string(tid % 1000);
      }
    
     public:
      pthread_t tid_;
      std::string threadname_;
    };
    
    void *threadRoutine(void *args) {
      int test_num = 0; // 添加局部变量 test_num
    
      pthread_t tid = pthread_self();
      threadData self_data(tid);
      while (1) {
        printf("I am %s , the tid : %p , the test_num = %d , the &test_num is %p\n",
               self_data.threadname_.c_str(), (void *)tid, test_num,
               &test_num);  // 输出线程信息 并打印出线程中局部变量test_num的值与地址
        test_num++;
    
        sleep(1);
      }
      return nullptr;
    }
    
    int main() {
      std::vector<pthread_t> tids;
    
      for (size_t i = 0; i < NUM; ++i) {
        pthread_t tid;
        pthread_create(&tid, nullptr, threadRoutine, nullptr);
        tids.push_back(tid);
      }
    
      for (size_t i = 0; i < tids.size(); ++i) {
        pthread_join(tids[i], nullptr);
      }
    
      sleep(2);
    
      return 0;
    }
    

    其他代码不变,在线程中加入test_num的局部变量循环++,并每次打印出该变量的值与地址;

    运行结果为:

    $ ./mythread 
    I am Thread_144 , the tid : 0x7f017af2f700 , the test_num = 0 , the &test_num is 0x7f017af2eef4
    I am Thread_960 , the tid : 0x7f017cf33700 , the test_num = 0 , the &test_num is 0x7f017cf32ef4
    I am Thread_256 , the tid : 0x7f017c732700 , the test_num = 0 , the &test_num is 0x7f017c731ef4
    I am Thread_848 , the tid : 0x7f017b730700 , the test_num = 0 , the &test_num is 0x7f017b72fef4
    I am Thread_552 , the tid : 0x7f017bf31700 , the test_num = 0 , the &test_num is 0x7f017bf30ef4
    I am Thread_256 , the tid : 0x7f017c732700 , the test_num = 1 , the &test_num is 0x7f017c731ef4
    I am Thread_848 , the tid : 0x7f017b730700 , the test_num = 1 , the &test_num is 0x7f017b72fef4
    I am Thread_552 , the tid : 0x7f017bf31700 , the test_num = 1 , the &test_num is 0x7f017bf30ef4
    I am Thread_144 , the tid : 0x7f017af2f700 , the test_num = 1 , the &test_num is 0x7f017af2eef4
    I am Thread_960 , the tid : 0x7f017cf33700 , the test_num = 1 , the &test_num is 0x7f017cf32ef4
    ^C
    

    结果为由于是在各个线程中的栈中创建的局部变量,其每个值都由0开始自增,且地址都不同,分别为0x7f017af2eef4,0x7f017cf32ef4,0x7f017c731ef4,0x7f017b72fef4,0x7f017bf30ef4;

    线程的栈虽然是独立的但不代表其是私有的,具有私有属性的数据无法被任何其他执行流所看到,而线程栈上的数据可间接使用全局指针变量并判断条件将地址赋值给全局指针变量从而被其他线程访问并修改(该操作前提为线程的生命周期并未结束);

  • 线程局部存储

    线程局部存储本质上是指线程拥有独立的存储空间,这些存储空间中的变量在不同的线程中是互相隔离的,因此线程可以拥有一套属于自己的全局变量;

    确保不同线程之间的变量互不干扰;

    在Linux中可以使用__thread关键字对全局变量进行修饰,如__thread g_val = 0;

    __thread关键字是由编译器提供的,在使用pthread线程库时__thread可以用来定义每个线程的独立变量;

    #define NUM 5  
    
    __thread int g_val = 0; // 使用 __thread 修饰全局变量 g_val
    
    class threadData {
     public:
      threadData(const pthread_t tid) : tid_(tid) {
        threadname_ = "Thread_" + std::to_string(tid % 1000);
      }
    
     public:
      pthread_t tid_;
      std::string threadname_;
    };
    
    void *threadRoutine(void *args) {
    
      pthread_t tid = pthread_self();
      threadData self_data(tid);
      while (1) {
        printf("I am %s , the tid : %p , the g_val = %d , the &g_val is %p\n",
               self_data.threadname_.c_str(), (void *)tid, g_val,
               &g_val);  // 输出线程信息 并打印出全局变量 g_val 的值与地址
        g_val++;
    
        sleep(1);
      }
      return nullptr;
    }
    
    int main() {
      std::vector<pthread_t> tids;
    
      for (size_t i = 0; i < NUM; ++i) {
        pthread_t tid;
        pthread_create(&tid, nullptr, threadRoutine, nullptr);
        tids.push_back(tid);
      }
    
      for (size_t i = 0; i < tids.size(); ++i) {
        pthread_join(tids[i], nullptr);
      }
    
      return 0;
    }
    

    在该段代码中使用__thread声明了一个全局变量int g_val并设置初始值为0;

    每个线程都会对该全局变量进行++,并打印出对应的结果与地址;

    运行结果为:

    $ ./mythread 
    I am Thread_992 , the tid : 0x7fcab8cc8700 , the g_val = 0 , the &g_val is 0x7fcab8cc86fc
    I am Thread_104 , the tid : 0x7fcaba4cb700 , the g_val = 0 , the &g_val is 0x7fcaba4cb6fc
    I am Thread_696 , the tid : 0x7fcab94c9700 , the g_val = 0 , the &g_val is 0x7fcab94c96fc
    I am Thread_400 , the tid : 0x7fcab9cca700 , the g_val = 0 , the &g_val is 0x7fcab9cca6fc
    I am Thread_288 , the tid : 0x7fcab84c7700 , the g_val = 0 , the &g_val is 0x7fcab84c76fc
    I am Thread_104 , the tid : 0x7fcaba4cb700 , the g_val = 1 , the &g_val is 0x7fcaba4cb6fc
    I am Thread_696 , the tid : 0x7fcab94c9700 , the g_val = 1 , the &g_val is 0x7fcab94c96fc
    I am Thread_400 , the tid : 0x7fcab9cca700 , the g_val = 1 , the &g_val is 0x7fcab9cca6fc
    I am Thread_992 , the tid : 0x7fcab8cc8700 , the g_val = 1 , the &g_val is 0x7fcab8cc86fc
    I am Thread_288 , the tid : 0x7fcab84c7700 , the g_val = 1 , the &g_val is 0x7fcab84c76fc
    ^C
    

    其中每个g_val的值都由0开始,同时其对应的地址都不相同,地址后五位分别为c86fc,cb6fc,c96fc,ca6fc,c76fc;

    该关键字只能用来修饰内置类型,无法修饰自定义类型(包括C++中的容器等);


线程的分离

请添加图片描述

分离线程(Detached Thread)指一个独立运行的线程,主线程不需要等待它的结束;

当分离线程完成任务并退出后,系统将会自动回收它的资源;

pthread库中可以使用pthread_detach()函数将其设置为分离状态;

一但线程被分离后主线程无法通过pthread_join()函数等待该线程结束,也无法获得该线程的返回值;

NAME
       pthread_detach - detach a thread

SYNOPSIS
       #include <pthread.h>

       int pthread_detach(pthread_t thread);

       Compile and link with -pthread.

DESCRIPTION
       The pthread_detach() function marks the thread identified by thread as detached.  When a detached
       thread terminates, its resources are automatically released back to the system without  the  need
       for another thread to join with the terminated thread.

       Attempting to detach an already detached thread results in unspecified behavior.

RETURN VALUE
       On success, pthread_detach() returns 0; on error, it returns an error number.

函数调用成功时返回0,调用失败时返回一个错误码即非0;

  • pthread_t thread

    该参数为传递一个需要分离线程的tid;

#define NUM 3  // 需要创建一批线程的个数

class threadData {
 public:
  threadData(const pthread_t tid) : tid_(tid) {
    threadname_ = "Thread_" + std::to_string(tid % 1000);
  }

 public:
  pthread_t tid_;
  std::string threadname_;
};

void *threadRoutine(void *args) {
  pthread_t tid = pthread_self();
  threadData self_data(tid);
  while (1) {
    printf("I am %s , the tid : %p\n", self_data.threadname_.c_str(),
           (void *)tid);
    sleep(1);
  }
  return nullptr;
}

int main() {
  std::vector<pthread_t> tids;

  for (size_t i = 0; i < NUM; ++i) {
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, nullptr);
    tids.push_back(tid);
  }

  for (int i = 0; i < NUM; ++i) {
    // 循环调用 pthread_detach() 来分离所有线程
    pthread_detach(tids[i]);
  }

  for (size_t i = 0; i < tids.size(); ++i) {
    int n = pthread_join(tids[i], nullptr);
    if (n) {
      printf("thread %p join fail , the n is %d\n", (void *)tids[i], n);
    }
  }
  sleep(2);

  return 0;
}

代码中将线程的数量控制到了3个;

创建一批线程,线程打印自己的基础信息,同时主线程循环调用pthread_detac()来分离线程,并且循环使用pthread_join()等待线程;

若是线程等待失败则打印等待失败;

运行结果为:

$ ./mythread 
thread 0x7f656fdb3700 join fail , the n is 22
thread 0x7f656f5b2700 join fail , the n is 22
thread 0x7f656edb1700 join fail , the n is 22
I am Thread_184 , the tid : 0x7f656f5b2700
I am Thread_888 , the tid : 0x7f656fdb3700
I am Thread_480 , the tid : 0x7f656edb1700
I am Thread_184 , the tid : 0x7f656f5b2700
I am Thread_888 , the tid : 0x7f656fdb3700
I am Thread_480 , the tid : 0x7f656edb1700

结果为线程分离后主线程对其他线程进行join等待时等待失败,当pthread_join()调用失败时将返回一个错误码,对于join一个分离线程时返回的错误码为22;

#define	EINVAL		22	/* Invalid argument */

即无效参数,表明分离线程无法joinable;

同时该结果为提前结束的原因本质是因为主线程结束,这表明无论线程分离与否,主线程都必须最后退出;

线程的分离可由其他线程执行也可由线程自身执行,线程自己执行时只需要调用pthread_detach(pthread_self())即可;

本质上线程的分离是pthread线程库将其TCB结构体中关于是否可以join的标志位进行转换;


多线程并发导致数据不一致

请添加图片描述

当一个进程中的多个线程同时对一共享资源进行访问操作时将出现数据不一致的问题;

#define NUM 5  // 需要创建一批线程的个数

int g_val = 700;  // 创建一个全局变量作为共享资源

class threadData {
  /* 封装一个类 作为线程的基本信息存储 */
 public:
  threadData(int number) { threadname_ = "Thread_" + to_string(number); }

 public:
  std::string threadname_;
};

void *threadRoutine(void *args) {
  int *arg = static_cast<int *>(args);
  threadData self_data(*arg);
  while (true) {
    // 每个线程都对该全局变量进行访问
    if (g_val > 0) {
      // g_val > 0 时打印对应值并对该g_val进行 -- 操作
      usleep(100);
      printf("I am %s , g_val = %d\n", self_data.threadname_.c_str(), g_val);
      --g_val;
    } else {
      // g_val < 0 时break 退出
      break;
    }
  }
  return nullptr;
}

int main() {
  std::vector<pthread_t> tids;

  for (size_t i = 0; i < NUM; ++i) {
    // 循环创建一批线程
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, &i);
    tids.push_back(tid);
  }

  for (size_t i = 0; i < tids.size(); ++i) {
    // 循环 join 这批线程
    pthread_join(tids[i], nullptr);
  }
  return 0;
}

该代码中创建了一批线程,并定义了一个int类型值为700g_val全局变量作为共享资源;

每个线程都将对该共享资源进行操作,当g_val > 0时进行--操作,否则break跳出循环;

运行结果为:

$ ./mythread 
I am Thread_4 , g_val = 700
I am Thread_3 , g_val = 700
I am Thread_1 , g_val = 698
I am Thread_2 , g_val = 698
I am Thread_5 , g_val = 696
I am Thread_4 , g_val = 695
I am Thread_3 , g_val = 694
I am Thread_1 , g_val = 693
I am Thread_2 , g_val = 692
I am Thread_5 , g_val = 691
...
...
I am Thread_5 , g_val = 1
I am Thread_4 , g_val = 0
I am Thread_1 , g_val = -1
I am Thread_3 , g_val = -2
I am Thread_2 , g_val = -3

从结果看出,代码设置了条件判断为g_val > 0,而最终的结果甚至被减到了-3,即为多线程并发所导致的数据不一致问题;

是因为本质上自减操作是一个不具有原子性的操作,即不安全操作;

--操作在汇编代码中将分为三步:

  • 将数据加载至寄存器中

    mov eax, [var]
    
  • 进行减1操作

    dec eax
    
  • 存储新值回内存

    mov [var], eax
    

但各个线程都被分配了一定的时间片,可能当该线程刚执行完某一步时时间片就被用完,从而需要保存上下文从而后切换给另一个线程;

假设存在两个线程( 线程1与线程2 )对g_val进行操作且此时g_val1,可能出现以下情况:

  • 线程1判断条件并准备修改

    线程1检查 g_val 的值为1,判断 g_val > 0 为真,准备执行 --g_val;

    在执行 --g_val 之前,线程1的时间片用完,进行上下文切换,线程1的寄存器状态(包括 g_val 的值)被保存;

  • 线程2进行完整操作:

    线程2被调度执行,也检查 g_val 的值为1,判断 g_val > 0 为真;

    线程2执行 --g_val,g_val 变为0,并将结果存回内存;

  • 线程1恢复并继续操作:

    线程1被重新调度执行,恢复其上下文,此时寄存器中的 g_val 值为上次判断的值,在计算--时需要重新将内存中的g_val加载进寄存器中;

    线程1并重新加载 g_val 值(此时已变为0)进寄存器并执行 --g_val 操作;

    线程1执行 --g_val,寄存器中的值变为0,然后存回内存;

该问题即为多线程并发产生数据竞争从而导致数据不一致的问题;

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Dio夹心小面包

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值