《C++并发编程实战》随笔


写在开头:本书中的许多代码过于复杂,使用了C++中较新的特性等。
关于多线程一些名词的概念在链接: link.第十一章

第一章

  多线程的之始:
    包含<thread>头文件.
    std::thread t(A_func);    // 声明一个线程对象,之后这个线程执行函数A_func()

第二章 线程对象的基本操作

  1.概:
    若声明线程对象后,需要显示调用结合或者分离。如果在对象超出作用域析构时未执行这两个操作中其中一个,那么程序会被终止。

    若无明确说明,本章中所有的t都代表一个线程对象。

    PS:一个小问题,若在声明线程对象后,结合或分离之前发生异常,那么异常导致栈解退,线程对象在没有被结合或分离前就被析构,那么导致程序结束。不过可以使用RAII来解决,类似标准库中的智能指针一样,在线程对象超出作用域时析构时调用join()。当然,未处理的异常也会导致程序结束。

  2.结合还是分离:
    结合(同步等待):t.join(),如此调用后,在当前进程等待t所指示的线程执行完毕。
    分离(异步执行):t.detach(),如此调用后,t所指的线程像守护进程一样,被抛到后台执行。

    不管结合还是分离,都只能执行其中一个操作,结合了就不能分离,分离了就不能结合。关于结合操作,最好在结合前执行t.joinable(),来判断线程是否能被结合。

  3.传递参数给线程函数的操作和两个问题:

    类成员函数启动线程:
      class X{ public: void something(int a) };
      X my_x;
      std::thread t2(&X::something, &my_x, 123);

    普通函数启动线程:
      void f(int i, std::string const & s);
      std::thread t1(f, 3, "hello");

    第一个问题:
      当字符串"hello"存储char buffer[10]中时,调用std::thread t1(f, 3, buffer);会导致使用buffer的数据构造一个临时string对象交给线程引用,但是线程可能并没有及时启动,buffer也只是缓存中等待被构成string,若buffer是一个局部数组,那么很有可能发生函数退出,buffer被释放,线程又没有及时启动,在缓存中持有的buffer指针成为了野指针,此刻线程被启动,野指针buffer构造string对象,将导致未定义行为。
      解决方法:调用thread之前,函数的参数总是先转型为合适的类型,再声明线程对象。如:std::thread t1(f, 3, std::string(buffer));,先生成string的临时对象,就不会导致持有buffer的空指针。

    第二个问题:
      虽然函数f(int, std::string const&)的第二参数是一个引用,但是thread的行为是:总是对传递过来的参数生成副本,供线程操作,不会影响到原值。
      解决方法:std::ref();。std::thread t1( f, 3, std::ref(某个string对象) );

  4.转移线程的所有权(句柄传递):
    线程对象需要作为参数传递,线程对象被返回的情况。
    当一个线程对象将要被被返回时,它是一个右值,会自动转让。
    当线程对象作为参数传递,因为线程对象不能被复制,所以需要转让,f(std::move(t));。这里使用了移动语义来传递对象。

  5.线程数量的选择
    通过函数std::thread::hardware_currency(),返回硬件所支持的线程数量。

  6.线程的ID
    线程的ID类型为:std::thread::id
    通过t.get_id()取得。或者通过std::this_thread::get_id()取得当前线程的线程ID。

第三章 线程间共享数据的操作(互斥元与锁)

  1.共享数据的操作通过两种大类的对象配合操作。
    一种是互斥元,std:mutex、shared_mutex、recursive_mutex等等等。
    一种是锁,std::lock_guard、shared_lock、std::unique_lock等等等。

    互斥元和数据之间 没有直接关系,只是在使用时,应该明白某个互斥元和某组数据是相互对应的。
    锁和互斥元之间是 使用关系,只是锁的行为不同,如共享锁或者独占锁,他们可以锁定同一个互斥元,只是行为上是不同的。

  2.自解锁
    普通的std::lock锁,需要调用lock上锁,之后又需要通过unlock解锁。这就产生了类似第二章中描述的问题:lock之后,unlock之前发生异常,永远没办法解锁,从而产生死锁。
    由此产生了诸如本章第一小节中如std::lock_guard的自解锁,这些自解锁的行为同样类似于智能指针,声明局部锁对象,在超出作用域时,局部对象被析构,析构函数执行解锁操作。
    但自解锁方便的同时,也可能增加了锁的粒度,除非像书中的那样,上锁之后,随后就是return语句。return语句则跟随一些函数调用

  3.不要传出对受保护数据的引用或指针
    许多地方采用类的形式将互斥元和数据组织在一起。
    但是如果某个成员函数返回了数据的指针。那么数据就暴露在了不安全的环境下。当数据指针被传出,成员函数还在通过加锁解锁来进行操作时,外部线程可能已经通过被传出的数据指针已经修改了数据。

  4.一些使用锁的例子及说明
    lock_guard:
    以下这段代码的行为是:首先调用lock函数对两个互斥元同时上锁,如果其中一个无法上锁,那么另外一个上锁的会被暂时解锁,然后阻塞等待直到上锁。接下来声明两个lock_guard对象,通过构造函数的第二参数std::adopt_lock指明互斥元已被锁定,不再进行锁定,沿用互斥元上已有的所有权,最终超出作用,对象被析构,自动解锁。

