一、线程的基本使用

正常情况下,一个进程只有一个一线程在运行。有了多线程之后,就可以使得程序在同一时间可以做多间不同的事情。

这样就可以利用起来很多被浪费掉的时间,例如执行阻塞任务,磁盘IO等等,这些等待的时间都可以被用来做其他的事情,这也就是多线程的用武之地。

1.1 线程标识

线程通过线程ID来标识,在linux环境中被定义为pthread_t类型。在调试中,我们可以把它当作一个整形变量打印,但是实际上在严谨的场合不能这么干,它并不等于整形。

判断两个线程id是否相等要使用linux提供的函数接口:

#include <pthread.h>
int pthread_equal(pthread_t t1, pthread_t t2);

对线程而言,可以通过pthread_self()函数来获得当前线程的ID,例如打印出一个函数中主线程的id:

#include <stdio.h>
#include <pthread.h>

int main() {
    pthread_t tid;

    tid = pthread_self();

    printf("main thread: %08x\n", tid);

    return 0;
}

编译运行:

main thread: 0x66744700
注意,多线程程序编译时要加-lpthread选项

1.2 创建一个函数

创建一个线程的函数原型:

#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                  void *(*start_routine) (void *), void *arg);

各个参数的含义:

  • thread:用来接收新创建的线程ID,该参数不能省略,不能写成NULL
  • attr:设置线程的属性,一般为NULL
  • start_routine:线程启动的函数,是一个函数指针,线程启动后将调用这个函数
  • arg:自定义参数,会作为线程函数的启动参数传入

以下是一个简单的创建线程的示例,创建一个线程并打印出它当前的进程id和线程id:

#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
#include<pthread.h>
#include<string.h>

typedef unsigned int uint;

void f(const char *s) {
    pid_t pid = getpid();
    pthread_t tid = pthread_self();
    // 打印出当前的进程id和线程id
    printf("%s pid is %d, tid is 0x%x\n", s, pid, (uint)tid);
}

void *thread_start(void *arg) {
    f((char*)arg);
    return 0;
}

int main(){
    int err;
    pthread_t tid;
    // 创建线程
    err = pthread_create(&tid, 0, thread_start, (void*)"new thread:");
    if (err != 0) {
        fprintf(stderr, "pthread_create error: %s", strerror(err));
        return 0;
    }
    printf("main thread: pid is %d, child thread is 0x%x.\n", (int)getpid(), (uint)tid);
    // 等待线程执行完毕
    sleep(1);
    return 0;
}

运行结果:

main thread: pid is 18267, child thread is 0xe33ba700.
new thread: pid is 18267, tid is 0xe33ba700

根据两个线程打印出来的pid可以看到:多线程实际上还是处于同一个进程的。

二、线程终止

2.1 pthread_exit()

pthread_exit()的作用就是退出当前线程,并设置退出的返回值。定义为:

#include <pthread.h>
void pthread_exit(void *retval);

retval参数表示线程的退出状态。

线程也可以使用return来返回,但是在线程定义了清理函数的时候,如果使用return返回,清理函数将不会被执行。

2.2 pthread_join()

上面代码的主函数中,最后面有一个sleep(1)来等待子线程退出,这么做实际上是不合理的,因为线程和进程一样,执行的时机是不确定的,1S之后线程可能还没有执行完。

如果要在主线程等待子线程执行完毕的话,可以通过pthread_join()函数来完成,其定义为:

#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);

参数含义:

  • thread:线程id
  • retval:线程的返回码

调用这个函数后,线程会一直阻塞,直到指定线程退出。如果调用时线程已经处于结束状态,那么函数会立即返回。

如果指定的线程被取消了,retval的值将会是PTHREAD_CANCELED。不关心线程返回值的话,可以直接把retval设为NULL。

注意,如果线程被分离了(执行了pthread_detach()),执行pthread_join()无效。

以下是一个调用pthread_exit()pthread_join()的示例:

#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
#include<pthread.h>
#include<string.h>

typedef unsigned int uint;

void *func1(void *arg) {
    pthread_t tid;
    tid = pthread_self();

    printf("this is func1(), tid = 0x%08x\n", tid);
    return (void *)111;
}

