《C++并发编程实战》阅读笔记

文章目录

    • 第 2 章 管理线程
        • 2.2 传递参数给线程函数
        • 2.3 转移线程的所有权
        • 2.4在运行时选择线程数量
        • 2.5 标识线程
    • 第 3 章 在线程间共享数据、
        • 3.2.4 死锁:问题和解决方案
        • 3.2.6用```std::unique_lock```灵活锁定
        • 3.3 用于共享数据保护的替代工具
            • 3.3.1 在初始化时保护共享数据
            • 3.3.2 保护很少更新的数据结构
            • 3.3.3 递归锁
    • 第 4 章 同步并发操作
            • 4.1.1 用条件变量等待条件
        • 4.2 使用```future```等待一次性事件
            • 4.2.1 从后台任务中返回值
            • 4.2.2 将任务与```future```相关联
            • 4.2.3 生成```std::promise```
            • 4.2.4 位```future```保存异常
            • 4.2.5 等待自多个线程
        • 4.3 有时间限制的等待
            • 4.3.1 时钟
            • 4.3.2 时间段
            • 4.3.3 时间点
            • 4.3.4 接受超时的函数

第 2 章 管理线程

2.2 传递参数给线程函数

#include
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
using namespace  std;


struct Status
{
	int value = 1;
};

void DisplayStatus(const Status&status)
{
	cout << "status is " << status.value << endl;
}

void UpdateStatus(Status &status)
{
	status.value++;
	DisplayStatus(status);
}


class X
{
public:
	void do_something(int &n)
	{
		cout << "do something" << endl;
	}
};


int main()
{
	Status status;
	std::thread t(UpdateStatus, status);//这里的status并没有传入引用,而是传入了一份拷贝
	t.join();
	DisplayStatus(status);//线程执行完之后,status并没有被改变

	std::thread t2(UpdateStatus, ref(status));//这里传递了引用
	t2.join();
	DisplayStatus(status);//status在线程中被改变


	X x;
	int n = 5;
	//thread t3(&X::do_something, &x, n);这样是错误的
	thread t3(&X::do_something, &x,ref(n));//传递一个成员函数的指针作为函数
	t3.join();
	return 0;
}

2.3 转移线程的所有权

  std::thread是可移动的,而非可复制的。所以转移所有权要以移动操作来进行。
  explicit可以防止构造函数隐式自动转换

void some_function()
{

}
void some_other_function()
{

}


int main()
{
	std::thread t1(some_function);
	std::thread t2 = std::move(t1);	//t1的控制权移动到了t2
	t1 = std::thread(some_other_function);//从临时对象中进行移动是自动的和隐式的,临时对象的控制权移动到t1
	std::thread t3;		//没有关联的线程
	t3 = std::move(t2);	//t2的控制权移动到了t3
	t1 = std::move(t3);//会导致程序终止,因为t1已经关联了线程

	return 0;
}

2.4在运行时选择线程数量

#include
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
using namespace  std;


template<typename Iterator, typename T>
struct accumulate_block
{
	void operator()(Iterator first, Iterator last, T&result)
	{
		result = std::accumulate(first, last,result);
	}
};

template<typename Iterator,typename T>
T parallel_accumulate(Iterator first, Iterator last, T init)
{
	unsigned long const length = std::distance(first, last);//计算两个迭代器之间的距离
	if (!length)
	{
		return init;
	}

	unsigned long const min_per_thread = 25;
	unsigned long const max_threads = (length + min_per_thread - 1) / min_per_thread;
	/*
	给定程序执行时能够真正并发运行的线程数量的指示,在多核系统上它可能是CPU核心的数量
	*/
	unsigned long const hardware_threads = std::thread::hardware_concurrency();			
	unsigned long const num_threads = std::min(hardware_threads != 0 ? hardware_threads : 2, max_threads);
	unsigned long const block_size = length / num_threads;
	std::vector<T> results(num_threads);
	std::vector<std::thread> threads(num_threads - 1);
	Iterator block_start = first;
	for (unsigned long i = 0; i < (num_threads - 1); ++i)
	{
		Iterator block_end = block_start;
		std::advance(block_end, block_size);	//将迭代器移动一定的距离
		threads[i] = std::thread(accumulate_block<Iterator,T>(),block_start, block_end, ref(results[i]));
		block_start = block_end;
	}
	accumulate_block<Iterator,T>()(block_start, last, results[num_threads - 1]);
	std::for_each(threads.begin(), threads.end(), std::mem_fn(&std::thread::join));
	return std::accumulate(results.begin(), results.end(), init);
}


