高并发服务器架构笔记(3)——muduo_base 源码分析

来源:互联网 发布:熊猫加速器mac 编辑:程序博客网 时间:2024/06/07 12:43

Timestamp 类

  • Timestamp 类封装
    • < base/types.h >
    • less_than_comparable
      • 要求实现 <,可自动实现 > , <=, >=
    • Boost_STATIC_ASSERT
    • 使用 PRId64
    • Timestamp 实现及测试
class Timestamp : public muduo::copyable,                  public boost::less_than_comparable<Timestamp>{//...}
  • C++ 变量的两种语义
    • 值语义:可已拷贝的,拷贝之后,与原对象脱离关系。
    • 对象语义:
      • 要么不能拷贝
      • 要么可以拷贝,但拷贝之后与原对象仍然存在一定的关系,比如共享底层资源。需要自己实现拷贝构造函数。
  • muduo::copyable空基类,标识类,值类型,表示可复制。
  • microSecondsSinceEpoch_ 64位整形,表示距离 1970-01-01 的微秒数。
  • BOOST_STATIC_ASSERT 编译时断言,ASSERT 运行时断言。分别在编译时和运行时报错。
  • PRId64 是为了跨平台打印int64_t

    • 如果不用这个宏的话
      • printf("%ld", value); // 64 位系统
      • printf("%lld", value); // 32 位系统
    • 使用这个宏可跨平台
      #define __STDC_FORMAT_MACROS#include <inttypes.h>#undef __STDC_FORMAT_MACROSprintf("%" PRId64, "\n", value);

原子性操作

  • 为什么需要原子性操作
    • x++
    • 从内存中读 x 的值到寄存器中,对寄存器加 1, 再把新值写回 x 所处的内存地址。
      假设两个线程分别执行 x++
time Thread1 Thread2 0 load eax, x 1 load eax, x 2 add eax, 1 3 add eax, 1 4 store x, eax 5 store x, eax

显然是不符合常理的

  • gcc 提供的原子性操作
// 原子自增操作// *ptr + valuetype __sync_fetch_and_add (type *ptr, type value)// 原子比较和交换(设置)操作type __sync_val_compare_and_swap (type *ptr, type oldval type newval)bool __sync_bool_compare_and_swap (type *ptr, type oldval type newval)// 原子赋值操作type __sync_lock_test_and_set (type *ptr, type value)// 使用这些原子性操作,编译的时候需要加-march=cpu-type

如果没有这些原子性操作,我们就要用锁,锁操作比原子性操作的开销大得多。

AtomicIntegerT 类封装

class AtomicIntegerT : boost::noncopyable{}
  • volatile:作为指令关键字,确保本条指令不会因为编译器的优化而省略,且要求每次直接读值。简单的说就是防止编译器对代码进行优化。
  • 当使用 volatile 声明的变量的时候,系统总是重新从他所在的内存中读取数据。即使他前面的指令刚刚从该处读取过数据。而且读取的数据立即被保存。
  • 编译选项
    -Wall // 大部分警告
    -Wextra // 一些额外的警告
    -Werror // 当出现警告时转为错误,停止编译
    -Wconversion // 一些可能改变值的隐式转换,给出警告。
    -Wno-unused-parameter // 函数中出现未使用的参数,不给出警告。
    -Wold-style-cast // C风格的转换,给出警告
    -Woverloaded-virtual // 如果函数的声明隐藏住了基类的虚函数,就给出警告。
    -Wpointer-arith // 对函数指针或者void *类型的指针进行算术操作时给出警告
    -Wshadow // 当一个局部变量遮盖住了另一个局部变量,或者全局变量时,给出警告。
    -Wwrite-strings // 规定字符串常量的类型是const char[length],因此,把这样的地址复制给 non-const char *指针将产生警告.这些警告能够帮助你在编译期间发现企图写入字符串常量 的代码
    -march=native // 指定cpu体系结构为本地平台

Exception类

  • Exception 类实现
    • backtrace 栈回溯,保存各个帧栈的地址
    • backtrace_symbols,根据地址,转成相应的函数符号
    • abi::__cxa_demangle
Exception massage: string stack_: string << create>> -Exception(what: char) << create >> -Exception(what: string) << destroy >> -Exception() what(): const char* stackTrace(): const char* fillStackTrace(): void

Thread 类

