C++并发编程之二 在线程间共享数据

Posted ZHAOCHENHAO-

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++并发编程之二 在线程间共享数据相关的知识,希望对你有一定的参考价值。

文章目录

前情回顾: 在前一篇文章中,我们了解了创建线程、向线程中传递参数、移交线程的归属权,识别线程id等等。那么我们在该篇文章中将要讨论一下线程之间数据共享的问题。

当多线程同时存在访问共享资源的时候,其结果依赖于这些线程的执行顺序,这样的一种状况叫做条件竞争。但如果多线程都只是读取共享资源时,那么条件竞争并不会产生坏的影响。但当条件竞争会因执行顺序不同而结果不同,执行顺序不是程序员所期待的,这就是一种恶性的条件竞争。

恶性条件竞争:是指在多线程编程中,程序的正确性依赖于线程的执行顺序,如果线程的执行顺序不是程序员所期望的,那么就会导致程序出现错误。恶性条件竞争通常是由于锁的使用不当、共享变量的访问不同步、多线程访问同一个资源等原因导致的。这种竞争非常难以发现和修复,而且可能会导致程序崩溃或产生不可预测的结果,因此应该尽量避免。

#include <iostream>
#include <thread>

int counter = 0;

void increment_counter()

    counter++;


int main()

    std::thread t1(increment_counter);
    std::thread t2(increment_counter);

    t1.join();
    t2.join();

    std::cout << "Counter = " << counter << std::endl;

    return 0;

该程序创建了两个线程 t1 和 t2,并让它们同时执行 increment_counter 函数,该函数将全局计数器 counter 加 1。如果两个线程同时访问 counter,那么就可能出现条件竞争。在某些情况下,两个线程可能同时读取 counter 的旧值,然后将其加 1,导致最终结果不是预期的 2。

那么我们该如何防止在多线程开发中出现的恶行条件竞争呢?肯定是由办法的!

我们首先提出第一种方法:使用互斥锁(mutex)保护共享数据!

1.1 互斥锁(mutex)保护共享数据

1.1.1 std::mutex 的成员函数 std::mutex::lock() 和std::mutex::unlock() (不推荐使用)

std::mutex 是 C++11 标准库提供的一个互斥锁类,用于保护多个线程共享访问的数据。其定义如下:

class mutex 
public:
    void lock();    // 上锁,如果已经被上锁了则阻塞当前线程
    bool try_lock();    // 尝试上锁,如果已经被上锁了则立即返回 false,否则返回 true 并上锁
    void unlock();  // 解锁,如果没有被上锁则行为未定义
;

写一个成员函数上锁实例:

#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx;
int shared_data = 10;

void func()

    mtx.lock();
    shared_data++;
    std::cout << "shared_data is " << share_data << std::endl;
    mtx.unlock();
    


int main()

    std::thread t(func);
    std::thread t1(func);
    t.join();
    t1.join();
    return 0;

lock() 和 unlock() 方法分别用于上锁和解锁,可以在多个线程之间进行同步。在多个线程访问共享资源的时候,需要先上锁,访问完成之后再解锁,以确保同一时间只有一个线程访问该资源。

当某个线程对共享资源上锁之后,其他线程对同一共享资源的访问就会被阻塞,直到该线程解锁。如果没有及时解锁,则其他线程将一直被阻塞,直到该线程解锁。在 C++11 中,可以使用 std::lock_guard 或 std::unique_lock 等 RAII 封装类来自动管理互斥锁的上锁和解锁,避免手动调用 lock() 和 unlock() 方法导致的代码错误。

1.1.2 使用std::lock_guard保护共享数据

std::lock_guard 是 C++ 标准库中提供的一个互斥锁管理类,它是一个模板类,需要传入互斥锁对象作为模板参数。它使用了 RAII(资源获取即初始化)技术,能够自动的进行加锁和解锁,从而防止忘记解锁导致的死锁问题。
当 std::lock_guard 对象被创建时,它会尝试对互斥锁进行加锁,当该对象被销毁时,它会自动调用 std::mutex 对象的 unlock 方法来解锁。
使用 std::lock_guard 可以有效地简化代码,避免手动进行加锁和解锁操作,从而减少出错的可能性。
以下是使用 std::lock_guard 进行互斥锁保护的一个示例代码:

#include <iostream>
#include <thread>
#include <mutex>

std::mutex mtx;

