C++多线程编程

多线程是学习SLAM过程中不可缺少的一步,正确的使用多线程能使SLAM系统的运行速度提升很多,达到更高的精度和速度。本节就来学习SLAM中的多线程编程方法。

多线程是多任务处理的一种特殊形式,多任务处理允许让电脑同时运行两个或两个以上的程序。一般情况下,两种类型的多任务处理:基于进程和基于线程

  • 基于进程的多任务处理是程序的并发执行。
  • 基于线程的多任务处理是同一程序的片段的并发执行。

多线程程序包含可以同时运行的两个或多个部分。这样的程序中的每个部分称为一个线程,每个线程定义了一个单独的执行路径。

线程

概念

线程在Unix系统下,通常被称为轻量级的进程,线程虽然不是进程,但却可以看作是Unix进程的表亲,同一进程中的多条线程将共享该进程中的全部系统资源,如虚拟地址空间,文件描述符和信号处理等等。但同一进程中的多个线程有各自的调用栈(call stack),自己的寄存器环境(register context),自己的线程本地存储(thread-local storage)。 一个进程可以有很多线程,每条线程并行执行不同的任务。

优点

线程可以提高应用程序在多核环境下处理诸如文件I/O或者socket I/O等会产生堵塞的情况的表现性能。在Unix系统中,一个进程包含很多东西,包括可执行程序以及一大堆的诸如文件描述符地址空间等资源。在很多情况下,完成相关任务的不同代码间需要交换数据。如果采用多进程的方式,那么通信就需要在用户空间和内核空间进行频繁的切换,开销很大。但是如果使用多线程的方式,因为可以使用共享的全局变量,所以线程间的通信(数据交换)变得非常高效。

C++

一个简单的Demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
#include <thread>
#include <future>
using namespace std;
void helloworld()
{
cout << "hello world \n";
}
int main()
{
//开启一个线程
std::thread t(helloworld);
std::cout << "hello world main thread\n";

//线程的终结
t.join();

return 0;
}

多线程库

C++11中终于提供了多线程的标准库,提供了线程管理、保护共享数据、线程间同步操作、原子操作等类。

多线程库对应的头文件是#include<thread>,类名为std::thread

一个简单的串行程序如下:

1
2
3
4
5
6
7
8
9
10
11
#include <iostream>
#include <thread>

void function_1() {
std::cout << "I'm function_1()" << std::endl;
}

int main() {
function_1();
return 0;
}

这是一个典型的单线程的单进程程序,任何程序都是一个进程,main()函数就是其中的主线程,单个线程都是顺序执行。

将上面的程序改造成多线程程序其实很简单,让function_1()函数在另外的线程中执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
#include <thread>

void function_1() {
std::cout << "I'm function_1()" << std::endl;
}

int main() {
std::thread t1(function_1);
// do other things
t1.join();
return 0;
}

分析:

  1. 首先,构建一个std::thread对象t1,构造的时候传递了一个参数,这个参数是一个函数,这个函数就是这个线程的入口函数,函数执行完了,整个线程也就执行完了。
  2. 线程创建成功后,就会立即启动,并没有一个类似start的函数来显式的启动线程。
  3. 一旦线程开始运行, 就需要显式的决定是要等待它完成(join),或者分离它让它自行运行(detach)。注意:只需要在std::thread对象被销毁之前做出这个决定。这个例子中,对象t1是栈上变量,在main函数执行结束后就会被销毁,所以需要在main函数结束之前做决定。
  4. 这个例子中选择了使用t1.join(),主线程会一直阻塞着,直到子线程完成,join()函数的另一个任务是回收该线程中使用的资源。

线程对象和对象内部管理的线程的生命周期并不一样,如果线程执行的快,可能内部的线程已经结束了,但是线程对象还活着,也有可能线程对象已经被析构了,内部的线程还在运行。

线程创建

下面的程序,我们可以用它来创建一个 POSIX 线程:

1
2
#include <pthread.h>
pthread_create (thread, attr, start_routine, arg)

在这里,pthread_create 创建一个新的线程,并让它可执行。下面是关于参数的说明:

参数 描述
thread 指向线程标识符指针。
attr 一个不透明的属性对象,可以被用来设置线程属性。您可以指定线程属性对象,也可以使用默认值 NULL。
start_routine 线程运行函数起始地址,一旦线程被创建就会执行。
arg 运行函数的参数。它必须通过把引用作为指针强制转换为 void 类型进行传递。如果没有传递参数,则使用 NULL。

创建线程成功时,函数返回 0,若返回值不为 0 则说明创建线程失败。

终止线程

使用下面的程序,我们可以用它来终止一个 POSIX 线程:

1
2
#include <pthread.h>
pthread_exit (status)

在这里,pthread_exit 用于显式地退出一个线程。通常情况下,pthread_exit() 函数是在线程完成工作后无需继续存在时被调用。