int main()
{
	std::vector<int> v;
	for (int i = 1; i <= 99; i++)
	{
		v.push_back(i);
	}
	int result = parallel_accumulate<decltype(v.begin()),int>(v.begin(), v.end(),0);
	cout << result << endl;
	return 0;
}

其他知识点:关于std::mem_fn

class ClassA
{
public:
	void do_something(int n)
	{
	
	}
};

int main()
{
	ClassA a;
	auto memeber_fun = std::mem_fn(&ClassA::do_something);
	
	memeber_fun(a,1);
	return 0;
}

2.5 标识线程

  • 线程标识符可以通过与之相关的std::thread对象中通过调用get_id()成员函数来获得。
  • 当前线程的标识符可以通过调用std::this_thread::get_id()获得

第 3 章 在线程间共享数据、

竞争条件一般是时间敏感的,他们常常在应用程序运行于调试工具下时完全消失,因为调试工具会影响程序的时间,即使是轻微的。

3.2.4 死锁:问题和解决方案

死锁的四个条件

  • 互斥
  • 不可抢占
  • 占有且等待
  • 循环等待

解决死锁的方案

  • 一次性获取所有锁
  • 避免嵌套锁 如果已经持有一个锁,就不该再获取锁
  • 以固定的顺序获取锁
  • 占有锁时避免调用用户代码,因为用户代码中可能会申请锁

使用std::lock同时锁定多个锁
std::adopt_lock告知std::lock_guard对象该互斥元已被锁定,并且他们只应沿用互斥元上已有锁的所有权,而不是试图在构造函数中锁定互斥元

class some_big_object {};
void swap(some_big_object &lhs, some_big_object &rhs);

class X
{
private:
	some_big_object some_detail;
	std::mutex m;

public:
	X(const some_big_object&sd ):some_detail(sd){}

	friend void swap(X &lhs, X&rhs)
	{
		if (&lhs == &rhs)
		{
			return;
		}

		std::lock(lhs.m, rhs.m);		//同时锁定多个锁
		std::lock_guard<std::mutex> lock_a(lhs.m, std::adopt_lock);
		std::lock_guard<std::mutex> lock_b(rhs.m, std::adopt_lock);
		//std::adopt_lock告知std::lock_guard对象该互斥元已被锁定,并且他们只应沿用互斥元上已有锁的所有权,而不是试图在构造函数中锁定互斥元
		swap(lhs.some_detail, rhs.some_detail);
	}
};

3.2.6用std::unique_lock灵活锁定

  • std::unique_lock占用更多的空间并且使用起来比std::lock_guard略慢,但是更加灵活
  • std::unique_lock提供了lock()try_lock()unlock()三个成员函数。所以可以随时解锁,而不必非要等到析构。
class some_big_object {};
void swap(some_big_object &lhs, some_big_object &rhs);

class X
{
private:
	some_big_object some_detail;
	std::mutex m;

public:
	X(const some_big_object&sd ):some_detail(sd){}

	friend void swap(X &lhs, X&rhs)
	{
		if (&lhs == &rhs)
		{
			return;
		}

		
		std::unique_lock<std::mutex> lock_a(lhs.m, std::defer_lock);
		std::unique_lock<std::mutex> lock_b(rhs.m, std::defer_lock);
		std::lock(lock_a, lock_b);
		swap(lhs.some_detail, rhs.some_detail);
	}
};