void print(std::string msg)

    std::lock_guard<std::mutex> lock(mtx);
    std::cout << msg << std::endl;


int main()

    std::thread t1(print, "hello");
    std::thread t2(print, "world");

    t1.join();
    t2.join();

    return 0;

在上述示例中,我们使用 std::lock_guard 对象 lock 来保护了共享资源 std::cout 的访问。每当线程调用 print 函数时,就会自动加锁 mtx,并输出相应的信息。在 print 函数执行完毕后,std::lock_guard 对象 lock 会自动被销毁,从而释放 mtx 的所有权并解锁它。

1.1.3 使用std::unique_lock保护共享数据

std::unique_lock 是 C++11 标准中提供的一种锁类型,它提供了比 std::lock_guard 更为灵活的锁管理方式。与 std::lock_guard 类似,std::unique_lock 也是 RAII(资源获取即初始化)机制的实现方式之一,可以保证在作用域结束时自动释放锁资源。

与 std::lock_guard 不同的是,std::unique_lock 对象在构造时可以选择是否锁定关联的互斥量,同时在对象生命周期内可以多次获得或释放锁,从而提供了更为灵活的锁管理方式。另外,std::unique_lock 对象也提供了更多的锁操作方法,例如延迟锁定、锁定超时等。

下面是一个使用 std::unique_lock 的示例:

#include <iostream>
#include <thread>
#include <mutex>

std::mutex mtx;

void func()

    std::unique_lock<std::mutex> lck(mtx);
    std::cout << "Thread " << std::this_thread::get_id() << " is executing..." << std::endl;
    // 在作用域内,unique_lock 对象已经自动加锁了,可以安全地访问共享资源


int main()

    std::thread t1(func);
    std::thread t2(func);
    t1.join();
    t2.join();
    return 0;

在上面的示例中,std::unique_lock 对象 lck 的构造函数中传入了互斥量 mtx,在作用域内 lck 对象已经自动加锁了。可以通过 std::unique_lock 对象的 unlock() 方法手动释放锁,或者通过 std::unique_lock 对象的 lock() 方法再次加锁。

  1. 关于std::unique_lock的延迟锁定
    std::unique_lock可以指定锁的defer_lock属性,这意味着构造函数并不会立即锁住锁,而是在后面需要锁住锁的地方进行锁定。这对于某些情况非常有用,例如在多个条件变量上等待的线程需要获得同一个锁的所有权,但是直到等待之前才能获得锁。通过使用std::unique_lock的defer_lock属性,可以确保线程不会在没有准备好的情况下持有锁。
#include <iostream>
#include <thread>
#include <mutex>

int shared_data = 10;

void func(std::mutex& m)

    std::unique_lock<std::mutex> lock(m, std::defer_lock);
    lock.lock(); // 明确地调用lock获取锁
    std::cout << "Thread " << std::this_thread::get_id() << " is doing some work" << std::endl;
    // 在这里进行一些需要互斥访问的工作
    shared_data++;
    std::cout << "shared_data is " << shared_data << std::endl;
    std::cout << "Thread " << std::this_thread::get_id() << " has released the lock" << std::endl;
    lock.unlock();


int main()

    std::mutex m;
    std::thread t1(func, std::ref(m));
    std::thread t2(func, std::ref(m));
    t1.join();
    t2.join();
    return 0;

  1. 关于锁定超时
    std::unique_lock支持超时,可以指定一个时间段,在这段时间内如果无法获得锁则会放弃锁并返回。这对于需要限制锁的持有时间的情况非常有用,可以防止死锁和资源争用的问题。

std::condition_variable::wait_for() 是 C++11中提供的等待函数,用于等待一个条件变量在一定时间内变为 true。它常常和std::unique_lock 一起使用,可以实现多线程的同步和互斥操作。 它的函数原型如下:

template< class Rep, class Period > 
std::cv_status wait_for( std::unique_lock<std::mutex>& lock, const std::chrono::duration<Rep,Period>& timeout_duration ); 

其中,lock 表示互斥锁,timeout_duration 表示等待的时间。
wait_for() 的返回值有以下几种:
std::cv_status::no_timeout: 表示条件变量在等待时间内被唤醒;
std::cv_status::timeout: 表示等待超时。

下面是一个超时锁定的实例:

#include <iostream>
#include <mutex>
#include <thread>
#include <condition_variable>

std::mutex mtx;
std::condition_variable cv;
bool data_ready = false;