void *func2(void *arg) {
    pthread_t tid;
    tid = pthread_self();

    printf("this is func2(), tid = 0x%08x\n", tid);
    pthread_exit((void *)222);
}

// 创建一个线程,并打印出对应的信息
int pthread_create_ex(pthread_t *tid, void *(pf)(void*)) {
    int err;

    err = pthread_create(tid, 0, pf, NULL);
    if (err != 0) {
        fprintf(stderr, "pthread_create error: %s", strerror(err));
    } else {
        printf("main thread: child thread is 0x%x\n", tid);
    }

    return err;
}

// 回收一个线程,并打印相关信息
int pthread_join_ex(pthread_t tid) {
    int err, retval;

    err = pthread_join(tid, (void *)&retval);
    if (err != 0) {
        fprintf(stderr, "pthread_join error: %s", strerror(err));
    } else {
        printf("thread 0x%x has exit, code %d\n", tid, retval);
    }

    return err;
}

int main(){
    int err; 
    pthread_t tid1, tid2;

    err = pthread_create_ex(&tid1, func1);
    if (err != 0)
        return -1;

    err = pthread_create_ex(&tid2, func2);
    if (err != 0)
        return -1;

    err = pthread_join_ex(tid1); 
    if (err != 0)
        return -1;

    err = pthread_join_ex(tid2);
    if (err != 0)
        return -1;
    
    return 0;
}

这里的代码用两个函数作为不同的线程开始,分别通过returnpthread_exit来返回,运行结果:

可以看到,pthread_join()都能拿到返回值,那这两者是否有区别呢?下面将会继续探究。

2.2 pthread_cancel()

pthread_cancel()函数可以取消同一进程中的其他线程:

#include <pthread.h>
int pthread_cancel(pthread_t thread);

参数就是希望取消的线程id,这会使得被取消的线程返回PTHREAD_CANCELED

#include <stdio.h>
#include <pthread.h>

void *f1(void *arg) {
    int i = 0;
    while (i++ < 5) {
        printf("second %d\n", i);
        sleep(1);
    }
    pthread_exit((void *)99);
}

void *f2(void *arg) {
    pthread_t tid;

    tid = *(pthread_t *)arg;

    sleep(1);
    printf("pthread_cancel --> 0x%08x\n", tid);
    pthread_cancel(tid);
    pthread_exit((void *)0);
}

int main() {
    pthread_t tid1, tid2;
    pthread_t ret1, ret2;

    pthread_create(&tid1, NULL, f1, NULL);
    pthread_create(&tid2, NULL, f2, (void *)&tid1);

    pthread_join(tid1, (void *)&ret1);
    pthread_join(tid2, (void *)&ret2);

    printf("tid1: %08x, exit status: %d\n", tid1, ret1);
    printf("tid2: %08x, exit status: %d\n", tid2, ret2);
    printf("PTHREAD_CANCELED = %d\n", PTHREAD_CANCELED);

    return 0;
}

以上是一个简单的调用示例,产生两个线程,一个线程每隔一秒打印一次,另一个线程则在一秒后关闭该线程。

主线程中等待退出并打印返回码,结果如下所示:

被取消掉的线程91ef7700并没有返回其函数内定义的99,而是-1,也就是PTHREAD_CANCELED的值。

一、问题描述

在使用shared_ptr时,如果出现了循环引用(如链表节点的next指向),就会导致内存泄漏。

例如以下两个类,内部互相有一个指向对方的智能指针,在生存空间结束后就不会被释放掉:

#include <iostream>
#include <memory>
using namespace std;

class CTestB;

class CTestA {
public:
    shared_ptr<CTestB> b;
    CTestA() {
        cout << "CTestA()" << endl;
    }
    ~CTestA() {
        cout << "~CTestA()" << endl;
    }
};

class CTestB {
public:
    shared_ptr<CTestA> a;
    CTestB() {
        cout << "CTestB()" << endl;
    }
    ~CTestB() {
        cout << "~CTestB()" << endl;
    }
};