std::unique_lock主要用于以下三种场景

  • 延迟锁定,如上文
  • 需要转移锁的所有权
  • 允许实例在备销毁之前撤回他们的锁,即随时可unlock

3.3 用于共享数据保护的替代工具

3.3.1 在初始化时保护共享数据
class some_resource
{
public:
	void do_something(){}
};
std::shared_ptr<some_resource> resource_ptr;
std::mutex resource_mutex;

//安全,但是序列化问题足够大
void foo()
{
	std::unique_lock<std::mutex> lock(resource_mutex);
	if (!resource_ptr)
	{
		resource_ptr.reset(new some_resource);
	}
	lock.unlock();
	resource_ptr->do_something();
}


//臭名昭著的二次检查锁定,会有问题
//就算一个线程看到另一个线程写入指针,它也可能无法看到新创建的some_resource实例,即虽然指针不为空,但是还没有被初始化
void undefined_behaviour_with_double_checked_locking()
{
	if (!resource_ptr)
	{
		std::lock_guard<std::mutex> lock(resource_mutex);
		if (!resource_ptr)
		{
			resource_ptr.reset(new some_resource);
		}
	}
	resource_ptr->do_something();
}

使用std::call_once可以完美解决上述问题

class some_resource
{
public:
	void do_something(){}
};
std::shared_ptr<some_resource> resource_ptr;
std::once_flag resource_flag;

void init_resource()
{
	resource_ptr.reset(new some_resource);
}

void foo()
{
	std::call_once(resource_flag, init_resource);
	resource_ptr->do_something();
}
3.3.2 保护很少更新的数据结构

  比如有些数据很少更新,但是经常会读。这种情况下使用std::mutex就太悲观了,意味着不能同时读了。此时可以使用shared_mutex来实现解决读写问题,类似pthread的读写锁。std::shared_mutex在C++17中才实现,考虑到现在大部分编译器都还不支持,所以实际上都是使用boost::shared_mutex

  • 使用shared_mutex来同步,而不是mutex
  • 使用std::unique_lockstd::lock_guard来独占锁,即写锁
  • 使用boost::shared_lock来共享锁,即读锁。
class dns_entry{};
class dns_cache
{
	std::map<std::string, dns_entry> entries;
	mutable std::shared_mutex entry_mutex;	//mutable突破const的限制,即使再const函数中也可被改变
public:
	dns_entry find_entry(std::string const &domain) const
	{
		std::shared_lock<std::shared_mutex> lock(entry_mutex);
		const auto it = entries.find(domain);
		return(it == entries.end()) ? dns_entry() : it->second;
	}

	void update_or_add_entry(const std::string&domain, const dns_entry&entry)
	{
		std::lock_guard<std::shared_mutex>lock(entry_mutex);
		entries[domain] = entry;
	}
};

3.3.3 递归锁

  在使用std::mutex的情况下,一个线程试图锁定其已经拥有的互斥元是错误的,并且试图这么做导致未定义行为。
  C++提供了std::recursive_mutex,可以在同一个线程中的单个实例上获取多个锁。在互斥元能够被另一个线程锁定前,必须释放所有的锁。
  大多数时间,如果你觉得需要一个递归互斥元,你可能反而需要改变你的设计。

第 4 章 同步并发操作

4.1.1 用条件变量等待条件
struct data_chunk{};
bool more_data_to_prepare() { return true; }
bool is_last_chunk(data_chunk) { return false; }
data_chunk prepare_data() { return data_chunk(); };
void process(data_chunk) {};

std::mutex mut;
std::queue<data_chunk>data_queue;
std::condition_variable data_cond;