void consumer()

    std::unique_lock<std::mutex> lock(mtx);
    while(!data_ready)
    
        if(cv.wait_for(lock, std::chrono::seconds(1)) == std::cv_status::timeout)
        
            std::cout << "timeout\\n";
            return;
        
    
    std::cout << "data ready\\n";


void producer()

    std::this_thread::sleep_for(std::chrono::seconds(3));
    data_ready = true;
    cv.notify_one();


int main()

    std::thread t1(consumer);
    std::thread t2(producer);
    t1.join();
    t2.join();
    return 0;

上述示例中,consumer() 是消费者线程,它等待 data_ready 变为 true,如果超时则输出 timeout,否则输出 data ready。producer() 是生产者线程,它在3秒后将 data_ready 设为 true,并调用 cv.notify_one() 唤醒消费者线程。

1.2 保护共享数据的其他方式

1.2.1 初始化过程中保护共享数据

比如当我们在多线程开发的初始化过程中用到一些共享数据,但是我们只想让这个初始化过程只运行一次,那么该怎么协调让多个线程对初始化函数只运行一次呢?

在C++11标准中,为了支持一些只需要被执行一次的线程安全操作,引入了std::call_once()函数和std::once_flag()函数。它们通常被用来实现一些全局初始化和单例模式等场景。该函数在<mutex>头文件中。

std::call_once()函数的声明如下:

template <class Callable, class... Args>
void call_once(std::once_flag& flag, Callable&& func, Args&&... args);

其中,std::once_flag是一个标志位类型,用于保证func只会被执行一次。Callable是一个可调用对象类型,可以是函数指针、函数对象、Lambda表达式等,args是func的参数列表。

std::call_once()函数的使用方法比较简单,只需要传入一个std::once_flag对象和一个可调用对象,就可以保证该可调用对象只会被执行一次,例如:

#include <iostream>
#include <thread>

std::once_flag flag;

void initialize()

    std::cout << "initialize" << std::endl;


void thread_func()

    std::call_once(flag, initialize);


int main()

    std::thread t1(thread_func);
    std::thread t2(thread_func);
    t1.join();
    t2.join();
    return 0;

上面的代码中,我们在initialize()函数中输出一条消息,然后在thread_func()函数中调用std::call_once()函数保证initialize()函数只会被执行一次。最后,在main()函数中创建了两个线程,并分别执行thread_func()函数。

由于std::call_once()函数的存在,我们可以放心地使用initialize()函数进行全局初始化,而不用担心它会被重复执行。

需要注意的是,std::once_flag对象只能用于一次调用std::call_once()函数。如果需要多次执行一些只需要被执行一次的操作,可以创建多个std::once_flag对象。

1.2.2 保护甚少更新的数据结构

但也有这样一种情况,比如有个数据表,这个数据表里面的数据很少会进行修改,那么如果还使用std::mutex的话,当一个线程进行访问的时候,其他线程需要等待访问线程释放锁了之后才能进行访问。这样未免也太浪费时间了。但不用着急,也有解决的办法!

std::shared_mutex和std::shared_timed_mutex是C++11标准库中<shared_mutex>引入的多线程同步原语,用于实现读写锁。当一个线程需要对某个共享资源进行读取操作时,可以使用std::shared_mutex或std::shared_timed_mutex进行加锁,以允许其他线程也能够同时读取该资源。而当一个线程需要对该共享资源进行写入操作时,则需要进行排它访问,此时必须等待其他所有线程释放读取锁和写入锁,才能获得写入锁进行写入。

std::shared_mutex和std::shared_timed_mutex的主要区别在于前者不支持超时等待,而后者支持。因此,在需要对共享资源进行读写访问的场景中,如果需要实现超时等待的功能,则可以使用std::shared_timed_mutex;否则,可以使用std::shared_mutex。

那什么场合下比较使用使用这种方法呢?

当多个线程需要同时读取共享数据,而只有少数线程需要写入数据时,可以使用std::shared_mutex和std::shared_timed_mutex来提高并发性能。这是因为,读取操作可以并发进行,而写入操作需要互斥执行。

  • 使用场景:
    具体来说,std::shared_mutex和std::shared_timed_mutex的使用场景包括:
  1. 读多写少的情况,例如在多线程的服务器应用程序中,多个线程需要读取某些共享数据,但只有少数线程需要修改这些数据。

  2. 需要对共享数据进行时间限制的情况,例如在超时机制中,需要对共享数据进行读取和写入,并且需要限制读取和写入操作的时间。