{
	std::mutex m1,m2;
	std::lock(m1,m2);
	std::lock_guard<std::mutex> lock_a(m1, std::adopt_lock);
	std::lock_guard<std::mutex> lock_b(m2, std::adopt_lock);
	dosomething();
}

    
    std::unique_lock:
    这里主要说明另一个参数std::defer_lock,指定该参数后的行为是,声明锁对象时,传递的互斥元不会被加锁,之后可以随时加锁,在超出作用域时自动解锁。
    转移锁对互斥元的所有权(锁不能被复制):如返回一个锁,或者以某个锁作为参数。如果转移的操作取决于unique_lock对象是左值还是右值。①当一个unique_lock对象被返回时,作为一个右值,其所有权自动被转移。②当作为参数传递时,是一个左值,使用std::move();来对所有权进行转移。

{
	std::mutex m1,m2;
	std::unique_lock<std::mutex> lock_a(m1, std::defer_lock);
	std::unique_lock<std::mutex> lock_a(m2, std::defer_lock);
	std::lock(lock_a,lock_a);
	dosomething();
}

    
    shared_lock:
    c++标准库好像没有读写锁,使用share_lock、shared_mutex 和 std::lock_guard配合来实现读写锁。
    在read函数中,共享锁和共享的互斥元配合,可以在多个线程中同时锁定,因为是读操作,没有竞争条件。
    在write中,使用独占锁和共享的互斥元配合,独占互斥元,阻塞共享锁的锁定。

std::shared_mutex m;
void read();
{
	// 书中代码上boost::shared_lock,但实际上已经在std中加入了
	std::shared_lock<std::shared_mutex> read_lock(m);
	READ_dosomething();
}
void write()
{
	std::lock_guard<std::shared_mutex> write_lock(m);
	WRITE_dosomething();
}

    
    还有一个递归锁,书中并没有例子。

                                      2020年8月11日15:58:23  1,2,3章

第四章 同步

  1.使用条件变量实现同步
    标准库提供了两个条件变量的实现:std::condition_variablestd::condition_variable_any,两者区别在于前者只能和普通的std::mutex一起工作,后者则没有限制。
    以下代码展示了如何使用条件变量实现线程间的同步:
      在读线程中:首先声明一个锁并对互斥元自动上锁,接下来条件变量调用wait等待,首先检查第二参数的条件,条件成立则继续运行,条件不成立则暂时对第一参数的lock解锁,等待来自写线程的通知,收到通知后,取出数据执行操作。使用unique_lock的原因是lock_guard不支持。
      在写线程中:对互斥元上锁,然后插入数据,发出通知。

std::mutex mut;
std::queue<data> dataQueue;
std::condition_variable dataCond;
void readThread()
{
 	while(...)
 	{
  		std::unique_lock<std::mutex> lk(mut);
  		dataCond.wait(lk, []{return !dataQueue.empty();});
  		...//取出数据
  		lk.unlock();
  		...
 	}
}
void writeThread()
{
 	while(...)
 		{
  		...
  		std::lock_guard<std::mutex> lk(mut);
  		dataQueue.push(data);
  		dataCond.notify_one();
 	}
}

  2.future 一次性事件
    future类似一个指针,指向用某个函数启动的异步任务,通过使用future的成员函数get()来获取线程结果。
    标准库提供了两种future:std::futurestd::shared_future。区别是前者只能get()一次,一次之后所得结果结果就不确定了,而后者大概是引用计数,多个不同的shared_future对象都可以调用get()。
    前者可通过std::move移动给后者。前者是不可复制的,后者则是可复制的。
    注:只要调用get(),线程就会阻塞到得到结果。
    使用std::async()来启动一个异步任务,并返回一个future对象。
    若作为std::async()一部分的函数调用引发了异常,该异常被存储正在future中,代替所存储的值,在调用get()时再次引发异常。

std::future<int> answer = std::async(someThingFunc, par1); someThingFunc是异步任务的函数
...        若函数有参数,则传递方式和thread的传递方式一样。
int res = answer.get();

