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_MACROS printf("%" 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 + value
type __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()
中
- 把 mutex 的解锁代码放在