需要注意的是,std::shared_mutex和std::shared_timed_mutex适用于读多写少的场景,如果读写操作的比例接近1:1,建议使用std::mutex或std::timed_mutex等其他的同步原语。

  • 使用方法
  1. 通常情况下,std::shared_lock 和 std::shared_mutex 是一起使用的。
  2. std::shared_mutex 是 C++标准库中提供的一种多读单写的互斥量,它允许多个线程同时获取共享锁,但只允许一个线程获取独占锁。因此,std::shared_mutex 适用于多个线程同时读取同一个数据结构,但只有一个线程写入数据结构的场景。
  3. 而 std::shared_lock 则是用于获取共享锁的一种锁类型,它可以让多个线程同时持有共享锁,而不会互相阻塞。当共享锁被多个线程持有时,其它线程只能获取共享锁,而不能获取独占锁。
  4. 因此,通常情况下,当我们需要保护一个数据结构时,可以使用 std::shared_mutex 作为互斥量,并使用 std::shared_lock 来获取共享锁,以实现多线程的读操作。同时,当我们需要写入数据结构时,可以使用 std::unique_lock 来获取独占锁,以保证数据的一致性和正确性。
  5. 综上所述,std::shared_lock 和 std::shared_mutex 的搭配使用可以实现多读单写的场景,提高了程序的并发性能。

使用std::shared_mutex的实例:

#include <iostream>
#include <thread>
#include <vector>
#include <mutex>
#include <shared_mutex>

std::vector<int> vec;
std::shared_mutex smutex;

void writer()

    for(int i = 0; i < 100; ++i)
    
        std::unique_lock<std::shared_mutex> lock(smutex);
        vec.push_back(i);
    


void reader(int id)

    for(int i = 0; i < 10; ++i)
    
        std::shared_lock<std::shared_mutex> lock(smutex);
        std::cout << "Reader " << id << " is reading: ";
        for(int j = 0; j < vec.size(); ++j)
        
            std::cout << vec[j] << " ";
        
        std::cout << std::endl;
    


int main()

    std::thread t1(writer);
    std::vector<std::thread> threads;
    for(int i = 0; i < 5; ++i)
    
        threads.push_back(std::thread(reader, i+1));
    

    t1.join();
    for(int i = 0; i < 5; ++i)
    
        threads[i].join();
    

    return 0;

在上述代码中,我们定义了一个全局的std::vector类型的vec变量,同时定义了一个std::shared_mutex类型的smutex变量,用于对vec变量进行读写保护。在writer函数中,我们对vec变量进行100次写入操作,而在reader函数中,我们对vec变量进行10次读取操作。在reader函数中,我们使用std::shared_lock类型的锁来对vec进行读取保护,而在writer函数中,我们使用std::unique_lock类型的锁来对vec进行写入保护。

在 reader() 函数中,我们使用 std::shared_lock 来获取共享锁,而不使用 std::unique_lock来获取独占锁,是因为共享锁可以被多个线程同时获取,而独占锁只能被一个线程获取。

在这个示例中,我们希望多个线程可以同时读取变量 data 的值,因此我们需要使用共享锁来保护这个变量。如果我们使用独占锁来保护变量data,那么只有一个线程可以获取锁,其他线程将被阻塞,无法读取变量 data。

使用 std::shared_lock的另一个好处是它可以允许多个线程同时获取锁,从而提高并发性能。当多个线程都只需要读取数据时,使用共享锁可以避免线程之间的竞争,提高程序的效率。