int main() {
    shared_ptr<CTestA> a(new CTestA);
    shared_ptr<CTestB> b(new CTestB);

    cout << "a.use_count = " << a.use_count() << endl;
    cout << "b.use_count = " << b.use_count() << endl;

    a->b = b;
    b->a = a;

    cout << "a.use_count = " << a.use_count() << endl;
    cout << "b.use_count = " << b.use_count() << endl;

    return 0;
}

编译后的运行程序的结果:

原因很简单,a->b = b导致b的引用计数增加,b->a = a导致a的引用计数增加,在程序运行完后,a、b自行析构引用计数都会减一,但是此时引用计数都是1,因此不会释放。

二、解决方法

shared_ptr循环引用要通过weak_ptr来解决,weak_ptr也是智能指针的一种,不过它是一种弱引用,不会增加智能指针的引用计数。

分别修改两个类为以下形式:

class CTestA {
public:
    weak_ptr<CTestB> b;
    // ...
}

class CTestB {
public:
    weak_ptr<CTestA> a;
    // ...
}

运行结果:

一、互斥量

互斥量的本身是一把锁,在访问共享资源时前对互斥量加锁,访问完成后释放。在对互斥量加锁后,其他线程如果想再次加锁,操作会被阻塞,直到锁被释放为止。

创建互斥量和销毁互斥量的函数定义:

#include <pthread.h>

int pthread_mutex_init(pthread_mutex_t *restrict mutex,
      const pthread_mutexattr_t *restrict attr);
int pthread_mutex_destroy(pthread_mutex_t *mutex);

也可以直接使用宏定义PTHREAD_MUTEX_INITIALIZER完成对锁的初始化:

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

对互斥量加锁和解锁:

#include <pthread.h>

int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);

其中,pthread_mutex_trylock()会尝试对互斥量加锁,如果此时互斥量没有被锁住,加锁成功。如果互斥量已经被其他线程锁住了,返回EBUSY。还有一个可以执行超时加锁的函数:

#include <pthread.h>
#include <time.h>

int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex,
        const struct timespec *restrict abs_timeout);

对加锁设置超时时间,如果在指定时间内,加锁不成功(即锁还没有被释放或者释放了又被其他线程抢占了),加锁失败,直接返回,不阻塞线程了。

二、使用示例

分别创建50个线程,每个线程对全局变量n执行1000次自加操作:

#include <stdio.h>
#include <pthread.h>
#include "log.h"

static int n = 0;

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

// 线程函数
void *f(void *args) {
    int idx = 0;
    // n自加1000次
    while (idx++ < 1000) {
        pthread_mutex_lock(&mutex);
        n++;
        pthread_mutex_unlock(&mutex);
    }
    return 0;
}

int main() {
    int i;
    pthread_t ts[50];
    // 创建50个线程
    for (i = 0; i < 50; i++) {
        pthread_create(&ts[i], 0, f, 0);
    }

    // 回收线程
    for (i = 0; i < 50; i++) {
        pthread_join(ts[i], 0);
    }

    pthread_mutex_destroy(&mutex);
    info("n = %d\n", n);

    return 0;
}

执行结果:

mutex.png

三、死锁

当多个线程之间,对多个锁产生不同的竞争关系时,就有可能发生死锁现象。

注意:死锁只会发生在一个线程试图锁住另一个线程以相反顺序锁住的互斥量

例如此时有A/B两个锁,t1t2两个线程分别执行以下抢占:

  1. t1对A加锁,t2对B加锁,两者都加锁成功
  2. t2再对A加锁,t1对B加锁,此刻t1会等待t2释放B锁,而t2又要等待t1先释放A锁,此时产生死锁。

一、读写锁

读写锁和互斥量相似,都是对共享空间执行加锁和解锁的过程。不过,读写锁比互斥量有更高的并行性。

读写锁有三种状态:读模式加锁、写模式加锁和不加锁。读写锁只允许同时有一个线程占有写模式的锁。

这就意味着读锁可以同时被多个线程拥有,读写锁特别适合读次数远大于写次数的场景。

1.1 创建并初始化一把锁