  • Linux 下的 POSIX 线程也有一个id,类型是 pthread_t,由 pthread_self() 取得,该 id 由线程库维护。其 id 空间是由各个进程独立的(即不同进程中的线程可能有相同的 id)。Linux 中的 POSIX 线程库实现的线程其实也是一个进程(LWP 轻量级进程),只是该进程与主进程(启动线程的进程)共享一些资源,比如代码段,数据段等。
  • 有些时候我们需要知道线程的真实pid,比如说进程 P1 要向另一个进程 P2 中的某个线程发送信号,既不能使用 P2 的 pid,更不能使用线程的 pthread_id,而只能使用该线程的真实 pid,称为 tid。
  • gettid()可以得到 tid,但glibc 并没有实现该函数,只能通过 Linux 的系调用 syscall 来获取。
    • return syscall(SYS_gettid)

muduo 中 Thread 类图(采用基于对象风格)

typedef boost::function< void () > ThreadFunc

Thread started: bool(线程是否已经启动) pthreadId: pthread_t(线程 id) tid: pid_t(线程真实 id) func: ThreadFunc(该线程要回调的函数) name: string(线程名字) numCreated: AtomicInt32(static 成员,已经创建的线程个数) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ << create >>+Thread(func: const ThreadFunc&, name: string) << destroy >>+Thread() start(): void join(): int started: bool tid(): pid_t name(): const string& numCreated(): int(static 函数) startThread(thread: void): void* (static ,线程入口函数,调用下面的 runInThread ) runInThread(): void (调用数据成员 func)

与线程进程有关的知识

  • 线程标识符

    • phread_self
    • gettid
  • __thread,gcc内置的线程局部存储设施,这种变量每个线程都有一份。

    • __thread 只能修饰 POD 类型
    • POD 类型(plain old data),与 C 兼容的原始数据。如结构体和整形等 C 语言中的类型是 POD 类型,但带有用户定义的构造函数或虚函数的类则不是。
__thread string t_obj1("cppcourse");  // 错误,不能调用类的构造函数__thread string8 t_obj2 = new string; // 错误,初始化只能是编译期常量__thread string t_obj3 = NULL;   //正确
namespace muduo{    namespace CurrentThread    {        __thread 修饰的变量是线程局部存储的        __thread int t_cacheTid = 0; // 线程 tid 的缓存,减少::syscall(SYS_gettid) 系统调用的次数,提高效率。        __thread char t_tidString[32];// tid 的字符串表示形式        __thread const chat* t_threadName = "unknown"; // 每个线程名称        const bool sameType = boost::is_same<int, pid_t>::value; // boost::is_same 判断是否相同类型        BOOST_STATIC_ASSERT(sameType); // 编译时断言    }}
  • 如果想把非 POD 类型设为线程特定数据,即每个线程都有一份,我们可以用 TSD (线程特定数据)实现。
  • boost::is_same 判定两种类型是否同一种类型
  • pthread_atfork(void(*prepare)(void), void(*parent)(void), void(*child)(void))
    • 调用 fork 时,内部创建子进程前在父进程中会调用 prepare, 内部创建子进程成功后,父进程会调用 parent, 子进程会调用 child。
    • fork 可能是在主线程中调用,也可能是在子线程中调用。
    • fork得到一个新进程,新进程只有一个执行序列,只有一个线程(调用 fork 的线程被继承下来)。
    • 对于编写多线程程序来说,我们最好不要再调用 fork。不要编写多线程多进程 程序, 要么用多线程,要么用多进程。
    • 一个死锁的实例
// 一个在多线程程序里fork造成死锁的例子// 一个输出示例:/*pid = 19445 Entering main ...pid = 19445 begin doit ...pid = 19447 begin doit ...pid = 19445 end doit ...pid = 19445 Exiting main ...父进程在创建了一个线程,并对mutex加锁,父进程创建一个子进程,在子进程中调用doit,由于子进程会复制父进程的内存,这时候mutex处于锁的状态,父进程在复制子进程的时候,只会复制当前线程的执行状态,其它线程不会复制。因此子进程会处于死锁的状态。*/#include <stdio.h>#include <time.h>#include <pthread.h>#include <unistd.h>pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;void* doit(void* arg){    printf("pid = %d begin doit ...\n",static_cast<int>(getpid()));    pthread_mutex_lock(&mutex);    struct timespec ts = {2, 0};    nanosleep(&ts, NULL);    pthread_mutex_unlock(&mutex);    printf("pid = %d end doit ...\n",static_cast<int>(getpid()));    return NULL;}int main(void){    printf("pid = %d Entering main ...\n", static_cast<int>(getpid()));    pthread_t tid;    pthread_create(&tid, NULL, doit, NULL);    struct timespec ts = {1, 0};    nanosleep(&ts, NULL);    if (fork() == 0)    {        doit(NULL);    }    pthread_join(tid, NULL);    printf("pid = %d Exiting main ...\n",static_cast<int>(getpid()));    return 0;}

fork()的时候,mutex 已经上了锁,然后再次调用 doit(),再次尝试上锁,造成了死锁。尽管原来的进程中的 mutex 在 2s 后解锁,但子进程不会复制该解锁状态(因为复制的时候没有解锁)。

  • pthread_atfork() 就可用于解决这类问题。
    • 把 mutex 的解锁代码放在 prepare()
    • 把 mutex 再次上锁的代码放在 parent()
原创粉丝点击