void data_preparation_thread()
{
	while (more_data_to_prepare())
	{
		const data_chunk data= prepare_data();
		std::lock_guard<std::mutex> lock(mut);
		data_queue.push(data);
		data_cond.notify_one();		//通知等待中的线程
	}
}

void data_processing_thread()
{
	while (true)
	{
		std::unique_lock<std::mutex> lk(mut);
		data_cond.wait(lk, [] {return !data_queue.empty(); });//获取锁,查条件,条件不满足则释放锁继续等
		data_chunk data = data_queue.front();
		data_queue.pop();
		lk.unlock();
		process(data);
		if(is_last_chunk(data))
			break;
	}
}

4.2 使用future等待一次性事件

4.2.1 从后台任务中返回值

std::async可以传入参数来决定是否启动一个新线程

  • std::launch::async:在新线程中运行
  • std::launch::deferred:在wait()get()中运行
  • std::launch::async|std::launch::deferred(默认):由具体实现来选择
struct X
{
	void foo(int, std::string const &) {};
	std::string bar(const std::string &) { return std::string(); };
};
X x;
auto f1 = std::async(&X::foo, &x, 42, "hello");//调用p->foo(42,"hello"),其中p是&x
auto f2 = std::async(&X::bar, x, "goodbye");	//调用tempx.bar("goodbye"),其中tempx是x的副本,所以不会对x有影响

struct Y
{
	double operator()(double) { return 0; };
};
Y y;
auto f3 = std::async(Y(), 3.141);//调用tempy(3.141),其中tempy是从Y()移动构造的
auto f4 = std::async(std::ref(y), 2.718);//调用y(2.718)

X baz(X&) {};
auto f5 = std::async(baz, std::ref(x));//调用baz(x)
4.2.2 将任务与future相关联
std::mutex m;
std::deque<std::packaged_task<void()>> tasks;

std::condition_variable task_cond;

bool gui_shutdown_message_recived();
void get_and_process_gui_message();

void gui_thread()
{
	while (!gui_shutdown_message_recived())
	{
		get_and_process_gui_message();
		std::unique_lock<std::mutex> lk(m);
		task_cond.wait(lk, [] {return !tasks.empty(); });
		std::packaged_task<void()> task = std::move(tasks.front());
		tasks.pop_front();
		lk.unlock();
		task();
	}
}

std::thread gui_bg_thread(gui_thread);
template<typename Func>
std::function<void> post_task_for_gui_thread(Func f)
{
	std::packaged_task<void()> task(f);
	std::future<void> res = task.get_future();
	std::lock_guard<std::mutex>lk(m);
	tasks.push_back(std::move(task));
	task_cond.notify_one();
	return res;
}
4.2.3 生成std::promise

参考:C++11多线程-异步运行(1)之std::promise

void read(std::future<std::string> *future) {
    // future会一直阻塞,直到有值到来
    std::cout << future->get() << std::endl;
}

int main() {
    // promise 相当于生产者
    std::promise<std::string> promise;
    // future 相当于消费者, 右值构造
    std::future<std::string> future = promise.get_future();
    // 另一线程中通过future来读取promise的值
    std::thread thread(read, &future);
    // 让read等一会儿:)
    std::this_thread::sleep_for(seconds(1));
    // 
    promise.set_value("hello future");
    // 等待线程执行完成
    thread.join();

    return 0;
}
4.2.4 位future保存异常

可以通过some_promise.set_exception()来让future抛出异常

4.2.5 等待自多个线程
  • 对于std::future而言,只有一个线程可以获取值,因为在首次调用get()后,就没有任何可获取的值留下了。
  • 如果每个线程都通过自己的std::shared_future对象来访问该状态,那么就是安全的。

4.3 有时间限制的等待