因此,在这个示例中,使用 std::shared_lock 是正确的选择,可以保证多个线程可以同时读取变量 data的值,而不会导致线程之间的竞争和阻塞。

  • std::shared_lock和std::unique_lock的区别:
    std::shared_lock 和 std::unique_lock 是 C++标准库中两种不同类型的锁,它们之间的主要区别在于锁的所有权和线程的并发性。
    1. 锁的所有权
    std::unique_lock 拥有独占锁,它允许一个线程独占锁,并且可以在锁定期间多次释放和获取锁。std::shared_lock 拥有共享锁,它允许多个线程同时获取共享锁,但不能独占锁。在锁定期间,std::shared_lock 和 std::unique_lock 都可以释放和获取锁。
    2. 线程的并发性 std::unique_lock 允许线程独占锁,因此只能有一个线程同时持有 std::unique_lock。而 std::shared_lock 允许多个线程同时获取共享锁,因此可以有多个线程同时持有std::shared_lock。这样可以提高程序的并发性能。
    3. 另外,std::unique_lock 和 std::shared_lock 还有一些其他的区别:
    • 构造函数的参数
      std::unique_lock 的构造函数可以接受一个 std::defer_lock 参数,这意味着它可以在构造时不立即获取锁。而 std::shared_lock 没有这个参数,它必须在构造时获取锁。
    • 释放锁的方式
      std::unique_lock 可以通过调用 unlock() 方法来释放锁,也可以在其作用域结束时自动释放锁。而 std::shared_lock 只能在其作用域结束时自动释放锁。

总的来说,std::unique_lock 适用于需要独占锁的场景,而 std::shared_lock适用于需要共享锁的场景。在使用这两种锁时,需要根据实际需求选择合适的锁类型。

使用std::shared_timed_mutex的实例:

#include <iostream>
#include <queue>
#include <thread>
#include <mutex>
#include <shared_mutex>

std::queue<int> q;
std::shared_timed_mutex mutex;

void read_queue(int id)

    std::shared_lock<std::shared_timed_mutex> lock(mutex, std::chrono::seconds(1));
    if (lock.owns_lock())
    
        while (!q.empty())
        
            int val = q.front();
            q.pop();
            std::cout << "Thread " << id << " read value: " << val << std::endl;
            std::this_thread::sleep_for(std::chrono::milliseconds(10));
        
    
    else
    
        std::cout << "Thread " << id << " failed to read queue." << std::endl;
    


void write_queue()

    std::unique_lock<std::shared_timed_mutex> lock(mutex);
    for (int i = 0; i < 10; ++i)
    
        q.push(i);
        std::cout << "Write value: " << i << std::endl;
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    


int main()

    std::thread t1(read_queue, 1);
    std::thread t2(read_queue, 2);<