读写锁的数据类型为pthread_rwlock_t,使用以下方式初始化和销毁:

#include <pthread.h>

int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
      const pthread_rwlockattr_t *restrict attr);
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);

1.2 加锁和解锁

#include <pthread.h>

// 加读锁
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
// 加写锁
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
// 解锁
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

pthread_rwlock_rdlock()可以同时对一把锁使用,不会阻塞线程。

1.3 带超时的读写锁

#include <pthread.h>
#include <time.h>

int pthread_rwlock_timedrdlock(pthread_rwlock_t *restrict rwlock,
      const struct timespec *restrict abs_timeou
int pthread_rwlock_timedwrlock(pthread_rwlock_t *restrict rwlock,
      const struct timespec *restrict abs_timeout);

二、示例

#include <stdio.h>
#include <pthread.h>
#include "log.h"

static int n = 0;

pthread_rwlock_t rwlock;

// 线程函数
void *f(void *args) {
    int idx = 0;
    while (idx++ < 1000) {
        pthread_rwlock_wrlock(&rwlock);
        n++;
        pthread_rwlock_unlock(&rwlock);
    }
    return 0;
}

int main() {
    int i;
    pthread_t ts[50];

    pthread_rwlock_init(&rwlock, NULL);

    for (i = 0; i < 50; i++) {
        pthread_create(&ts[i], 0, f, 0);
    }

    for (i = 0; i < 50; i++) {
        pthread_join(ts[i], 0);
    }

    pthread_rwlock_destroy(&rwlock);
    info("n = %d", n);

    return 0;
}

执行结果:

rwlock.png

一、自旋锁

自旋锁和互斥量相似,通过加锁和解锁来保护对共享数据的保护。

和互斥量不同的是:互斥量在锁已经被占有的情况下,会阻塞等待,此时线程处于休眠状态,不占用CPU资源。而自旋锁此时虽然也会被阻塞,但是不会休眠,处于忙等的状态,不会交出CPU资源。

自旋锁适用于那些锁持有时间短不希望把时间浪费在CPU调度上的场景。

linux内核中,自旋锁被广泛的使用,因为内核模块特别是在网卡驱动中,时间精度要求很高,相比其他锁切换时间,自旋锁的忙等效率更高一些。

自旋锁的数据类型为pthread_spinlock_t,相关函数为:

#include <pthread.h>

// 初始化和销毁自旋锁
int pthread_spin_init(pthread_spinlock_t *lock, int pshared);
int pthread_spin_destroy(pthread_spinlock_t *lock);

// 对自旋锁加锁和解锁
int pthread_spin_lock(pthread_spinlock_t *lock);
int pthread_spin_trylock(pthread_spinlock_t *lock);
int pthread_spin_unlock(pthread_spinlock_t *lock);

自旋锁的函数定义基本和其他锁一致,具体使用方法不再详细描述。

二、示例

#include <stdio.h>
#include <pthread.h>
#include "log.h"

static int n = 0;

pthread_spinlock_t spin_lock;

static void *f(void *args) {
    int idx = 0;
    while (idx++ < 1000) {
        pthread_spin_lock(&spin_lock);
        n++;
        pthread_spin_unlock(&spin_lock);
    }
    return 0;
}

int main() {
    int i;
    pthread_spin_init(&spin_lock, 0);

    pthread_t ts[50];
    for (i = 0; i < 50; i++) {
        pthread_create(&ts[i], 0, f, 0);
    }

    for (i = 0; i < 50; i++) {
        pthread_join(ts[i], 0);
    }
    pthread_spin_destroy(&spin_lock);

    info("n = %d", n);

    return 0;
}

编译运行:

一、ELF文件

ELF(Executable and Linkable Format)文件是linux下的二进制可执行文件,它同时兼容可执行文件和可链接文件。

一个ELF文件包含两个部分:一个固定长度的文件头和多个可扩展的数据块。其中,文件头是整个可执行文件的总地图,描述了整个文件的组织结构。可扩展数据块分为两类,对应着不同的视图——在链接视图下,数据块的单位是节(Section),用多个节区头索引所有内容;而在执行视图下,数据块的单位是段(Segment),用程序头(Program Header)索引所有的段。如下图所示:

通过readelf -S命令可以打印出文件的节区头部分,这里使用ln命令作为源程序分析:

[ma@localhost:~]$ readelf -S /bin/ls
There are 29 section headers, starting at offset 0x1a358:

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .interp           PROGBITS         0000000000400200  00000200
       000000000000001c  0000000000000000   A       0     0     1
  [ 2] .note.ABI-tag     NOTE             000000000040021c  0000021c
       0000000000000020  0000000000000000   A       0     0     4
  [ 3] .note.gnu.build-i NOTE             000000000040023c  0000023c
       0000000000000024  0000000000000000   A       0     0     4
  [ 4] .gnu.hash         GNU_HASH         0000000000400260  00000260
       0000000000000064  0000000000000000   A       5     0     8
  [ 5] .dynsym           DYNSYM           00000000004002c8  000002c8
       0000000000000be8  0000000000000018   A       6     1     8
  [ 6] .dynstr           STRTAB           0000000000400eb0  00000eb0
       00000000000005c4  0000000000000000   A       0     0     1
  [ 7] .gnu.version      VERSYM           0000000000401474  00001474
       00000000000000fe  0000000000000002   A       5     0     2
  [ 8] .gnu.version_r    VERNEED          0000000000401578  00001578
       00000000000000a0  0000000000000000   A       6     3     8
  [ 9] .rela.dyn         RELA             0000000000401618  00001618
       00000000000001b0  0000000000000018   A       5     0     8
  [10] .rela.plt         RELA             00000000004017c8  000017c8
       0000000000000990  0000000000000018   A       5    12     8
  [11] .init             PROGBITS         0000000000402158  00002158
       0000000000000018  0000000000000000  AX       0     0     4
  [12] .plt              PROGBITS         0000000000402170  00002170
       0000000000000670  0000000000000010  AX       0     0     4
  [13] .text             PROGBITS         00000000004027e0  000027e0
       000000000000fb68  0000000000000000  AX       0     0     16
  [14] .fini             PROGBITS         0000000000412348  00012348
       000000000000000e  0000000000000000  AX       0     0     4
  [15] .rodata           PROGBITS         0000000000412360  00012360
       0000000000003b27  0000000000000000   A       0     0     32
  [16] .eh_frame_hdr     PROGBITS         0000000000415e88  00015e88
       00000000000006b4  0000000000000000   A       0     0     4
  [17] .eh_frame         PROGBITS         0000000000416540  00016540
       0000000000001fdc  0000000000000000   A       0     0     8
  [18] .ctors            PROGBITS         0000000000619000  00019000
       0000000000000010  0000000000000000  WA       0     0     8
  [19] .dtors            PROGBITS         0000000000619010  00019010
       0000000000000010  0000000000000000  WA       0     0     8
  [20] .jcr              PROGBITS         0000000000619020  00019020
       0000000000000008  0000000000000000  WA       0     0     8
  [21] .data.rel.ro      PROGBITS         0000000000619040  00019040
       0000000000000a48  0000000000000000  WA       0     0     32
  [22] .dynamic          DYNAMIC          0000000000619a88  00019a88
       00000000000001d0  0000000000000010  WA       6     0     8
  [23] .got              PROGBITS         0000000000619c58  00019c58
       0000000000000098  0000000000000008  WA       0     0     8
  [24] .got.plt          PROGBITS         0000000000619cf0  00019cf0
       0000000000000348  0000000000000008  WA       0     0     8
  [25] .data             PROGBITS         000000000061a040  0001a040
       0000000000000200  0000000000000000  WA       0     0     32
  [26] .bss              NOBITS           000000000061a240  0001a240
       0000000000000d20  0000000000000000  WA       0     0     32
  [27] .gnu_debuglink    PROGBITS         0000000000000000  0001a240
       0000000000000010  0000000000000000           0     0     4
  [28] .shstrtab         STRTAB           0000000000000000  0001a250
       0000000000000101  0000000000000000           0     0     1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings)
  I (info), L (link order), G (group), x (unknown)
  O (extra OS processing required) o (OS specific), p (processor specific)

