分类 编程语言 下的文章

  • 面向对象的原则是什么?
封装、继承和多态
  • C++的空类默认产生哪些类成员函数?
默认构造函数、析构函数、复制构造函数和赋值函数
  • 为什么拷贝构造函数只能传递引用
以传值方式调用函数时,会拷贝临时变量,此时又会调用拷贝构造函数来构造临时变量,从而出现无限循环。
  • 哪一种成员变量可以在所有该类的实例之间共享?
静态成员变量
  • C++中class和struct的区别
  • 如何在类中使用常量成员变量?
使用const修饰的成员变量,必须在初始化列表中初始化。
  • 把析构函数定义成virtual的意义在哪?
当析构函数被定义成virtual的以后,销毁父类对象时,会先执行子类的析构函数,销毁掉子类对象。
  • 为什么构造函数不能被定义成virtual的?
虚函数内部是通过虚函数表来实现的,在执行时能通过vptr指针指向正确的子类对象函数。而在创建对象时,必须要知道创建对象的准确类型,因此构造函数不能为虚。
  • 析构函数可以时内联函数吗?
可以

static关键字的作用:

  1. 修饰局部变量:使得该变量在函数运行完后不会被释放,一直存在于整个程序的运行周期。
  2. 限制函数或者变量的作用域:在某一模块内声明的static变量或者函数无法被其他模块使用,例如使用static修饰的全局变量其他模块不能使用,static修饰的函数其他模块也不能使用。
  3. 作为类成员函数或者变量:被static修饰过的成员变量或函数生存在整个程序周期中,所有的类共享同一个静态成员。使用前必须在类外部手动定义该变量,并且被static修饰过后无法访问类里面的this指针

一、线程的基本使用

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

这样就可以利用起来很多被浪费掉的时间,例如执行阻塞任务,磁盘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;
}

编译运行:

一、题目

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

二、思路

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

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

  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先打印出来了,并且三个实例的地址也都是相同的:

一、两者对比

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

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

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

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

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

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

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

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

进程比线程稳定:

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

表格对比

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

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

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

三、参考文档

Difference between Process and Thread

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

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

使用map要添加头文件#include <map>,命名空间using namespace std

初始化一个map:

map<int, bool> m1; 
map<int, const char *> m2; 

对于C++11,还可以在初始化时设定一系列初始值:

map<const char *, int> age_map = {
    pair<const char *, int>("maqian", 22),
    pair<const char *, int>("xiaobai", 1)
};

基本用法:

// 插入元素
insert();
// 删除元素
erase();
// 元素是否存在
count():
// 得到k的值
map[k];

注意的是获取key的值的时候,如果不存在这个元素,map将会自动增加一个当前key的元素。

int main() {
    map<int, bool> m1;
    map<int, const char *> m2;

    map<const char *, int> age_map = {
        pair<const char *, int>("maqian", 22),
        pair<const char *, int>("xiaobai", 1)
    };

    age_map.insert(
        pair<const char *, int>("zhouzhou", 3)
    );

    printf("count: %d, name: %s, age: %d\n", age_map.size(), "maqian", age_map["maqian"]);

    age_map.erase("maqian");
    printf("count: %d, name: %s, age: %d\n", age_map.size(), "maqian", age_map["maqian"]);


    return 0;
}
count: 3, name: maqian, age: 22
count: 3, name: maqian, age: 0
因此,注意不要通过map[x]来判断元素是否存在,使用count()方法。