Post

多线程

C++多线程相关笔记

在传统的C++(C++11之前) 中并没有引入线程这个概念,在C++11出来之前,如果想要在C++中实现多线程,需要借助操作系统平台提供的API,比如Linux的<pthread.h> ,或者Windows下的<windows.h>
C++11提供了语言层面上的多线程,包含在头文件<thread> 中。其解决了跨平台的问题,提供了管理线程、保护共享数据、线程间同步操作、原子操作等类。C++11引入了5个头文件来支持多线程编程

头文件作用
<thread>提供多线程编程所需的类和函数,包括创建、启动、等待和管理线程
<mutex>提供互斥锁和其他同步原语的类和函数,用于保护共享资源,防止竞态条件
<atomic>提供原子操作库,用于执行线程安全的原子操作
<future>异步编程相关,用于异步执行函数和获取结果
<condition_variable>提供条件变量类,用于线程之间的协调和通信

std::thread

std::thread常用函数

  • 构造&析构函数
函数类别作用
thread() noexcept默认构造函数创建一个线程
template <class Fn, class… Args>
explicit thread(Fn&& fn, Args&&… args)
初始化构造函数创建一个线程
args为参数
执行fn函数
thread(const thread&) = delete复制构造函数禁用std::thread类的拷贝构造函数
thread(thread&& x) noexcept移动构造函数构造一个与x相同的对象
会破坏x对象
~thread()析构函数析构对象
  • 成员函数
函数作用
void join()等待线程结束并清理资源(会阻塞)
bool joinable()返回线程是否可以执行join函数
void detach()启动的线程自主在后台运行
必须在线程创建时立即调用,且调用此函数会使其不能被join
std::thread::id get_id()获取线程id
thread& operator=(std::thread &&rhs)见移动构造函数
(如果对象是joinable,会调用std::terminate()结束程序)

例一:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
#include <thread>

void thread1() 
{ 
    std::cout << "Hello," << std::endl; 
}

void thread2() 
{ 
    std::cout << "World!" << std::endl; 
}

int main() 
{
	std::thread a(thread1);
    std::thread b(thread2);

	a.join();
	b.join();

	return 0;
}

输出结果:

1
Hello, World!

或者

1
2
World!
Hello,

例二:

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 <iostream>
#include <thread>

void countnumber(int id, unsigned int n)
{
    for (unsigned int i = 1; i <= n; i++)
    {
        ;
    }

    std::cout << "Thread " << id << " finished!" << std::endl;
}

int main()
{
    std::thread th[10];
    for (int i = 0; i < 10; i++)
    {
        th[i] = thread(countnumber, i, 100000000);
    }

    for (int i = 0; i < 10; i++)
    {
        th[i].join();
    }

    return 0;
}

输出结果之一:

1
2
3
4
5
6
7
8
9
10
Thread 4 finished!
Thread 8 finished!
Thread 6 finished!
Thread 3 finished!
Thread 1 finished!
Thread 7 finished!
Thread 9 finished!
Thread 5 finished!
Thread 2 finished!
Thread 0 finished!

例三:

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
#include <iostream>
#include <thread>

template <class T>
void changevalue(T &x, T val)
{
    x = val;
}

int main()
{
    std::thread th[100];
    int nums[100];
    for (int i = 0; i < 100; i++)
    {
        th[i] = std::thread(changevalue<int>, nums[i], i + 1);
    }

    for (int i = 0; i < 100; i++)
    {
        th[i].join();
        std::cout << nums[i] << std::endl;
    }

    return 0;
}

正常编译这个程序,编译器一定会报错。
原因是thread在传递参数时,是以右值传递(Args&&… args)的:

1
2
template <class Fn, class... Args>
explicit thread(Fn&& fn, Args&&... args)

如果要传递左值,std::refstd::cref解决了这个问题。 std::ref用于创建对对象的可修改引用(左值引用) std::cref用于创建对对象的常量引用(const 左值引用)

修改代码为

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
#include <iostream>
#include <thread>

template <class T>
void changevalue(T &x, T val)
{
    x = val;
}