节区头包含了我们经常见到的字段:.data/.text/.init等,这里只描述一下这几个常见节区的作用。

1.1 .text 节区

该节区存储了源文件编译生成的机器指令。对开发者来说,这可能是最重要的一个节区,所有的程序逻辑都放在这里。但开发者对该节区能做的控制却很少,能影响它的因素只有开发者写的程序逻辑,以及编译时使用的选项。比如,使用 -O1 优化选项编译程序可以生成尽量紧凑的 .text 节区,而用 -O2 优化选项会使编译器倾向于生成执行速度更快的指令组合,但有可能让 .text 节区的体积轻微地增大。

1.2 .rodata 节区

从字面上就能看出,ro表示read only,即不可写,因此该节区存储了程序中的常量数据,例如:

const char *p = "helloworld";

1.3 .data 节区

所有的全局和静态的已初始化变量会存放在这个节区中,这个节区是可读可写的。

1.4 .bss 节区

该节区存储了所有未初始化或初始化为 0 的全局和静态变量,该节区的设计初衷就是为了节省目标文件的存储空间。变量未被初始化,或者虽被初始化了,但值为 0,就没必要浪费空间,再在目标文件中存储大量的 0 值。

1.5 .got 和 .plt 节区

这两个节区存储了动态链接用到的全局入口表和跳转表。当程序中用到动态链接库中的某个函数时,会在该节区内记录相应的数据。