如果 main() 是在它所创建的线程之前结束,并通过 pthread_exit() 退出,那么其他线程将继续执行。否则,它们将在 main() 结束时自动被终止。

互斥锁

lock_guard 和 unique_lock 的简单使用

在 C++1x 之后,我们编写多线程可以直接使用标准库里的函数,不必根据平台的不同使用 posix_thread 之类的库了,这样就实现了跨平台的编程。

一、std::lock_guard 的介绍

​ std::lock_guard 的原型是一个模板类,定义如下:

1
template<class Mutex> class lock_guard;

​ lock_guard 通常用来管理一个 std::mutex 类型的对象,通过定义一个 lock_guard 一个对象来管理 std::mutex 的上锁和解锁。在 lock_guard 初始化的时候进行上锁,然后在 lock_guard 析构的时候进行解锁。这样避免了我们对 std::mutex 的上锁和解锁的管理。注意,lock_guard 并不管理 std::mutex 对象的声明周期,也就是说在使用 lock_guard 的过程中,如果 std::mutex 的对象被释放了,那么在 lock_guard 析构的时候进行解锁就会出现空指针错误之类。

二、std::unique_lock 的介绍

​ unique_lock 和 lock_guard 一样,对 std::mutex 类型的互斥量的上锁和解锁进行管理,一样也不管理 std::mutex 类型的互斥量的声明周期。但是它的使用更加的灵活,支持的构造函数如下:

image-20220626225902414

  1. 默认构造函数,新创建的 unique_lock 对象不管理任何 Mutex 对象。

  2. locking 初始化,新创建的 unique_lock 对象管理 Mutex 对象 m,并尝试调用 m.lock() 对 Mutex 对象进行上锁,如果此时另外某个 unique_lock 对象已经管理了该 Mutex 对象 m,则当前线程将会被阻塞。

  3. try-locking 初始化,新创建的 unique_lock 对象管理 Mutex 对象 m,并尝试调用 m.try_lock() 对 Mutex 对象进行上锁,但如果上锁不成功,并不会阻塞当前线程。

  4. deferred 初始化,新创建的 unique_lock 对象管理 Mutex 对象 m,但是在初始化的时候并不锁住 Mutex 对象。 m 应该是一个没有当前线程锁住的 Mutex 对象。

  5. adopting 初始化,新创建的 unique_lock 对象管理 Mutex 对象 m, m 应该是一个已经被当前线程锁住的 Mutex 对象。(并且当前新创建的 unique_lock 对象拥有对锁(Lock)的所有权)。

  6. locking,一段时间(duration) 新创建的 unique_lock 对象管理 Mutex 对象 m,并试图通过调用 m.try_lock_for(rel_time) 来锁住 Mutex 对象一段时间(rel_time)。

  7. locking,直到某个时间点(time point) 新创建的 unique_lock 对象管理 Mutex 对象m,并试图通过调用 m.try_lock_until(abs_time) 来在某个时间点(abs_time)之前锁住 Mutex 对象。

  8. 拷贝构造 [被禁用],unique_lock 对象不能被拷贝构造。

  9. 移动(move)构造,新创建的 unique_lock 对象获得了由 x 所管理的 Mutex 对象的所有权(包括当前 Mutex 的状态)。调用 move 构造之后, x 对象如同通过默认构造函数所创建的,就不再管理任何 Mutex 对象了。

std::unique_lock和std::lock_guard类的区别

std::unique_lock 与std::lock_guard都能实现自动加锁与解锁功能,但是std::unique_lock要比std::lock_guard更灵活,但是更灵活的代价是占用空间相对更大一点且相对更慢一点。

信号量机制

0 C++中信号量的使用

1、sem_init函数

sem_init函数是Posix信号量操作中的函数。sem_init() 初始化一个定位在 sem 的匿名信号量。value 参数指定信号量的初始值。 pshared 参数指明信号量是由进程内线程共享,还是由进程之间共享。如果 pshared 的值为 0,那么信号量将被进程内的线程共享,并且应该放置在这个进程的所有线程都可见的地址上(如全局变量,或者堆上动态分配的变量)。

如果 pshared 是非零值,那么信号量将在进程之间共享,并且应该定位共享内存区域(见 shm_open(3)、mmap(2) 和 shmget(2))。因为通过 fork(2) 创建的孩子继承其父亲的内存映射,因此它也可以见到这个信号量。所有可以访问共享内存区域的进程都可以用 sem_post(3)、sem_wait(3) 等等操作信号量。初始化一个已经初始的信号量其结果未定义。

1
2
3
#include <semaphore.h>
// 初始化
int sem_init(sem_t *sem,int pshared,unsigned int value);

返回值 :
sem_init() 成功时返回 0;错误时,返回 -1,并把 errno 设置为合适的值。

2、semaphore相关函数