C++笔记--线程间共享数据

         当线程在访问共享数据的时候,必须制定一些规矩,用来限定线程可访问的数据位。还有,一个线程更新了共享数据,需要对其他线程进行通知。从易用性的角度,同一进程中的多个线程进行数据共享。错误的共享数据使用是产生并发bug的一个主要原因,当涉及到共享数据时,问题很可能是因为共享数据修改所导致。如果共享数据是只读的,那么只读操作不会影响到数据,更不会涉及对数据的修改,所以所有线程都会获得同样的数据。但是,当一个或多个线程要修改共享数据时,就会产生很多麻烦。这种情况下,就必须小心,才能确保一切所有线程都工作正常。

        线程间潜在问题就是修改共享数据,致使不变量遭到破坏。当不做些事来确保在这个过程中不会有其他线程进行访问的话,可能就有线程访问到刚刚删除一边的节点;这样的话,线程就读取到要删除节点的数据(因为只有一边的连接被修改,所以不变量就被破坏。破坏不变量的后果是多样,当其他线程按从左往右的顺序来访问列表时,它将跳过被删除的节点。在一方面,如有第二个线程尝试删除从右向左的节点,那么可能会让数据结构产生永久性的损坏,使程序崩溃。无论结果如何,都是并行代码常见错误:条件竞争。

  使用互斥量保护共享数据
         当程序中有共享数据,肯定不想让其陷入条件竞争,或是不变量被破坏。那么,将所有访问共享数据结构的代码都标记为互斥岂不是更好?这样任何一个线程在执行这些代码时,其他任何线程试图访问共享数据结构,就必须等到那一段代码执行结束。于是,一个线程就不可能会看到被破坏的不变量,除非它本身就是修改共享数据的线程。
         当访问共享数据前,使用互斥量将相关数据锁住,再当访问结束后,再将数据解锁。线程库需要保证,当一个线程使用特定互斥量锁住共享数据时,其他的线程想要访问锁住的数据, 都必须等到之前那个线程对数据进行解锁后,才能进行访问。这就保证了所有线程能看到共享数据,而不破坏不变量。

 代码1:

int g_i = 0;
std::mutex g_i_mutex;

void safe_increment() 
    std::lock_guard lock(g_i_mutex); //safe_increment结束时自动解锁
    g_i++;

代码2:

#include <list>
#include <mutex>
#include <algorithm>

std::list<int> some_list;    // 1
std::mutex some_mutex;    // 2

void add_to_list(int new_value)

  std::lock_guard<std::mutex> guard(some_mutex);    // 3
  some_list.push_back(new_value);


bool list_contains(int value_to_find)

  std::lock_guard<std::mutex> guard(some_mutex);    // 4
  return std::find(some_list.begin(),some_list.end(),value_to_find) != some_list.end();

分析: 有一个全局变量1,这个全局变量被一个全局的互斥量保护2。add_to_list()3和list_contains()4函数中使用std::lock_guard<std::mutex>,使得这两个函数中对数据的访问是互斥的:list_contains()不可能看到正在被add_to_list()修改的列表。

组织代码来保护共享数据

         使用互斥量来保护数据,并不是仅仅在每一个成员函数中都加入一个std::lock_guard对象那么简单;一个迷失的指针或引用,将会让这种保护形同虚设。不过,检查迷失指针或引用是很容易的,只要没有成员函数通过返回值或者输出参数的形式向其调用者返回指向受保护数据的指针或引用,数据就是安全的。在确保成员函数不会传出指针或引用的同时,检查成员函数是否通过指针或引用的方式来调用也是很重要的(尤其是这个操作不在你的控制下时)。函数可能没在互斥量保护的区域内,存储着指针或者引用,这样就很危险。更危险的是:将保护数据作为一个运行时参数,
如下:

class some_data

  int a;
  std::string b;
public:
  void do_something();
;

class data_wrapper

private:
  some_data data;
  std::mutex m;
public:
  template<typename Function>
  void process_data(Function func)
  
    std::lock_guard<std::mutex> l(m);
    func(data);    // 1 传递“保护”数据给用户函数
  
;

some_data* unprotected;

void malicious_function(some_data& protected_data)

  unprotected=&protected_data;


data_wrapper x;
void foo()

  x.process_data(malicious_function);    // 2 传递一个恶意函数
  unprotected->do_something();    // 3 在无保护的情况下访问保护数据

分析:std::lock_guard对数据做了很好的保护,但调用用户提供的函数func1,就意味着foo能够绕过保护机制将函数malicious_function传递进去2,在没有锁定互斥量的情况下调用do_something()。

死锁:问题描述及解决

      线程有对锁的竞争:一对线程需要对他们所有的互斥量做一些操作,其中每个线程都有一个互斥量,且等待另一个解锁。这样没有线程能工作,因为他们都在等待对方释放互斥量。这种情况就是死锁,它的最大问题就是由两个或两个以上的互斥量来锁定一个操作。

          避免死锁的一般建议,就是让两个互斥量总以相同的顺序上锁:总在互斥量B之前锁住互斥量A,就永远不会死锁。某些情况下是可以这样用,因为不同的互斥量用于不同的地方。不过,事情没那么简单,比如:当有多个互斥量保护同一个类的独立实例时,一个操作对同一个类的两个不同实例进行数据的交换操作,为了保证数据交换操作的正确性,就要避免数据被并发修改,并确保每个实例上的互斥量都能锁住自己要保护的区域。不过,选择一个固定的顺序(例如,实例提供的第一互斥量作为第一个参数,提供的第二个互斥量为第二个参数),可能会适得其反:在参数交换了之后,两个线程试图在相同的两个实例间进行数据交换时,程序又死锁了.

       C++标准库有办法解决这个问题,std::lock——可以一次性锁住多个(两个以上)的互斥量,并且没有副作用(死锁风险)。下面的程序清单中,就来看一下怎么在一个简单的交换操作中使用std::lock。

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(some_big_object const& sd):some_detail(sd)

  friend void swap(X& lhs, X& rhs)
  
    if(&lhs==&rhs)
      return;
    std::lock(lhs.m,rhs.m); // 1
    std::lock_guard<std::mutex> lock_a(lhs.m,std::adopt_lock); // 2
    std::lock_guard<std::mutex> lock_b(rhs.m,std::adopt_lock); // 3
    swap(lhs.some_detail,rhs.some_detail);
  
;

以上是关于C++并发编程之二 在线程间共享数据的主要内容,如果未能解决你的问题,请参考以下文章

Linux C与C++一线开发实践之六 多线程高级编程

并发编程多线程程序同步策略

并发编程

C++笔记--线程间共享数据

C++笔记--线程间共享数据

C++11 并发编程基础:并发并行与C++多线程