int main()
{
    std::thread th[100];
    int nums[100];
    for (int i = 0; i < 100; i++)
    {
        th[i] = std::thread(changevalue<int>, ref(nums[i]), i + 1);
    }

    for (int i = 0; i < 100; i++)
    {
        th[i].join();
        std::cout << nums[i] << std::endl;
    }

    return 0;
}

输出结果:

1
2
3
4
5
6
7
1
2
3
4
...
99
100

注意事项:

  • 线程是在thread对象被定义的时候开始执行的,而不是在调用join函数时才执行的,调用join函数只是阻塞等待线程结束并回收资源
  • 分离的线程(执行过detach的线程)会在调用它的线程结束或自己结束时释放资源
  • 线程会在函数运行完毕后自动释放,不推荐利用其他方法强制结束线程,可能会因资源未释放而导致内存泄漏
  • 没有执行joindetach的线程在程序结束时会引发异常

std::this_thread

<thread>头文件中,不仅有std::thread这个类,而且还有一个std::this_thread命名空间,其可以很方便地让线程对自己进行控制

std::this_thread常用函数

函数作用
std::thread::id get_id() noexcept获取当前线程id
template<class Rep, class Period>
void sleep_for( const std::chrono::duration<Rep,
Period>& sleep_duration )
等待sleep_duration
void yield() noexcept暂时放弃线程的执行,将主动权交给其他线程

例四:

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
37
38
39
40
#include <iostream>
#include <thread>
#include <atomic>

std::atomic_bool ready;

void sleep(uintmax_t ms)
{
    std::this_thread::sleep_for(std::chrono::milliseconds(ms));
}
void count()
{
    while (!ready)
        std::this_thread::yield();
    for (int i = 0; i <= 20'0000'0000; i++)
        ;
    std::cout << "Thread " << std::this_thread::get_id() << " finished!" << std::endl;

    return;
}
int main()
{
    ready = 0;
    std::thread th[10];
    for (int i = 0; i < 10; i++)
    {
        th[i] = std::thread(::count);
    }
    sleep(5000);
    
    ready = true;
    std::cout << "Start!" << std::endl;

    for (int i = 0; i < 10; i++)
    {
        th[i].join();
    }

    return 0;
}

输出结果:

1
2
3
4
5
6
7
8
9
10
11
Start!
Thread 139621413664512 finished!
Thread 139621396879104 finished!
Thread 139621371700992 finished!
Thread 139621354915584 finished!
Thread 139621422057216 finished!
Thread 139621380093696 finished!
Thread 139621405271808 finished!
Thread 139621363308288 finished!
Thread 139621346522880 finished!
Thread 139621388486400 finished!

std::mutex和std::atomic

std::mutex

std::mutex是C++中最基本的互斥量,一个线程将mutex锁住时,其它的线程就不能操作mutex,直到这个线程将mutex解锁。

例五:

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
#include <iostream>
#include <thread>
#include <mutex>

int n = 0;
std::mutex mtx;

void count10000()
{
    for (int i = 1; i <= 10000; i++)
    {
        mtx.lock();
        n++;
        mtx.unlock();
    }
}

int main()
{
    std::thread th[100];
    for (std::thread &x : th)
    {
        x = std::thread(count10000);
    }

    for (std::thread &x : th)
    {
        x.join();
    }

    std::cout << n << std::endl;
    
    return 0;
}

输出结果为1000000,这里如果不使用锁,实际输出结果比1000000小,并且输出结果不定

std::mutex互斥类型

类型说明
std::mutex基本Mutex
std::recursive_mutex递归Mutex
std::time_mutex定时Mutex
std::recursive_timed_mutex定时递归Mutex

std::mutex常用成员函数(mutex代指对象)

函数作用
void lock()mutex上锁
如果mutex已经被其它线程上锁,那么会阻塞,直到解锁;
如果mutex已经被同一个线程锁住,会产生死锁
void unlock()解锁mutex,释放其所有权
如果有线程因为调用lock()而被阻塞,随机将mutex的控制权交给其中一个线程。
如果当前线程未锁定 mutex,会引发未定义的异常
bool try_lock()尝试将mutex上锁
如果mutex未被上锁,则将其上锁并返回true,如果mutex已被锁则返回false

std::lock_guard

