0%

1. 前言

在单核时代,大家所编写的程序都是单进程/单线程程序。随着计算机硬件技术的发展,进入了多核时代后,为了降低响应时间,重复充分利用多核cpu的资源,使用多进程编程的手段逐渐被人们接受和掌握。然而因为创建一个进程代价比较大,多线程编程的手段也就逐渐被人们认可和喜爱了。

记得在我刚刚学习线程进程的时候就想,为什么很少见人把多进程和多线程结合起来使用呢,把二者结合起来不是更好吗?现在想想当初真是too young too simple,后文就主要讨论一下这个问题。

2. 进程与线程模型

进程的经典定义就是一个执行中的程序的实例。系统中的每个程序都是运行在某个进程的context中的。context是由程序正确运行所需的状态组成的,这个状态包括存放在存储器中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器(pc)、环境变量以及打开的文件描述符的集合。

进程主要提供给上层的应用程序两个抽象:

  • 一个独立的逻辑控制流,它提供一个假象,好像我们程序独占的使用处理器。
  • 一个私有的虚拟地址空间,它提供一个假象,好像我们的程序独占的使用存储器系统。

线程,就是运行在进程context中的逻辑流。线程由内核自动调度。每个线程都有它自己的线程context,包括一个唯一的整数线程id、栈、栈指针、程序计数器(pc)、通用目的寄存器和条件码。每个线程和运行在同一进程内的其他线程一起共享进程context的剩余部分。这包括整个用户虚拟地址空间,它是由只读文本(代码)、读/写数据、堆以及所有的共享库代码和数据区域组成。线程也同样共享打开文件的集合。

即进程是资源管理的最小单位,而线程是程序执行的最小单位。

在linux系统中,posix线程可以「看做」为一种轻量级的进程,pthread_create创建线程和fork创建进程都是在内核中调用__clone函数创建的,只不过创建线程或进程的时候选项不同,比如是否共享虚拟地址空间、文件描述符等。

3. fork与多线程

我们知道通过fork创建的一个子进程几乎但不完全与父进程相同。子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份拷贝,包括文本、数据和bss段、堆以及用户栈等。子进程还获得与父进程任何打开文件描述符相同的拷贝,这就意味着子进程可以读写父进程中任何打开的文件,父进程和子进程之间最大的区别在于它们有着不同的pid。

但是有一点需要注意的是,在linux中,fork的时候只复制当前线程到子进程,在fork(2)-Linux Man Page中有着这样一段相关的描述:

the child process is created with a single thread–the one that called fork. the entire virtual address space of the parent is replicated in the child, including the states of mutexes, condition variables, and other pthreads objects; the use of pthread_atfork(3) may be helpful for dealing with problems that this can cause.

也就是说除了调用fork的线程外,其他线程在子进程中“蒸发”了。

这就是多线程中fork所带来的一切问题的根源所在了。

阅读全文 »

前言

前段时间在网上看到了个的面试题,大概意思是如何在不使用锁和C++11的情况下,用C++实现线程安全的Singleton。

看到这个题目后,第一个想法就是用Scott Meyer在《Effective C++》中提到的,在static成员函数中构造local static变量的方法来实现,但是经过一番查找、思考,才明白这种实现在某些情况下是有问题的。本文主要将从最基本的单线程中的Singleton开始,慢慢讲述多线程与Singleton的那些事。

单线程

在单线程下,下面这个是常见的写法:

template<typename T>
class Singleton
{
public:
static T& getInstance()
{
if (!value_)
{
value_ = new T();
}
return *value_;
}

private:
Singleton();
~Singleton();

static T* value_;
};

template<typename T>
T* Singleton<T>::value_ = NULL;

在单线程中,这样的写法是可以正确使用的,但是在多线程中就不行了。

多线程加锁

在多线程的环境中,上面单线程的写法就会产生race condition从而产生多次初始化的情况。要想在多线程下工作,最容易想到的就是用锁来保护shared variable了。下面是伪代码:

template<typename T>
class Singleton
{
public:
static T& getInstance()
{
{
MutexGuard guard(mutex_) // RAII
if (!value_)
{
value_ = new T();
}
}
return *value_;
}

private:
Singleton();
~Singleton();

static T* value_;
static Mutex mutex_;
};

template<typename T>
T* Singleton<T>::value_ = NULL;

template<typename T>
Mutex Singleton<T>::mutex_;

这样在多线程下就能正常工作了。这时候,可能有人会站出来说这种做法每次调用getInstance的时候都会进入临界区,在频繁调用getInstance的时候会比较影响性能。这个时候,为了解决这个问题,DCL写法就被聪明的先驱者发明了。

DCL

DCL即double-checked locking。在普通加锁的写法中,每次调用getInstance都会进入临界区,这样在heavy contention的情况下该函数就会成为系统性能的瓶颈,这个时候就有先驱者们想到了DCL写法,也就是进行两次check,当第一次check为假时,才加锁进行第二次check:

template<typename T>
class Singleton
{
public:
static T& getInstance()
{
if(!value_)
{
MutexGuard guard(mutex_);
if (!value_)
{
value_ = new T();
}
}
return *value_;
}

private:
Singleton();
~Singleton();

static T* value_;
static Mutex mutex_;
};

