02_muduo_base1

5.3 At0mic源码剖析

为什么需要原子性操作:在多线程环境下,一次简单的加法操作:先从内存读取数据到寄存器,然后进行加法,最后再把数据写回内存。这是由于多线程环境下,在寄存器上的加法到写回内存这个动作不是当成一个动作执行的,而是被划分了为三个动作,导致问题。

解决方案:第一个就是上锁(lock->x++->unlock),但是这样子多个线程会导致锁竞争,这会导致并发量的下降。第二个就是原子性操作(将读取内存到寄存器,寄存器加法,写回内存)当成一个整体。

继承基类:

class AtomicIntegerT : noncopyable	//这个类是对象语义,不可拷贝

数据成员:

volatile T value_;	//加上volatile确保该指令不会被编译器优化而省略,具体来说就是每次都是重新在内存中读取数据,而不是使用在寄存器中的备份。

一些接口:

  T get(){	//获取当前value的值,并且是一个原子性操作,也就是线程安全
    return __sync_val_compare_and_swap(&value_, 0, 0);
  }
  T getAndAdd(T x){//原子性的获取value值并且加上x,主要返回的是没有修改的值
    return __sync_fetch_and_add(&value_, x);
  }
  T addAndGet(T x){//先加再获取,返回修改之后的值
    return getAndAdd(x) + x;
  }

C++11封装的原子性操作如下:

  1. store:将值写入原子对象。
  2. load:从原子对象读取值。
  3. exchange:替换原子对象的值,并返回该对象之前的值。
  4. compare_exchange_weak / compare_exchange_strong:比较原子对象的值,并在相等时替换为新值。
  5. fetch_add / fetch_sub:对原子对象进行加/减操作,并返回操作前的值。
  6. fetch_and / fetch_or / fetch_xor:对原子对象进行位与/位或/位异或操作,并返回操作前的值。
  7. ++ / --:原子地递增或递减对象的值。
  • 一个小例子

    #include <iostream>
    #include <thread>
    #include <atomic>
    const int MAX = 1000;
    std::atomic_int total(0);//定义一个原子类型的变量
    void threadTask() {
        for (int i = 0; i < MAX; i++) {
            total.fetch_add(1);//进行原子操作
        }
    }
    

    一些额为信息

5.4 Exception类源码剖析⭐

数据成员:message:异常信息字符串。stack:异常发生时候栈回溯的信息。

成员方法:what()返回message,stackTrace()返回stack。fillStackTrace()记录异常发生栈回溯的信息【注意在C++11版本中已经没有这个函数了】。

继承标准库中的exception类。远古版本中fillStackTrace()包含了两个函数。一个是backtrace(),栈回溯,保存各个栈帧的地址。还有一个是backtrace_symbols()根据地址转换为相应的函数符号。

5.5 Thread线程的封装⭐⭐⭐

类图

线程标识符

在Linux中,每一个进程有一个pid,类型是pid_t。可以通过getpid()获取。Linux下的POSIX(只能在unix-linux环境使用)线程也有一个id,类型是pthread_t,由pthread_self()获取,改id是由线程库维护,其id是各个进程独立的,也就是各个进程可能有相同的id。Linux中POSIX线程库实现的线程本质上是一个轻量级进程,这是该进程与主进程共享一些资源。

但是有的时候我们可能需要线程的真实pid,比如进程P1先进程P2发送信号时,既不能使用P2的pid,更不能使用线程的pthread id(一方面是因为POSIX维护的是各个进程中的线程,这里的线程id并不是真实的线程id,第二个方面是发送信号通常是在操作系统的进程层次处理的,POSIX尽管是一个进程有多个线程,但是本质还是在进程的环境下执行的。),而是只能使用该线程的真实pid,也就是tid。

可以使用gettid()获取tid,但是glibc并没有实现该函数。只能通过Linux的系统调用syscall()获取,也就是使用return syscall(SYS_gettid);这里会将返回的真实id存储起来,避免频繁的调用。

代码

class Thread : noncopyable{
 public:
  typedef std::function<void ()> ThreadFunc;
  explicit Thread(ThreadFunc, const string& name = string());
  // FIXME: make it movable in C++11
  ~Thread();

  void start();
  int join(); // return pthread_join()

  bool started() const { return started_; }
  // pthread_t pthreadId() const { return pthreadId_; }
  pid_t tid() const { return tid_; }
  const string& name() const { return name_; }

  static int numCreated() { return numCreated_.get(); }

 private:
  void setDefaultName();

  bool       started_; 				//线程是否启动
  bool       joined_;
  pthread_t  pthreadId_;			//线程pthreadid
  pid_t      tid_;					//线程的真实id
  ThreadFunc func_;					//线程的回调函数
  string     name_;					//线程名称
  CountDownLatch latch_;

  static AtomicInt32 numCreated_;	//已经创建的线程个数
};

额外知识

__thread关键字:gcc内置的线程局部存储设施。只能修饰POD(plain old data)类型,与C语言兼容,也就是C语言中的类型都是POD类型。对于像用户定义的构造函数或者虚函数不是POD类型,但是一个类如果没有构造函数之类的也算POD类型。一个小例子。

__thread string obj1("hello");//error,因为会调用构造函数
__thread string* obj2 = new string; //error,__thread变量初始化要求是编译时常量,而new string是一个运行时表达式。
//在fork时,也就是真正实现数据拷贝/创建子线程之前父进程会先调用prepare,创建成功之后,父进程会调用parent,子进程会调用child
int pthread_atfork(void(*prepare)(void), void(*parent)(void), (void)(*child)(void));

线程的一次创建流程

void afterFork(){
  muduo::CurrentThread::t_cachedTid = 0;
  muduo::CurrentThread::t_threadName = "main";
  CurrentThread::tid();
}
ThreadNameInitializer(){
    muduo::CurrentThread::t_threadName = "main";
    CurrentThread::tid();
    //如果fork,那么子线程会调用afterFork()函数
    pthread_atfork(NULL, NULL, &afterFork);
  }

为什么子线程会把修改tid和name。无论是在主线程还是在子线程中fork,创建出来的新进程都是继承了原有线程的部分状态,但是这个新进程和原有线程所在的线程其实是两个独立的执行实体。所以需要重新设置一下信息。

多线程fork造成死锁的场景

父进程创建一个线程,并对mutex上锁。此时父进程会把自己的内存状态等信息拷贝到子进程。但是对于线程来说,子进程只会复制调用fork的那个特定线程状态,而不是父进程中全部状态。这样子会导致原来父进程如果存在多个线程,线程A上锁,解锁。但是fork的线程是线程B,但是线程B没有上锁、解锁能力。这样子就会导致子进程死锁。注意这里互斥锁子进程也会拷贝,这里的拷贝是指将互斥锁的状态也拷贝也拷贝过去,但是由于两个进程是独立的内存空间,父进程对互斥锁的修改并不会影响到子进程。

非POD类型数据如何进行线程局部存储

线程特定数据tsd。

posted @ 2024-11-08 10:28  炫炫子  阅读(3)  评论(0编辑  收藏  举报