当前位置:   article > 正文

C++多线程

c++多线程

一、多线程简介

C++11 新标准中引入了四个头文件来支持多线程编程,他们分别是<atomic><thread><mutex><condition_variable><future>

  • <atomic> :该头文主要声明了两个类, std::atomic 和 std::atomic_flag,另外还声明了一套 C 风格的原子类型和与 C 兼容的原子操作的函数。
  • <thread>:该头文件主要声明了 std::thread 类,另外 std::this_thread 命名空间也在该头文件中。
  • <mutex>:该头文件主要声明了与互斥量(mutex)相关的类,包括 std::mutex 系列类,std::lock_guard, std::unique_lock, 以及其他的类型和函数。
  • <condition_variable>:该头文件主要声明了与条件变量相关的类,包括 std::condition_variable 和 std::condition_variable_any。
  • <future>:该头文件主要声明了 std::promise, std::package_task 两个 Provider 类,以及 std::future 和 std::shared_future 两个future 类,另外还有一些与之相关的类型和函数,std::async() 函数就声明在此头文件中。

二、std::thread

下面是一个最简单的使用 std::thread 类的例子:

#include <stdio.h>
#include <stdlib.h>

#include <iostream> // std::cout
#include <thread>   // std::thread

void thread_task() {
    std::cout << "hello thread" << std::endl;
}

int main(int argc, const char *argv[])
{
    std::thread t(thread_task);
    t.join();

    return EXIT_SUCCESS;
}  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

std::thread 在 头文件中声明,因此使用 std::thread 时需要包含 <thread> 头文件。

2.1 std::thread 构造函数

thread构造.jpg

  1. 默认构造函数,创建一个空的 thread 执行对象。
  2. 初始化构造函数,创建一个 thread对象,该 thread对象可被 joinable,新产生的线程会调用 fn 函数,该函数的参数由 args 给出。
  3. 拷贝构造函数(被禁用),意味着 thread 不可被拷贝构造。
  4. move 构造函数,move 构造函数,调用成功之后 x 不代表任何 thread 执行对象。
    注意:可被 joinable 的 thread 对象必须在他们销毁之前被主线程 join 或者将其设置为 detached.

std::thread 各种构造函数例子如下:

#include <iostream>
#include <utility>
#include <thread>
#include <chrono>
#include <functional>
#include <atomic>
 
void f1(int n)
{
    for (int i = 0; i < 5; ++i) {
        std::cout << "Thread " << n << " executing\n";
        std::this_thread::sleep_for(std::chrono::milliseconds(10));
    }
}
 
void f2(int& n)
{
    for (int i = 0; i < 5; ++i) {
        std::cout << "Thread 2 executing\n";
        ++n;
        std::this_thread::sleep_for(std::chrono::milliseconds(10));
    }
}
 