1
2
3
4
5
6
7
// 销毁
int sem_destroy(sem_t *sem);
//
int sem_wait(sem_t *sem); // 资源减少1
int sem_trywait(sem_t *sem);
int sem_post(sem_t *sem); // 资源增加1
int sem_getvalue(sem_t *sem);

相关例题

1114. 按序打印

我们提供了一个类:

1
2
3
4
5
public class Foo {
public void first() { print("first"); }
public void second() { print("second"); }
public void third() { print("third"); }
}

三个不同的线程将会共用一个 Foo 实例。

  • 线程 A 将会调用 first() 方法
  • 线程 B 将会调用 second() 方法
  • 线程 C 将会调用 third() 方法

请设计修改程序,以确保 second() 方法在 first() 方法之后被执行,third() 方法在 second() 方法之后被执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <semaphore.h>
class Foo {
sem_t syn1 ;
sem_t syn2 ;
public:
Foo() {
sem_init(&syn1, 0, 0);
sem_init(&syn2, 0, 0);
}
void first(function<void()> printFirst) {
// printFirst() outputs "first". Do not change or remove this line.
printFirst();
sem_post(&syn1);
}

void second(function<void()> printSecond) {
// printSecond() outputs "second". Do not change or remove this line.
sem_wait(&syn1);
printSecond();
sem_post(&syn2);
}

void third(function<void()> printThird) {
// printThird() outputs "third". Do not change or remove this line.
sem_wait(&syn2);
printThird();
}
};

1115. 交替打印FooBar

难度中等73

我们提供一个类:

1
2
3
4
5
6
7
8
9
10
11
12
13
class FooBar {
public void foo() {
for (int i = 0; i < n; i++) {
print("foo");
}
}

public void bar() {
for (int i = 0; i < n; i++) {
print("bar");
}
}
}

两个不同的线程将会共用一个 FooBar 实例。其中一个线程将会调用 foo() 方法,另一个线程将会调用 bar() 方法。

请设计修改程序,以确保 “foobar” 被输出 n 次。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <semaphore.h>

class FooBar {
private:
int n;
sem_t syn1;
sem_t syn2;
public:
FooBar(int n) {
this->n = n;
sem_init(&syn1,0,1);
sem_init(&syn2,0,0);
}

void foo(function<void()> printFoo) {

for (int i = 0; i < n; i++) {

// printFoo() outputs "foo". Do not change or remove this line.
sem_wait(&syn1);
printFoo();
sem_post(&syn2);
}
}

void bar(function<void()> printBar) {

for (int i = 0; i < n; i++) {

// printBar() outputs "bar". Do not change or remove this line.
sem_wait(&syn2);
printBar();
sem_post(&syn1);
}
}
};

1226. 哲学家进餐

5 个沉默寡言的哲学家围坐在圆桌前,每人面前一盘意面。叉子放在哲学家之间的桌面上。(5 个哲学家,5 根叉子)

所有的哲学家都只会在思考和进餐两种行为间交替。哲学家只有同时拿到左边和右边的叉子才能吃到面,而同一根叉子在同一时间只能被一个哲学家使用。每个哲学家吃完面后都需要把叉子放回桌面以供其他哲学家吃面。只要条件允许,哲学家可以拿起左边或者右边的叉子,但在没有同时拿到左右叉子时不能进食。

假设面的数量没有限制,哲学家也能随便吃,不需要考虑吃不吃得下。

设计一个进餐规则(并行算法)使得每个哲学家都不会挨饿;也就是说,在没有人知道别人什么时候想吃东西或思考的情况下,每个哲学家都可以在吃饭和思考之间一直交替下去。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class DiningPhilosophers {
pthread_mutex_t forks[5];
public:
DiningPhilosophers() {
for(int i = 0; i < 5; i++) pthread_mutex_init(forks + i, NULL);
}

void wantsToEat(int philosopher,
function<void()> pickLeftFork,
function<void()> pickRightFork,
function<void()> eat,
function<void()> putLeftFork,
function<void()> putRightFork) {
int left_hand = philosopher, right_hand = (philosopher + 1) % 5; //左右手序号
int ret1 = 1, ret2 = 1;
while(ret1 || ret2) { //尝试同时锁两个直到成功
if(ret1 == 0) pthread_mutex_unlock(forks + left_hand); //锁失败锁住的打开
if(ret2 == 0) pthread_mutex_unlock(forks + right_hand);
ret1 = pthread_mutex_trylock(forks + left_hand); //继续尝试
ret2 = pthread_mutex_trylock(forks + right_hand); //pthread_mutex_trylock 成功会返回0
}
pickLeftFork();
pickRightFork();
eat();
putLeftFork();
putRightFork();
pthread_mutex_unlock(forks + left_hand); //全部解锁
pthread_mutex_unlock(forks + right_hand);
}
};
打赏
  • 版权声明: 本博客所有文章除特别声明外,均采用 Apache License 2.0 许可协议。转载请注明出处!
  • © 2019-2022 guoben
  • PV: UV:

微信