二、进程内存分布

2.1 虚拟地址空间

linux为每个进程都分配了4G(32位平台)的虚拟地址空间,虚拟空间可以认为是操作系统给每个进程准备的沙盒。就像电影《黑客帝国》中 Matrix 给每个人准备的充满营养液的容器一样。实际上,每个进程只存活在自己的虚拟世界里,却感觉自己独占了所有的系统资源(内存)。

当一个进程要使用某块内存时,它会将自己世界里的一个内存地址告诉操作系统,剩下的事情就由操作系统接管了。操作系统中的内存管理策略将决定映射哪块真实的物理内存,供应用使用。操作系统会竭尽全力满足所有进程合法的内存访问请求。一旦发现应用试图访问非法内存,它将会把进程杀死,防止它做“坏事”影响到系统或其他进程。

这样做,一方面为了安全,防止进程操作其他进程或者系统内核的数据;另一方面为了保证系统可同时运行多个进程,且单个进程使用的内存空间可以超过实际的物理内存容量。

该做法的另一结果则是降低了每个进程内存管理的复杂度,进程只需关心如何使用自己线性排列的虚拟地址,而不需关心物理内存的实际容量,以及如何使用真实的物理内存。

2.2 虚拟地址空间分布

虚拟内存的排布规则如下所示:

从下往上依次是0-4G的内存空间,分别分给了不同的区段。可以用一段程序来验证这一个观点:

#include <stdlib.h>

static const char *p = "HelloWorld";
static int s_init_var = 0;
static int s_not_init;

int g_init_var = 0;
int g_not_init;

int main() {
    const char *q = "abc";

    int a, b;
    int *pa = (int *)malloc(sizeof(int));
    int *pb = (int *)malloc(sizeof(int));

    printf("static variable address: %p %p\n",
            &s_init_var, &s_not_init);
    printf("global variable address: %p %p\n",
            &g_init_var, &g_not_init);
    printf("const variable:%p[static] %p\n",
            p, q);
    printf("a and b: %p %p\n", &a, &b);
    printf("pa and pb: %p %p\n", pa, pb);

    return 0;
}

代码分别创建了静态变量、全局变量以及临时变量等多种不同场景下的变量,打印出它们的地址。使用gcc编译运行:

static variable address: 0x600a0c 0x600a10
global variable address: 0x600a08 0x600a14
const variable:0x4006c8 0x4006d3
a and b: 0x7ffda942df14 0x7ffda942df10
pa and pb: 0xa82010 0xa82030