int main()
{
    int n = 0;
    std::thread t1; // t1 is not a thread
    std::thread t2(f1, n + 1); // pass by value
    std::thread t3(f2, std::ref(n)); // pass by reference
    std::thread t4(std::move(t3)); // t4 is now running f2(). t3 is no longer a thread
    t2.join();
    t4.join();
    std::cout << "Final value of n is " << n << '\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

2.2 move 赋值操作

move赋值操作.jpg

  1. move 赋值操作,如果当前对象不可 joinable,需要传递一个右值引用(rhs)给 move 赋值操作;如果当前对象可被 joinable,则 terminate() 报错。
  2. 拷贝赋值操作被禁用,thread 对象不可被拷贝。

下面是例子:

#include <stdio.h>
#include <stdlib.h>

#include <chrono>    // std::chrono::seconds
#include <iostream>  // std::cout
#include <thread>    // std::thread, std::this_thread::sleep_for

void thread_task(int n) {
    std::this_thread::sleep_for(std::chrono::seconds(n));
    std::cout << "hello thread "
        << std::this_thread::get_id()
        << " paused " << n << " seconds" << std::endl;
}

int main(int argc, const char *argv[])
{
    std::thread threads[5];
    std::cout << "Spawning 5 threads...\n";
    for (int i = 0; i < 5; i++) {
        threads[i] = std::thread(thread_task, i + 1);
    }
    std::cout << "Done spawning threads! Now wait for them to join\n";
    for (auto& t: threads) {
        t.join();
    }
    std::cout << "All threads joined.\n";

    return EXIT_SUCCESS;
}  
  • 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

2.3 其他成员函数

get_id
获取线程 ID。

joinable
检查线程是否可被 join,如果线程未被joindetach则返回true

join
Join 线程,阻塞当前线程等待该线程返回。

detach
Detach 线程,不阻塞当前线程,不等待该线程返回,相当于这是个守护线程。

swap
Swap 线程 。

native_handle
返回 native handle。

hardware_concurrency [static]
检测硬件并发特性。

2.4 创建线程的几种方法

2.4.1 使用普通函数
#include <stdio.h>
#include <stdlib.h>

#include <iostream> // std::cout
#include <thread>   // std::thread

void thread_task(int i) 
{
	std::cout << "hello thread " << i << std::endl;
}

int main()
{
	std::thread t(thread_task, 5);
	t.join();

	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
2.4.2 lambda函数
#include <stdio.h>
#include <stdlib.h>

#include <iostream> // std::cout
#include <thread>   // std::thread

int main()
{
	auto thread_task = [] (int i) {std::cout << "hello thread " << i << std::endl; };
	std::thread t(thread_task, 5);
	t.join();

	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
2.4.3 类的成员函数
#include <stdio.h>
#include <stdlib.h>

#include <iostream> // std::cout
#include <thread>   // std::thread

class MyClass
{
public:
	void thread_task(int i)
	{
		std::cout << "hello thread " << i << std::endl;
	}
};

int main()
{
	MyClass myclass;
	//std::thread t(&MyClass::thread_task, myclass, 5);		//这种调用会调用myclass的副本
	std::thread t(&MyClass::thread_task, &myclass, 5);
	t.join();

	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
2.4.4 可调用对象
#include <stdio.h>
#include <stdlib.h>

#include <iostream> // std::cout
#include <thread>   // std::thread

class MyClass
{
public:
	void operator ()(int i)
	{
		std::cout << "hello thread " << i << std::endl;
	}
};

int main()
{
	MyClass myclass;
	std::thread t(myclass, 5);
	t.join();

	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

2.5 关于线程参数的传递

除非显式的调用std::ref声明传入引用,不然哪怕被调用的函数参数是引用,实际上被创建的线程都会复制一份参数,不是真正的引用,而且如果没有显式的调用std::ref,那么被调用的函数参数要写引用也只能用const引用。
上面使用类的成员函数中有特殊情况示例。

三、std::mutex

3.1 mutex的作用

可以尝试下如下代码,由于创建了两个线程来对a进行写操作,最后的结果往往不是预期的200000。
如果共享的数据只读,那么不需要特殊处理,可以多个线程同时读,如果共享的数据存在写,那么需要互斥锁来获取所有权以便在本线程写的时候不让别的线程读写。

#include <stdio.h>
#include <stdlib.h>

#include <iostream> // std::cout
#include <thread>   // std::thread

int a = 0;

void thread_task1()
{
	for (int i = 0; i < 100000; ++i)
	{
		++a;
	}
}

void thread_task2()
{
	for (int i = 0; i < 100000; ++i)
	{
		++a;
	}
}

int main()
{
	std::thread t1(thread_task1);
	std::thread t2(thread_task2);
	t1.join();
	t2.join();
	std::cout << a << std::endl;
	return 0;
}
  • 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

于是定义了一个mutex对象,在某个线程获取了互斥量的锁后,不允许别的线程再获取锁。这下结果就必然是20000了。

#include <stdio.h>
#include <stdlib.h>

#include <iostream> // std::cout
#include <thread>   // std::thread
#include <mutex>	// std::mutex

int a = 0;
std::mutex mtx;

void thread_task1()
{
	for (int i = 0; i < 100000; ++i)
	{
		mtx.lock();
		++a;
		mtx.unlock();
	}
}

void thread_task2()
{
	for (int i = 0; i < 100000; ++i)
	{
		mtx.lock();
		++a;
		mtx.unlock();
	}
}

int main()
{
	std::thread t1(thread_task1);
	std::thread t2(thread_task2);
	t1.join();
	t2.join();
	std::cout << a << std::endl;
	return 0;
}
  • 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

3.2 std::mutex介绍

std::mutex:最基本的 Mutex 类。
std::recursive_mutex:递归 Mutex 类。
std::time_mutex:定时 Mutex 类。
std::recursive_timed_mutex:定时递归 Mutex 类。

std::mutex是C++11 中最基本的互斥量,std::mutex 对象提供了独占所有权的特性——即不支持递归地对 std::mutex 对象上锁,而 std::recursive_lock 则可以递归地对互斥量对象上锁。

构造函数,std::mutex不允许拷贝构造,也不允许 move 拷贝,最初产生的 mutex 对象是处于 unlocked 状态的。

3.2.1 lock()

调用线程将锁住该互斥量。线程调用该函数会发生下面 3 种情况:

  1. 如果该互斥量当前没有被锁住,则调用线程将该互斥量锁住,直到调用 unlock之前,该线程一直拥有该锁。
  2. 如果当前互斥量被其他线程锁住,则当前的调用线程被阻塞住。
  3. 如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)。
3.2.2 unlock()

解锁,释放对互斥量的所有权,如果没有锁的所有权尝试解锁会导致程序异常。

3.2.3 try_lock()

尝试锁住互斥量,如果互斥量被其他线程占有,则当前线程也不会被阻塞。线程调用该函数也会出现下面 3 种情况:

  1. 如果当前互斥量没有被其他线程占有,则该线程锁住互斥量,直到该线程调用 unlock 释放互斥量。
  2. 如果当前互斥量被其他线程锁住,则当前调用线程返回 false,而并不会被阻塞掉。
  3. 如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)。
3.2.4 try_lock_until()
std::mutex mut;
mut.try_lock_until(std::chrono::steady_clock::now() + std::chrono::milliseconds(100));
  • 1
  • 2
3.2.5 try_lock_for()
std::mutex mut;
mut.try_lock_until(std::chrono::milliseconds(100));
  • 1
  • 2

3.3 std::recursive_mutex介绍

std::recursive_mutex 与 std::mutex 一样,也是一种可以被上锁的对象,但是和 std::mutex 不同的是,std::recursive_mutex 允许同一个线程对互斥量多次上锁(即递归上锁),来获得对互斥量对象的多层所有权,std::recursive_mutex 释放互斥量时需要调用与该锁层次深度相同次数的 unlock(),可理解为 lock() 次数和 unlock() 次数相同,除此之外,std::recursive_mutex 的特性和 std::mutex 大致相同。

3.4 std::time_mutex介绍

std::time_mutex 比 std::mutex 多了两个成员函数,try_lock_for(),try_lock_until()。

try_lock_for 函数接受一个时间范围,表示在这一段时间范围之内线程如果没有获得锁则被阻塞住(与 std::mutex 的 try_lock() 不同,try_lock 如果被调用时没有获得锁则直接返回 false),如果在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如果超时(即在指定时间内还是没有获得锁),则返回 false。

try_lock_until 函数则接受一个时间点作为参数,在指定时间点未到来之前线程如果没有获得锁则被阻塞住,如果在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如果超时(即在指定时间内还是没有获得锁),则返回 false。

下面的小例子说明了 std::time_mutex 的用法:

#include <iostream>       // std::cout
#include <chrono>         // std::chrono::milliseconds
#include <thread>         // std::thread
#include <mutex>          // std::timed_mutex

std::timed_mutex mtx;

void fireworks() {
  // waiting to get a lock: each thread prints "-" every 200ms:
  while (!mtx.try_lock_for(std::chrono::milliseconds(200))) {
    std::cout << "-";
  }
  // got a lock! - wait for 1s, then this thread prints "*"
  std::this_thread::sleep_for(std::chrono::milliseconds(1000));
  std::cout << "*\n";
  mtx.unlock();
}

int main ()
{
  std::thread threads[10];
  // spawn 10 threads:
  for (int i=0; i<10; ++i)
    threads[i] = std::thread(fireworks);

  for (auto& th : threads) th.join();

  return 0;
}
  • 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

3.5 std::recursive_timed_mutex介绍

和 std:recursive_mutex 与 std::mutex 的关系一样,std::recursive_timed_mutex 的特性也可以从 std::timed_mutex 推导出来。

3.6 死锁

多个进程造成死锁。
由于下面例子中两个线程各自尝试获取已被另一个线程锁住的锁权限,会造成死锁。

#include <stdio.h>
#include <stdlib.h>

#include <iostream> // std::cout
#include <thread>   // std::thread
#include <mutex>	// std::mutex

int a = 0;
std::mutex mtx1;
std::mutex mtx2;

void thread_task1()
{
	for (int i = 0; i < 100000; ++i)
	{
		mtx1.lock();
		mtx2.lock();
		++a;
		mtx2.unlock();
		mtx1.unlock();
	}
}

void thread_task2()
{
	for (int i = 0; i < 100000; ++i)
	{
		mtx2.lock();
		mtx1.lock();
		++a;
		mtx1.unlock();
		mtx2.unlock();
	}
}

int main()
{
	std::thread t1(thread_task1);
	std::thread t2(thread_task2);
	t1.join();
	t2.join();
	std::cout << a << std::endl;
	return 0;
}
  • 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
  • 41
  • 42
  • 43
  • 44

四、std::lock

4.1 lock 类

std::lock_guard:与 mutex RAII 相关,方便线程对互斥量上锁。
std::unique_lock:与 mutex RAII 相关,方便线程对互斥量上锁,但提供了更好的上锁和解锁控制。

4.2 std::lock_guard

在定义时构造函数中会lock,析构函数会自动unlock。

使用了lock_guard后就不应该对mutex再使用lock或unlock了。

#include <stdio.h>
#include <stdlib.h>

#include <iostream> // std::cout
#include <thread>   // std::thread
#include <mutex>	// std::mutex

int a = 0;
std::mutex mtx;

void thread_task1()
{
	for (int i = 0; i < 100000; ++i)
	{
		std::lock_guard<std::mutex> lock(mtx);
		//mtx.lock();
		++a;
		//mtx.unlock();
	}
}

void thread_task2()
{
	for (int i = 0; i < 100000; ++i)
	{
		std::lock_guard<std::mutex> lock(mtx);
		//mtx.lock();
		++a;
		//mtx.unlock();
	}
}

int main()
{
	std::thread t1(thread_task1);
	std::thread t2(thread_task2);
	t1.join();
	t2.join();
	std::cout << a << std::endl;
	return 0;
}
  • 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
  • 41

其他类型:

  • std::once_flag
  • std::adopt_lock_t
  • std::defer_lock_t
  • std::try_to_lock_t

4.3 std::lock函数

可以同时对多个互斥量上锁如果互斥量中有一个没锁住,那么它会解锁已锁住的互斥量,不断尝试同时锁住所有的互斥量,所以这个函数的结果要么是所有互斥量都锁住了要么时所有互斥量一个都没锁住。
比如上面讲到的死锁问题,就可以用这个函数解决。

#include <stdio.h>
#include <stdlib.h>

#include <iostream> // std::cout
#include <thread>   // std::thread
#include <mutex>    // std::mutex

int a = 0;
std::mutex mtx1;
std::mutex mtx2;

void thread_task1()
{
	for (int i = 0; i < 100000; ++i)
	{
		std::lock(mtx1, mtx2);
		//mtx1.lock();
		//mtx2.lock();
		++a;
		mtx2.unlock();
		mtx1.unlock();
	}
}

void thread_task2()
{
	for (int i = 0; i < 100000; ++i)
	{
		std::lock(mtx1, mtx2);
		//mtx1.lock();
		//mtx2.lock();
		++a;
		mtx1.unlock();
		mtx2.unlock();
	}
}

int main()
{
	std::thread t1(thread_task1);
	std::thread t2(thread_task2);
	t1.join();
	t2.join();
	std::cout << a << std::endl;
	return 0;
}
  • 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
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46

但是std::lock锁住的锁不会自动释放锁。
下面介绍std::lock_guard的std::adopt_lock参数。

4.4 std::adopt_lock参数

此参数作用就是让std::lock_guard在构造函数中不调用mutex的lock函数。
此参数也可用于std::unique::lock,作用一样。

#include <stdio.h>
#include <stdlib.h>

#include <iostream> // std::cout
#include <thread>   // std::thread
#include <mutex>    // std::mutex

int a = 0;
std::mutex mtx1;
std::mutex mtx2;

void thread_task1()
{
	for (int i = 0; i < 100000; ++i)
	{
		std::lock(mtx1, mtx2);
		std::lock_guard<std::mutex> lock1(mtx1, std::adopt_lock);
		std::lock_guard<std::mutex> lock2(mtx2, std::adopt_lock);
		++a;
	}
}

void thread_task2()
{
	for (int i = 0; i < 100000; ++i)
	{
		std::lock(mtx1, mtx2);
		std::lock_guard<std::mutex> lock1(mtx1, std::adopt_lock);
		std::lock_guard<std::mutex> lock2(mtx2, std::adopt_lock);
		++a;
	}
}

int main()
{
	std::thread t1(thread_task1);
	std::thread t2(thread_task2);
	t1.join();
	t2.join();
	std::cout << a << std::endl;
	return 0;
}
  • 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
  • 41
  • 42

4.5 std::unique_lock

与 Mutex RAII 相关,方便线程对互斥量上锁,但提供了更好的上锁和解锁控制,它的功能比std::lock_guard更多,摩尔嗯不带参数的std::unique_lock与默认不带参数的std::lock_guard功能基本一致。
std::unique_lock使用std::adopt_lock参数前,互斥锁必须是已经被本线程锁定的。
使用std::try_to_lock参数前,互斥锁必须没有被本线程锁定的,后续可以owns_lock()函数判断是否已经获取到了锁的所有权。
使用std::defer_lock参数前,互斥锁必须没有被本线程锁定的,同时也不在构造函数中对锁进行锁定,使用这个参数定义的std::unique_lock,后续可以使用std::unique_lock的成员函数lock、unlock、try_lock,最后可以不解锁也可以解锁,不解锁的话析构仍会自动解锁。
release()函数会释放锁的所有权,而不是解锁,返回指向锁的指针。

#include <stdio.h>
#include <stdlib.h>

#include <iostream> // std::cout
#include <thread>   // std::thread
#include <mutex>    // std::mutex

int a = 0;
std::mutex mtx;

void thread_task1()
{
	for (int i = 0; i < 100000; ++i)
	{
		std::unique_lock<std::mutex> lock(mtx);
		++a;
	}
}

void thread_task2()
{
	for (int i = 0; i < 100000; ++i)
	{
		std::unique_lock<std::mutex> lock(mtx);
		++a;
	}
}

int main()
{
	std::thread t1(thread_task1);
	std::thread t2(thread_task2);
	t1.join();
	t2.join();
	std::cout << a << std::endl;
	return 0;
}
  • 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

4.6 std::try_lock函数

尝试同时对多个互斥量上锁,注意与std::unique_lock的std::try_to_lock参数区分。

###std::call_once函数
如果多个线程需要同时调用某个函数,call_once 可以保证多个线程对该函数只调用一次。需要配合std::one_flag类使用。

#include <stdio.h>
#include <stdlib.h>

#include <iostream> // std::cout
#include <thread>   // std::thread
#include <mutex>    // std::mutex

int a = 0;
std::once_flag g_flag;

void thread_task1()
{
	++a;
}

void thread_task2()
{
	std::call_once(g_flag, thread_task1);
}

int main()
{
	std::thread t1(thread_task2);
	std::thread t2(thread_task2);
	t1.join();
	t2.join();
	std::cout << a << std::endl;
	return 0;
}
  • 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

当有多个线程调用的某个函数中有func,可以采用上述方法让该函数中的func只被执行一次。
thread_task2函数虽然被执行了两次,但是thread_task1只被执行了一次。

五、std::condition_variable

条件变量允许我们通过通知进而实现线程同步。
因此,您可以实现发送方/接收方或生产者/消费者之类的工作流。
在这样的工作流程中,接收者正在等待发送者的通知。如果接收者收到通知,它将继续工作。

#include <stdio.h>
#include <stdlib.h>

#include <iostream> // std::cout
#include <thread>   // std::thread
#include <mutex>    // std::mutex

int a = 0;
std::mutex mtx;
std::condition_variable cond;

void thread_task1()
{
	std::unique_lock<std::mutex> lock(mtx);
	cond.wait(lock, [] {return !(a % 1000); });
	//cond.wait(lock);
	++a;
}

void thread_task2()
{
	for (int i = 0; i < 100000; ++i)
	{
		std::unique_lock<std::mutex> lock(mtx);
		if (!(a % 1000))
			cond.notify_one();
		++a;
	}
}

int main()
{
	std::thread t1(thread_task1);
	std::thread t2(thread_task2);
	t1.join();
	t2.join();
	std::cout << a << std::endl;
	return 0;
}
  • 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

如果wait没有第二个参数,那么wait会默认返回false,等待其它线程notify,一旦notify就会返回true继续向下执行。
wait时,第二个参数如果为false,则会对第一个参数提供的锁进行所有权释放,以便别的线程可以获取锁的所有权。

关于notify_one如果有多个线程在wait,那么notify_one将会随机的notify其中一个线程,如果调用notify_all,那么将会notify所有在wait的线程。

六、std::atomic

std::atomic又称原子操作,是一种不需要用到互斥量(无锁)技术的多线程并发编程方式,执行过程中不会被打断、切换线程。
原子操作只能操作一些简单的数据类型,比如int、char、bool,只能进行最基本的++、++、+=、-=,涉及复杂运算操作的不是原子操作。

#include <stdio.h>
#include <stdlib.h>

#include <iostream> // std::cout
#include <thread>   // std::thread
#include <atomic>

std::atomic<int> a = 0;
//int a = 0;    //这是不使用mutex也不使用原子操作的原始版本,最后得到的不是期望的结果。

void thread_task1()
{
	for (int i = 0; i < 100000; ++i)
	{
		++a;
	}
}

void thread_task2()
{
	for (int i = 0; i < 100000; ++i)
	{
		++a;
	}
}

int main()
{
	std::thread t1(thread_task1);
	std::thread t2(thread_task2);
	t1.join();
	t2.join();
	std::cout << a << std::endl;
	return 0;
}
  • 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

七、std::async、std::future、std::packaged_task、std::promise

7.1 std::async

std::async用于创建异步任务,实际上就是创建一个线程执行相应任务,默认立即开始执行。
std::async就是异步编程的高级封装,std::async的操作,其实相当于封装了std::promise、std::packaged_task加上std::thread,基本上可以代替std::thread 的所有事情。

#include <stdio.h>
#include <stdlib.h>

#include <iostream> // std::cout
#include <thread>   // std::thread
#include <mutex>    // std::mutex
#include <future>	// std::future

int thread_task()
{
	std::this_thread::sleep_for(std::chrono::milliseconds(1000));
	std::cout << "thread_task" << std::endl;
	return 0;
}

int main()
{
	std::async(thread_task);
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
7.1.1 std::launch::deferred参数

如果对std::async使用该参数,那么这个任务将变成同步任务,不会立即执行,直到std::future类对象调用get()或者wait()时才会开始执行,而且不会创建子线程,会直接在本线程执行。

#include <stdio.h>
#include <stdlib.h>

#include <iostream> // std::cout
#include <thread>   // std::thread
#include <mutex>    // std::mutex
#include <future>	// std::future

int thread_task()
{
	std::this_thread::sleep_for(std::chrono::milliseconds(1000));
	std::cout << "thread_task" << std::endl;
	return 0;
}

int main()
{
	std::future<int> result =  std::async(std::launch::deferred, thread_task);
	//std::cout << result.get() << std::endl;
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
7.1.2 std::launch::async参数

这是std::async的默认参数,即创建一个子线程并立即开始执行。

#include <stdio.h>
#include <stdlib.h>

#include <iostream> // std::cout
#include <thread>   // std::thread
#include <mutex>    // std::mutex
#include <future>	// std::future

int thread_task()
{
	std::this_thread::sleep_for(std::chrono::milliseconds(1000));
	std::cout << "thread_task" << std::endl;
	return 0;
}

int main()
{
	std::future<int> result =  std::async(std::launch::async, thread_task);
	//std::cout << result.get() << std::endl;
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

7.3 std::future

std::async异步任务可以返回一个std::future对象,可用来保存子线程入口函数的返回值。
std::async、std::packaged_task 或 std::promise都能提供一个std::future对象给该异步操作的创建者。

#include <stdio.h>
#include <stdlib.h>

#include <iostream> // std::cout
#include <thread>   // std::thread
#include <mutex>    // std::mutex
#include <future>	// std::future

int thread_task()
{
	std::this_thread::sleep_for(std::chrono::milliseconds(1000));
	std::cout << "thread_task" << std::endl;
	return 0;
}

int main()
{
	std::future<int> result =  std::async(thread_task);
	std::cout << result.get() << std::endl;
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

使用std::future类的get()函数,可以阻塞等待子线程返回获取返回值.
std::future类的wait()函数,可以阻塞等待子线程执行结束,本身不返回结果。

get()函数职能调用一次,不可调用多次,调用多次会导致程序异常退出。

7.4 std::packaged_task

std::packaged_task 包装一个可调用的对象,并且允许异步获取该可调用对象产生的结果,从包装可调用对象意义上来讲,std::packaged_task 与 std::function 类似,只不过 std::packaged_task 将其包装的可调用对象的执行结果传递给一个 std::future 对象(该对象通常在另外一个线程中获取 std::packaged_task 任务的执行结果)。
std::packaged_task 对象内部包含了两个最基本元素:一、被包装的任务(stored task),任务(task)是一个可调用的对象,如函数指针、成员函数指针或者函数对象;二、共享状态(shared state),用于保存任务的返回值,可以通过 std::future 对象来达到异步访问共享状态的效果。

#include <stdio.h>
#include <stdlib.h>

#include <iostream> // std::cout
#include <thread>   // std::thread
#include <mutex>    // std::mutex
#include <future>	// std::future

int thread_task(int i)
{
	std::this_thread::sleep_for(std::chrono::milliseconds(1000));
	std::cout << "thread_task" << i << std::endl;
	return 0;
}

int main()
{
	std::packaged_task<int(int)> pack(thread_task);
	std::thread mythread(std::ref(pack), 5);
	mythread.join();
	std::future<int> result = pack.get_future();
	std::cout << result.get() << std::endl;
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

std::packaged_task对象还可以直接调用,类似函数调用,如上例中使用pack(5)。

7.5 std::promise

我们可以在某个线程中对该类对象进行赋值,然后可以在其它线程中取出其值。

#include <stdio.h>
#include <stdlib.h>

#include <iostream> // std::cout
#include <thread>   // std::thread
#include <mutex>    // std::mutex
#include <future>	// std::future

int thread_task(std::promise<int>& pro, int i)
{
	std::this_thread::sleep_for(std::chrono::milliseconds(1000));
	std::cout << "thread_task" << i << std::endl;
	pro.set_value(i);
	return 0;
}

int main()
{
	std::promise<int> pro;
	std::thread mythread(thread_task, std::ref(pro), 5);
	mythread.join();
	std::future<int> result = pro.get_future();
	std::cout << result.get() << std::endl;
	return 0;
}
  • 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

set_value()只能被调用一次,多次调用会导致程序异常。
get()也只能被调用一次,多次调用会导致程序异常。
std::share_future()对象可以多次被get()。

#include <stdio.h>
#include <stdlib.h>

#include <iostream> // std::cout
#include <thread>   // std::thread
#include <mutex>    // std::mutex
#include <future>	// std::future

int thread_task(std::promise<int>& pro, int i)
{
	std::this_thread::sleep_for(std::chrono::milliseconds(1000));
	std::cout << "thread_task" << i << std::endl;
	pro.set_value(i);
	return 0;
}

int main()
{
	std::promise<int> pro;
	std::thread mythread(thread_task, std::ref(pro), 5);
	mythread.join();
	std::future<int> result = pro.get_future();
	std::future_status status =  result.wait_for(std::chrono::milliseconds(1000));
	if (std::future_status::ready == status)
	{
		//线程已成功返回
	}
	else if (std::future_status::timeout == status)
	{
		//wait_for的时间已结束,线程仍未成功返回
	}
	else if (std::future_status::deferred == status)
	{
		//如果std::async的第一个参数设置为std::launch::deferred
		//则该线程会直到std::future对象调用wait()或get()时才会执行
		//这种情况就会满足
	}
	std::cout << result.get() << std::endl;
	return 0;
}
  • 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

本系列笔记主要参考了一个博客以及一个视频。
博客: C++11 并发指南系列
视频:c++11并发与多线程视频课程

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/我家自动化/article/detail/1014206
推荐阅读
相关标签
  

闽ICP备14008679号