4.3.1 时钟
  • std::chrono::system_clock是系统时钟,是不匀速(steady)时钟,因为系统时钟可以调整。std::chrono::system_clock::now()返回的是当前时间。
  • std::chrono::steady_clock是匀速时钟
  • std::chrono::high_resolution_clock提供的是所有类库时钟中最小可能的节拍周期(和可能的最高精度),它实际上可能是其他时钟之一的typedef实际上,在vs2015中,该时钟就是steadt_clocktypedef
4.3.2 时间段

  时间段由std::chrono::duration<>来表示,实际上我们常用的milliseconds等都是durationtypedef。在VS2015中有以下定义:

typedef ratio<1, 1000000000000000000LL> atto;
typedef ratio<1, 1000000000000000LL> femto;
typedef ratio<1, 1000000000000LL> pico;

typedef ratio<1, 1000000000> nano;
typedef ratio<1, 1000000> micro;
typedef ratio<1, 1000> milli;
typedef ratio<1, 100> centi;
typedef ratio<1, 10> deci;
typedef ratio<10, 1> deca;
typedef ratio<100, 1> hecto;
typedef ratio<1000, 1> kilo;
typedef ratio<1000000, 1> mega;
typedef ratio<1000000000, 1> giga;

typedef ratio<1000000000000LL, 1> tera;
typedef ratio<1000000000000000LL, 1> peta;
typedef ratio<1000000000000000000LL, 1> exa;
	// duration TYPEDEFS
typedef duration<long long, nano> nanoseconds;
typedef duration<long long, micro> microseconds;
typedef duration<long long, milli> milliseconds;
typedef duration<long long> seconds;
typedef duration<int, ratio<60> > minutes;
typedef duration<int, ratio<3600> > hours;

  基于时间段的等待使用类库内部的匀速时钟来衡量时间,因此35毫秒意味着35毫秒的逝去时间,即便系统时钟在等待期间进行调整。

4.3.3 时间点
  • 一个特定的时间点被称为时钟的纪元*(epoch),典型的纪元包括1970年1月1日00:00,以及运行应用程序的计算机引导启动的瞬间。是不变的。
  • 时钟的时间点是通过std::chrono::time_point<>类模板的实例来表示的,第一个模板参数指定其参考的时钟,第二个模板参数指定计量单位。

计算两个时间点之间长度的时间段

auto start = std::chrono::high_resolution_clock::now();
do_sometiong();
auto stop = std::chrono::high_resolution_clock::now();
std::cout << "do_something() took " << std::chrono::duration<double, std::chrono::seconds>(stop - start).count() << " seconds" << std::endl;

等待一个具有超时的条件变量

bool wait_loop()
{
	auto const timeout = std::chrono::steady_clock::now() + std::chrono::milliseconds(500);//匀速时钟,不可变
	std::unique_lock<std::mutex> lk(m);
	while (!done)
	{
		if(cv.wait_until(lk,timeout)==std::cv_status::timeout)
			break;
	}
	return true;
}


bool wait_loop2()
{

	
	auto timeout = std::chrono::duration<long long, std::ratio<1,1000>>(20);
	std::unique_lock<std::mutex> lk(m);
	while (!done)
	{
		if(cv.wait_for(lk,timeout)==std::cv_status::timeout)
			break;

		//等价于:
		if (cv.wait_for(lk, std::chrono::microseconds(20))==std::cv_status::timeout)
			break;
	}
	return true;
}

4.3.4 接受超时的函数
类/命名空间 函数 返回值
std::this_thread命名空间 sleep_for(duration)
sleep_until(time_point)
不可用
std::condition_variable
std::condition_variable_any
wait_for(lock,duration)
wait_until(lock,time_point)
std::cv_status::timeout
std::cv_status::no_timeout
wait_for(lock,duration,predicate)
wait_until(lock,time_point,predicate)
bool——当唤醒时predicate的返回值
std::timed_mutexstd::recursive_timed_mutex try_lock_for(duration)
try_lock_until(time_point)
bool—true如果获得了锁,否则false
std::unique_lock

你可能感兴趣的:(C++)