template<typename T>
T* Singleton<T>::value_ = NULL;

template<typename T>
Mutex Singleton<T>::mutex_;

是不是觉得这样就完美啦?其实在一段时间内,大家都以为这是正确的、有效的做法。实际上却不是这样的。幸运的是,后来有大牛们发现了DCL中的问题,避免了这样错误的写法在更多的程序代码中出现。
阅读全文 »

有人说指针是c语言的灵魂,也有人说没学好指针就等于不会c语言。

虽然在现代c++中一般都是推荐尽量避免使用原生的raw指针,而是以smart pointer 和reference替代之。但是无论怎样,对于c/c++来说,指针始终是个绕不过去的坎。究其原因,是因为c/c++都是支持面向底层操作的语言,而面向底层操作就得能操纵内存,这个时候就需要指针了。为什么呢?个人觉得指针实际上就是对机器语言/asm中的通过虚拟地址操作内存的这一行为的一种抽象。

例如

movl %eax, (%edx)

将寄存器eax中的值写入内存地址为寄存器edx的值的内存中。如果把edx看做一个指针的话,也就相当于

*p_edx = value_eax

1. 指针的比较

关于指针,有着许多技巧和用途,后文主要谈谈关于c++中指针比较操作的一些容易踏入的坑。

先来看看这段代码

class BaseA
{
public:
int a;
};

class BaseB
{
public:
double b;
};

class Derived : public BaseA, public BaseB
{
};

int main(int argc, char const *argv[])
{
Derived derivd;
Derived* pd = &derivd;
BaseB* pb = &derivd;
printf("pb = %p\n", pb);
printf("pd = %p\n", pd);
if (pb == pd)
{
printf("pb == pd\n");
}
else
{
printf("pb != pd\n");
}
}

输出的结果是:
pb = 0028FEE0
pd = 0028FED8
pb == pd

可以看到指针pb和pd值并不一样,但是编译器却认为他们相等,为什么呢?
阅读全文 »

1. 前言

在多线程编程中,互斥锁与条件变量恐怕是最常用也是最实用的线程同步原语。

关于条件变量一共也就pthread_cond_init、pthread_cond_destroy、pthread_cond_wait、pthread_cond_timedwait、pthread_cond_signal、pthread_cond_broadcast这么几个函数,但是在实际使用中却是很容易用错,后文将来分析几种常见使用情况的正确性。

2. 分析

下面是一个辅助基类、便于减少篇幅(为了简单起见,后文中的所有函数调用并未检查返回的错误情况):

class ConditionBase
{
public:
ConditionBase()
{
pthread_mutex_init(&mutex_, NULL);
pthread_cond_init(&cond_, NULL);
}

~ConditionBase()
{
pthread_mutex_destroy(&mutex_);
pthread_cond_destroy(&cond_);
}

private:
pthread_mutex_t mutex_;
pthread_cond_t cond_;
};

2.1.1 版本一

class Condition1 : public ConditionBase
{
public:
void wait()
{
pthread_mutex_lock(&mutex_);
pthread_cond_wait(&cond_, &mutex_);
pthread_mutex_unlock(&mutex_);
}

void wakeup()
{
pthread_cond_signal(&cond_);
}
};

错误,这种情况有可能丢失事件。当signal发生在wait之前,就会丢失这次signal事件。如下图


阅读全文 »

2014可以算作真正意义上参加工作的第一年(虽然2013毕业,但是毕业后的半年基本是处于迷糊状态),心态上、技术上都有了一个比较大的转变。以前心态上总是比较浮躁,想着捷径啊快速什么的,现在觉得做事情,尤其是做技术得沉淀下来,慢慢积累。

工作上的事情基本是按部就班的进行,感觉没有什么想说的,但是这一年利用业余时间看了一些技术书籍,这里就按时间的顺序简单的总结一下。

  • 《C++ Primer》

刚毕业的时候对于c++,嗯,基本就是学校课堂上那一点c with class的东西,以及做过几个课程设计的水平。所以选择了这本经典的大部头来学习,之前在网上看到很多人说这本书太厚读起来困难什么的,其实我觉得只要真的静下来读的话,没那么难,作者写的还是非常简单易懂的。

因为想比较系统的学习一下c++的语法,在阅读学习的时候基本上碰到不太理解的地方就会停下来敲代码实践思考一下,每一道课后习题都敲代码做了一遍。最后读完后统计了一下课后习题的练习代码都有七八千行,这个是一开始没想到的。

学习整本书的的过程大概花了2-3个月的时间,与大学课堂上学的那一点c with class相比,收获非常大。

  • 《Effective C++》

读完了c++ primer之后就寻思着读本进阶一点的书籍,于是另外一本经典的书籍自然的进入了我的视线中。这本书主要是一本谈c++经验、避免踏入各种坑的书籍,里面讲了很多非常好的经验,比如视c++为语言联邦、以对象管理资源(RAII)、尽量以传引用代替传值、尽量以组合代替继承、善用智能指针等等。个人认为。目前来说,对我最为有益和深刻的莫过于RAII了,有了它可以在一定程度上很好的避免资源泄露。当然里面也有一些经验谈目前还理解的不是太深刻,需要一定的经验后再去阅读消化了。大概用了1个半月的时间读完了这本书。

阅读全文 »