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封装的原子性操作如下:
store
:将值写入原子对象。load
:从原子对象读取值。exchange
:替换原子对象的值,并返回该对象之前的值。compare_exchange_weak
/compare_exchange_strong
:比较原子对象的值,并在相等时替换为新值。fetch_add
/fetch_sub
:对原子对象进行加/减操作,并返回操作前的值。fetch_and
/fetch_or
/fetch_xor
:对原子对象进行位与/位或/位异或操作,并返回操作前的值。++
/--
:原子地递增或递减对象的值。
-
一个小例子
#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。