创建std::lock_guard对象时,它将尝试获取提供给它的互斥锁的所有权。当控制流离开std::lock_guard 对象的作用域时,std::lock_guard析构并释放互斥量。

std::lock_guard特点:

  • 创建即加锁,作用域结束自动析构并解锁,无需手工解锁
  • 不能中途解锁,必须等作用域结束才解锁
  • 不能复制

std::unique_lock

std::unique_lock具有std::lock_guard的所有功能,同时又具有其他很多方法,使用起来更加灵活方便,能够应对更复杂的锁定需要。

std::unique_lock的特点:

  • 创建时可以不锁定(通过指定第二个参数为std::defer_lock),而在需要时再锁定
  • 可以随时加锁解锁
  • 作用域规则同std::lock_guard,析构时自动释放锁
  • 不可复制,可移动
  • 条件变量需要该类型的锁作为参数(此时必须使用std::unique_lock

std::atomic

std::mutex很好地解决了多线程资源争抢的问题,但加锁和解锁都需要进行额外的工作,还有最常见的死锁问题

例六:

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
#include <iostream>
#include <thread>
#include <atomic>

std::atomic<int> n;

void count10000()
{
    for (int i = 1; i <= 10000; i++)
    {
        n++;
    }
}

int main()
{
    n = 0;
    std::thread th[100];
    for (std::thread &x : th)
    {
        x = std::thread(count10000);
    }
    for (std::thread &x : th)
    {
        x.join();
    }

    std::cout << n << std::endl;

    return 0;
}

原子操作是最小的且不可并行化的操作

即使是多线程,也要像同步进行一样同步操作std::atomic对象,从而省去了std::mutex上锁、解锁的时间消耗

注意事项:
原子变量不能使用拷贝构造,初始之后才可以赋值

std::atomic常用成员函数

函数类型作用
atomic() noexcept = default默认构造函数构造std::atomic对象
(可通过atomic_init进行初始化)
constexpr atomic(T val) noexcept初始化构造函数构造一个std::atomic对象
val的值来初始化
atomic(const atomic&) = delete复制构造函数删除std::atomic类型的拷贝构造函数

std::async

注:std::async定义在future头文件中

std::async参数 不同于std::threadstd::async是一个函数,所以没有成员函数。

重载版本作用
template <class Fn, class… Args>
future<typename result_of<Fn(Args…)>::type>
async (Fn&& fn, Args&&… args)
异步或同步(根据操作系统而定)
args为参数执行fn
传递引用参数需要std::refstd::cref
template <class Fn, class… Args>
future<typename result_of<Fn(Args…)>::type>
async (launch policy, Fn&& fn, Args&&… args);
异步或同步(根据policy参数而定)
args为参数执行fn
传递引用参数需要std::refstd::cref

std::launch强枚举类(enum class)
std::launch有2个枚举值和1个特殊值

标识符实际值作用
枚举值launch::async0x1(1)异步启动
枚举值launch::deferred0x2(2)调用future::getfuture::wait时同步启动
特殊值launch::asynclaunch::defereed0x3(3)同步或异步,根据操作系统而定

例七:

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

int main()
{
    std::async(
        std::launch::async, [](const char *message)
        { std::cout << message << std::flush; },
        "Hello, ");
    std::cout << "World!" << std::endl;

    return 0;
}

输出结果:

1
Hello, World!

std::future

例八:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
#include <future>

template <class... Args>
decltype(auto) sum(Args &&...args)
{
    return (0 + ... + args); // "0 +"避免空参数包错误
}

int main()
{
    std::future<int> val = std::async(std::launch::async, sum<int, int, int>, 1, 10, 100); // 必须带模板参数

    std::cout << val.get() << std::endl;

    return 0;
}

std::future常用成员函数

  • 构造&析构函数
函数类型作用
future() noexcept默认构造函数构造一个空的、无效的future对象
可以移动分配到另一个future对象
future(const future&) = delete复制构造函数删除std::future类型的拷贝构造函数
future (future&& x) noexcept移动构造函数构造一个与x相同的对象并破坏x
~future()析构函数析构对象
  • 常用成员函数
函数作用
一般:T get()
当类型为引用:R& future<R&>::get()
当类型为void:void future::get()
阻塞等待线程结束并获取返回值
若类型为void,则与future::wait()相同
只能调用一次
void wait() const阻塞等待线程结束
template <class Rep, class Period>
future_status wait_for(const chrono::duration<Rep,Period>& rel_time) const;
阻塞等待rel_time
若在这段时间内线程结束则返回future_status::ready
若没结束则返回future_status::timeout
async是以launch::deferred启动的
则不会阻塞并立即返回future_status::deferred

std::future_status强枚举类
见上文std::future::wait_for解释

例九:

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
#include <iostream>
#include <future>

void count_big_number()
{
    for (int i = 0; i <= 10'0000'0000; i++)
    {
        ;
    }
}

int main()
{
    std::future<void> fut = std::async(std::launch::async, count_big_number);
    std::cout << "Please wait" << std::flush;

    while (fut.wait_for(std::chrono::seconds(1)) != std::future_status::ready) // 每次等待1秒
    {
        std::cout << '.' << std::flush;
    }

    std::cout << std::endl
         << "Finished!" << std::endl;

    return 0;
}

std::promise

在使用std::thread而不是std::async时,直接使用std::future<int>会报错,需要通过传递引用的方式来获取std::thread的返回值

例十:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>

constexpr long double PI = 3.14159265358979323846264338327950288419716939937510582097494459230781640628;

// 求圆的直径、周长及面积
void get_circle_info(double r, double &d, double &c, double &s)
{
    d = r * 2;
    c = PI * d;
    s = PI * r * r;
}

int main()
{
    double r;
    std::cin >> r;
    double d, c, s;
    get_circle_info(r, d, c, s);
    std::cout << d << ' ' << c << ' ' << s << std::endl;

    return 0;
}

std::promise实际上是std::future的一个包装。
因为std::future的值不能被改变,但可以通过std::promise来创建一个拥有特定值的std::future

std::promise常用成员函数

  • 构造&析构函数
函数类型作用
promise()默认构造函数构造空的std::promise对象
template <class Alloc>
promise(allocator_arg_t aa,
const Alloc& alloc)
构造函数与默认构造函数相同
但使用特定的内存分配器alloc构造对象
promise (const promise&) = delete复制构造函数删除std::promise类型的拷贝构造函数
promise (promise&& x) noexcept移动构造函数构造一个与x相同的对象并破坏x
~promise()析构函数析构对象
  • 常用成员函数
函数作用
一般:
void set_value (const T& val)
void set_value (T&& val)
类型为引用:
void promise<R&>::set_value (R& val)
类型为void:
void promise::set_value (void)
设置promise的值并将共享状态设为ready
(将future_status设为ready
void特化:只将共享状态设为ready
future get_future()构造一个future对象,其值和status与promise相同

例十一: 以例八中的代码为基础加以修改

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
#include <iostream>
#include <thread>
#include <future>

template <class... Args>
decltype(auto) sum(Args &&...args)
{
    return (0 + ... + args);
}

template <class... Args>
void sum_thread(std::promise<long long> &val, Args &&...args)
{
    val.set_value(sum(args...));
}

int main()
{
    std::promise<long long> sum_value;
    std::thread get_sum(sum_thread<int, int, int>, ref(sum_value), 1, 10, 100);
    std::cout << sum_value.get_future().get() << std::endl;
    get_sum.join();

    return 0;
}

condition_variable

<condition_variable>头文件主要包含了与条件变量相关的类和函数。
包括:

    • std::condition_variable
    • std::condition_variable_any
  • 枚举类型
    • std::cv_status。
  • 函数
    • std::notify_all_at_thread_exit()

std::condition_variable必须结合std::unique_lock使用,std::condition_variable_any可以使用任何的锁。

std::condition_variable常用成员函数

函数作用
wait(lock)使当前线程等待,直到其他线程通知或唤醒
需要一个已经上锁的std::unique_lock对象作为参数
wait(lock, pred)额外提供一个条件函数
只有当pred返回true时,才会真正等待,否则会继续等待或返回
notify_one()通知等待在条件变量上的一个线程,使其从等待状态唤醒
notify_all()通知等待在条件变量上的所有线程,使它们从等待状态唤醒
This post is licensed under CC BY 4.0 by the author.