↓↓↓特殊参数的例子,
std::launch::async表示在新线程中运行,
std::launch::deferred表示在调用wait()get()时运行。
auto f1 = std::async(std::launch::async, someThingFunc, par1);
auto f2 = std::async(std::launch::deferred, someThingFunc, par1);

  3.时间限制的等待
    条件变量、锁、线程等这些对象,指定等待一段时间,超出等待时间时就返回。
    关于哪些对象有这些时间限制等待的函数,本章最后有一张表。
    通过后缀_for()的函数等待一段指定的时间,比如说几秒或者几分。
    通过后缀_until()的函数,等待一个绝对的时间,如等待到某年某月某日某时某分某秒。
    关于等待时间的使用,通过标准库的时钟、时间段、时间点三个概念来配合。(翻了翻C++标准库找这三个概念,突然就不想写了,几乎都能找到)。
    时钟:标准库提供了三个clock对象:system_clock、steady_clock、high_resolution_clock。通过调用clock::now()来取得当前对象。
    时间段:用来表示几分几秒,甚至微妙纳秒的表示,就是一段时间。
    时间点:一个绝对的时间,具体到某年的某分某秒。
关于这三个概念如何使用就不写了,网上到处都是,上个例子结束本章:

auto timeout = std::chrono::steady_clock::now() + std::chrono::millisecond(500); // 获取当前时间然后加上500毫秒,作为绝对超时时间
std::unique_lock<std::mutex> lk(m);
std::condition_variable cv;
if( cv.wait_unitl(lk, timeout) == std::cv_status::timeout )
	printf("timeout");

在这里插入图片描述
                                      2020年8月14日18:04:31  4章

结尾

来写个结束把,这本书之后几章大概讲了原子操作和内存顺序,内存顺序大概是更底层的另类的锁,书中用它来实现了无锁的线程安全结构,再往后就是讲一些线程使用的场景之类的。很早之前看过这本书一遍,后面也是看的迷迷糊糊的,现在也是,不过前四章对于多线程编程来说足够了。

                                      2020年8月20日22:07:28

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 《并发编程实战》是一本经典的并发编程书籍,其中包含了丰富的代码示例。这本书的代码示例非常有实战意义,可以帮助开发者在处理并发编程中的各种问题时提供参考。其中的代码示例主要涉及线程池、CAS、原子操作、锁、并发容器、BlockingQueue、CyclicBarrier和Semaphore等相关知识点。 本书的代码示例分布在各个章节中,开发者可以根据需要选择不同的示例进行学习和实践。例如,在线程池相关章节,作者提供了诸如ThreadPoolExecutor、ExecutorCompletionService等类的实现,并且提供了基于可扩展的ThreadPoolExecutor来实现动态调节线程池大小的代码示例。这些示例可以帮助开发者深入了解线程池的实现方式,以及如何进行线程池的调优。 在锁相关章节,作者提供了诸如ReentrantLock和读写锁ReentrantReadWriteLock等类的实现,并且提供了一些实际应用场景下的代码示例,例如票务系统和登录系统。这些示例可以帮助开发者了解锁的原理及其使用方法。 本书同时也介绍了一些常用的并发容器,例如ConcurrentHashMap、ConcurrentLinkedQueue等,在使用这些容器时需要注意线程安全的问题。作者为这些容器提供了详细的使用方法和代码示例,帮助开发者了解如何高效地使用这些容器。总之,《并发编程实战》的代码示例非常有价值,具有一定参考和借鉴意义,可以帮助开发者更好地掌握并发编程知识。 ### 回答2: 《Java并发编程实战》一书的源码是该书的大部分内容的实现代码。这些代码的使用可以帮助读者更好地理解并发编程的实现方式,同时也可以成为读者自己学习并发编程的参考资料。 该书的源码包括一些经典的并发编程实现,例如线程池、锁、原子变量、阻塞队列等。这些实现具有实用性和普遍性,可以帮助读者在自己的开发中解决并发编程问题。同时,该书的源码还包括一些基于实际场景的例子,让读者可以更好地理解并发编程在实际项目开发中的应用。 在使用该书源码时,读者需要关注一些细节问题,例如多线程环境下的原子性、可见性和有序性等。同时,读者还需要学会如何调试和排查多线程程序的问题,以保证程序的正确性和稳定性。 总之,该书的源码是学习并发编程的重要工具之一,读者需要认真学习源码并结合实际项目开发进行练习。只有这样,才能真正掌握并发编程的技巧和应用。 ### 回答3: 《Java并发编程实战》是一本著名的并发编程领域的经典著作,其中的源代码涵盖了Java并发编程的多个方面,非常有学习和参考的价值。 该书中的源代码主要包括了多线程并发、线程池、ThreadLocal、锁、信号量、条件等一系列并发编程相关的实例和案例,涵盖了从最基础的并发操作到应用场景的实践。 通过学习并实践这些源代码,我们可以更好地理解并发编程的思路和原理,掌握并发编程的技能和方法,提高代码质量和性能。同时,还可以培养我们的编码思维和能力,为我们今后的编程工作和研究打下坚实的基础。 总之,《Java并发编程实战》的源代码是具有非常实用和价值的,并发编程相关领域学习者和从业者都可以将其作为一个良好的学习和实践资源,不断探索和尝试。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值