可以看到已经初始化的静态变量s_init_var和已经初始化的全局变量g_init_var两者地址紧紧相邻,间距刚好隔了4个字节,因为他们被初始化了,都放在了.data区域。而未初始化的两个虽然也是相连在一起的,但是和初始化了的变量还是隔了一段距离,但距离不是很大,因为.bss.data地址差别不大。

而被const修饰的两个变量地址也是一样,处在同一个地址空间,和上面的静态变量以及全局变量相比,地址比它们小,这也就说明了.rodata是在.data.bss下面的。

最后的a/bpa/pb则验证了栈区地址和堆区地址的位置,堆是处在靠下的地址,而栈从地址位数上来说就已经很高了。

这里还能看出一点:局部变量b在a后面申请,但是它的地址比a要小;而pb同样也是在pa后面申请,地址却比pa大。
这说明了栈的地址空间是从上往下申请的,而堆则正常由下往上。

三、参考文档

文章部分摘抄于:攻克 Linux 系统编程

原文写得很不错,有兴趣可自行参考。

一、题目

设计一个类,我们只能生成该类的一个实例

二、思路

单例模式是一种软件设计模式,表示单例对象设计的类只能允许一个实例存在。

单例模式的基本解题思路为:

  1. 将类的构造函数设为私有,外部无法直接通过默认构造函数来生成新的实例,只提供一个公共的静态接口函数返回该实例。
  2. 当外部没有引用当前实例时,创建并返回该实例。如果该实例已经存在了,直接返回。

单例模式共有两种实现方法:

  1. 饿汉模式:饿汉是指在实例初始化时就创建好,后面直接使用。
  2. 懒汉模式:懒汉模式是实例在使用了,万不得已的情况下才创建。

三、饿汉模式实现

#include <iostream>
using namespace std;

class CSingleTon {
private:
    CSingleTon() {
        cout << "CSingleTon" << endl;
    }

    static CSingleTon *instance;
public:
    static CSingleTon *getInstance() {
        return instance;
    }
    static void destoryInstance() {
        if (instance != NULL) {
            delete instance;
            instance = NULL;
        }
    }
};

CSingleTon *CSingleTon::instance = new CSingleTon();

int main() {
    cout << "helloworld" << endl;

    CSingleTon *s1 = CSingleTon::getInstance();
    CSingleTon *s2 = CSingleTon::getInstance();
    CSingleTon *s3 = CSingleTon::getInstance();

    cout << s1 << "\n"
        << s2 << "\n"
        << s3 << endl;

    s1->destoryInstance();

    return 0;
}

编译运行:

注意:CSingleTon是在helloworld的前面打印出来的,说明程序在创建时就已经生成了实例。并且三个实例的地址都是相同的。

四、懒汉模式实现

#include <iostream>

using namespace std;

class CSingleTon {
private:
    CSingleTon() {
        cout << "CSingleTon" << endl;
    }

    static CSingleTon *instance;
public:
    static CSingleTon *getInstance() {
        if (instance == NULL) {
            instance = new CSingleTon();
        }
        return instance;
    }
    static void destoryInstance() {
        if (instance != NULL) {
            delete instance;
            instance = NULL;
        }
    }
};

CSingleTon *CSingleTon::instance = NULL;

int main() {
    cout << "helloworld" << endl;

    CSingleTon *s1 = CSingleTon::getInstance();
    CSingleTon *s2 = CSingleTon::getInstance();
    CSingleTon *s3 = CSingleTon::getInstance();

    cout << s1 << "\n"
        << s2 << "\n"
        << s3 << endl;

    s1->destoryInstance();

    return 0;
}

编译运行,和上面的不同,helloworld先打印出来了,并且三个实例的地址也都是相同的:

关于斐波那契数列的描述:
斐波那契数列(Fibonacci sequence),又称黄金分割数列、因数学家列昂纳多·斐波那契(Leonardoda Fibonacci)以兔子繁殖为例子而引入,故又称为“兔子数列”,指的是这样一个数列:1、1、2、3、5、8、13、21、34、……在数学上,斐波纳契数列以如下被以递推的方法定义:F(1)=1,F(2)=1, F(n)=F(n-1)+F(n-2)(n>=3,n∈N*)在现代物理、准晶体结构、化学等领域,斐波纳契数列都有直接的应用,为此,美国数学会从1963年起出版了以《斐波纳契数列季刊》为名的一份数学杂志,用于专门刊载这方面的研究成果。

一、面试题

求斐波那契数列的第n项:写一个函数,输入n,输出斐波那契数列的第n项。

二、解法一

求斐波那契数列最简单的解法就是用递归,大部分书籍上介绍递归的时候都会用斐波纳契数列做例子,递归很容易写:

long long Fibonacci_Solution1(unsigned int n)
{
    if(n <= 0)
        return 0;

    if(n == 1)
        return 1;

    return Fibonacci_Solution1(n - 1) + Fibonacci_Solution1(n - 2);
}

这个解法并不是最优的,因为中间充斥了很多重复的运算,应该还不足以应付面试。例如计算f(10)的过程中:

f(6)/f(7)/f(8)都被计算了多次,当数据越大时,重复计算的次数也越来越多,因此并不是最优的解法。

三、解法二

上面的递归之所以慢,是因为重复的计算太多,我们可以考虑从如何减少重复计算下手。例如可以选择从2开始,保存前两位的值,依次向后计算:

long long Fibonacci_Solution2(unsigned n)
{
    int result[2] = {0, 1};
    if(n < 2)
        return result[n];

    long long  fibNMinusOne = 1;
    long long  fibNMinusTwo = 0;
    long long  fibN = 0;
    for(unsigned int i = 2; i <= n; ++ i)
    {
        fibN = fibNMinusOne + fibNMinusTwo;

        fibNMinusTwo = fibNMinusOne;
        fibNMinusOne = fibN;
    }

     return fibN;
}

注意事项:传参的n的类型为unsigned int,保存值以及返回值的数据类型是long long,避免int溢出。

ps:虽然long long也会溢出,至少比int大一些。

一、两者对比

进程是最小的资源分配单位,线程是最小的执行单位:

  • 每个进程至少有一个线程,任务的执行都是由线程来完成,也就是说,线程时进程运行时的实体。
  • 线程运行时依赖进程中分配的资源,一个进程可以有多个线程,但是每个线程只属于一个进程。

进程之间相互独立,线程之间数据共享:

  • 每个进程拥有自己独立的地址空间:堆、栈等。
  • 线程之间大部分数据是共享的,它只拥有自己独立的栈,局部堆以及单独的信号屏蔽字等。

进程创建和切换的开销比线程要大:

  • 操作系统切换进程的时候,需要保存进程上下文(线程也属于进程上下文中的内容)。创建进程时,子进程需要拷贝父进程的数据内容,遵循读时共享、写时复制的原则。
  • 线程切换只需要切换线程所有的栈空间,只需要移动栈指针即可,开销远小于进程切换。

进程间通信比线程间通信麻烦:

  • 进程间通信依赖IPC通信方式:共享内存、管道以及socket等,实现复杂。
  • 线程间通信只需要通过定义全局变量即可,各线程都能共享。

进程比线程稳定:

  • 多进程间,有各自独立的数据段,一个进程的崩溃不会影响到其他进程。
  • 多线程间数据共享,一个线程的崩溃往往会影响其他线程。

表格对比

对比点进程线程
定义进程时运行中的程序,是最小的资源分配单位线程是进程执行的实体,是最小的执行单位
资源占用占用资源多占用资源较少
内存共享数据不共享,遵循读时共享、写时复制原则大部分数据共享,只有私有的栈区不共享
通信依赖IPC通信方式,机制复杂可直接通过全局变量通信,实现简单
上下文切换耗时耗时长耗时短,更轻量
稳定性进程崩溃不会影响另外进程线程崩溃可能导致他线程也崩溃

二、多进程和多线程的选择

  • 需要频繁创建销毁的优先用线程
  • 需要进行大量计算的优先使用线程
  • 可能要扩展到多机分布的用进程,多核分布的用线程

三、参考文档

Difference between Process and Thread

编程思想之多线程与多进程系列

多线程还是多进程的选择及区别