zoukankan      html  css  js  c++  java
  • c++11简单的线程

    线程的管理

    启动线程

    为了让编译器识别 std::thread 类,这个简单的例子也要包含 <thread> 头文件。

    如同大多数C++标准库一样

    线程在std::thread对象创建(为线程指定任务)启动

    无参任务

    最简单的任务,通常是无参数无返回(void-returning)的函数,这种函数在其所属线程上运行,直到函数执行完毕,线程也就结束了。

    例如:

    #include<iostream>

    #include<thread>

     

    using namespace std;

     

    void go()

    {

        cout << "Welcome to Thread!";

    }

    void main()

    {

        thread t1(go);

        cin.get();

    }

    运行结果

    有参任务

    std::thread 可以用可调用(callable)类型构造,将带有函数调用符类型

    的实例传入 std::thread 类中,替换默认的构造函数。

    #include <iostream>

    #include <thread>

    #include <string>

    using namespace std;

     

     

    void run(int num)

    {

        cout << "线程" << num << endl;

    }

     

    void main()

    {

        thread p(run,1);

     

        cin.get();

    }

    执行结果!

    等待线程

    启动了线程,你需要明确是要等待线程结束(加入式joined),还是让其自主运行(分

    离式——detached)。如果 std::thread 对象销毁之前还没有做出决定,程序就会终止

    ( std::thread 的析构函数会调用 std::terminate() )。因此,即便是有异常存在,也需要确保线程能够正确的加入(joined)或分离(detached)

    例如

    #include <iostream>

    #include <thread>

    #include <string>

    #include <chrono>       //c++时间库

     

    using namespace std;

     

    void run(int num)

    {

        chrono::seconds(3);    //c++标准库休眠3秒钟

        std::cout << "线程" << num << endl;

    }

     

     

     

    void main()

    {

        thread t(run, 1);

        t.join();             //阻塞主函数等待等待线程结束

        cin.get();

    }

    joinable()查看当前线程是否被join  true没有flase成功

    #include <iostream>

    #include <thread>

    #include <string>

    #include <chrono>       //c++时间库

     

    using namespace std;

     

    void run(int num)

    {

        chrono::seconds(3);    //c++标准库休眠

        std::cout << "线程" << num << endl;

    }

     

     

     

    void main()

    {

        thread t(run, 1);

        if(t.joinable())

        t.join();

       

        cin.get();

    }

    分离线程

    使用detach()会让线程在后台运行,这就意味着主线程不能与之产生直接交互。也就是说,不会等待这个线程结束;

    如果线程分离,那么就不可能有 std::thread 对象能引用它,分离线程

    的确在后台运行,所以分离线程不能被加入。不过C++运行库保证,当线程退出时,相关资源的能够正确回收,后台线程的归属和控制C++运行库都会处理。

     

    例如

    #include <iostream>

    #include <thread>

    #include <string>

    #include <chrono>       //c++时间库

     

    using namespace std;

     

    void run(int num)

    {

        chrono::seconds(3);    //c++标准库休眠

        std::cout << "线程" << num << endl;

    }

     

     

     

    void main()

    {

        thread t(run, 1);

        t.detach(); //脱离当前主线程自由执行  

        cin.get();

    }

    转移线程所有权

    假设要写一个在后台启动线程的函数,想通过新线程返回的所有权去调用这个函数,而不是

    等待线程结束再去调用;或完全与之相反的想法:创建一个线程,并在函数中转移所有

    都必须要等待线程结束。总之,新线程的所有权都需要转移。

    线程的所有可以在 std::thread 实例中移动,下面将展示一个例子。

    例如:

    #include<iostream>

    #include<thread>

    using namespace std;

     

     

    void run1()

    {

            cout << "run1" << endl;

    }

     

    void run2()

    {

            cout << "run2" << endl;  

    }

     

    void main()

    {

        std::thread t1(run1); // 1

        std::thread t2 = std::move(t1); // 2当显式使用 std::move() 创建t2后,t1的所有权就转移给了t2

        t1 = std::thread(run2);

        cin.get();

    }

    std::thread 支持移,就意味着线程的所有权可以在函数外进行转移,就如下面程序一样。

    #include<iostream>

    #include<thread>

    using namespace std;

    std::thread run1()

    {

        void some_function();

        return std::thread(some_function);

    }

     

    void main()

    {

        void some_function();

        thread t1(std::thread(run1));

    }

    运行时决定线程数量

    std::thread::hardware_concurrency()

    这个函数将返回能同时并发在一个程序中的线程数量。

    例如,多核系统中,返回值就可以能是CPU核芯的数量。

    返回值也仅仅是一个提示,当系统信息无法获取时,函数也会返回0。但是,这也无

    法掩盖这个函数对启动线程数量的帮助。

    使用线程组来分割任。

    //如下

    //将100个任务分片,分成4片

    #include <iostream>

    #include <thread>

    #include <vector>

    #include <string>

    #include <iterator>

    #include <numeric>

    #include <algorithm>

    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;

     

        // 计算所需要的最大线程数量,每个线程至少计算25个数据 

        unsigned long const min_per_thread = 25;

        unsigned long const max_threads =

            (length + min_per_thread - 1) / min_per_thread;

     

        // 获取硬件可并发线程数量 

        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, std::ref(results[i]));

            // 为下一个线程准备数据 

            block_start = block_end;

        }

     

        // 当启动了所有的子线程对数据进行计算,本线程就对数据的最后一块进行计算 

        accumulate_block<Iterator, T>()(block_start, last, results[num_threads - 1]);

     

        // 使用fore_each对所有的线程执行join操作,等待它们执行结束 

        std::for_each(threads.begin(), threads.end(),

            std::mem_fn(&std::thread::join));

     

        // 最后对所有的计算结果求和 

        return std::accumulate(results.begin(), results.end(), init);

    }

     

     

     

    int main()

    {

        std::cout << "threads: " << std::thread::hardware_concurrency() << std::endl;

        std::vector<int> vi;

        for (int i = 0; i<100; ++i)

        {

            vi.push_back(1);

        }

        int sum = parallel_accumulate(vi.begin(), vi.end(), 5);

        std::cout << "sum=" << sum << std::endl;

     

        cin.get();

        return 0;

    }

    识别线程

    线程标识类型是 std::thread::id ,可以通过两种方式进行检索。

    第一种,可以通过调用 std::thread 对象的成员函数 get_id() 来直接获取。

    如果 std::thread 对象没有与任何执行线程相关联, get_id() 将返回 std::thread::type 默认构造值,这个值表示“没有线程”。

    二种,当前线程中调用 std::this_thread::get_id() (这个函数定义在 <thread> 头文件中)也可

    以获得线程标识

    std::thread::id 实例常用作检测线程是否需要进行一些操作,比如:当用线程来分割一项工

    主线程可能要做一些与其他线程不同的工作。这种情况下,启动其他线程

    前,它可以将自己的线程ID通过 std::this_thread::get_id() 得到,并进行存储。

    就是算法核心部分(所有线程都一样的),每个线程都要检查一下,其拥有的线程ID是否与初始线程的ID相同。

     

    std::thread::id master_thread;

    void some_core_part_of_algorithm()

    {

    if(std::this_thread::get_id()==master_thread)

    {

    do_master_thread_work();

    }

    do_common_work();

    }

    总结

    讨论了C++标准库中基本的线程管理方式:启动线程,等待结束和不等待结束(因为需要它们运行在后台)。

    并了解应该如何在线程启动前,向线程函数中传递参数,如何转移线程的

    所有权,如何使用线来分割任务。

    最后使用线程标识来确定数据,以及特殊线程的特殊解决方案

     

     

     

     

     

     

     

     

     

    线程间共享数据

    当线程在访问共享数据的时候,必须定一些规矩,用来限定线程可访问的数据。

    还有,一个线程更新了共享数据,需要对其他线程进行通知。

    从易用性的角度,同一进程中的多个线程进行数据共享,有利有弊。

    错误的共享数据使用是产生并发bug的一个主要原因。

    共享数据带来的问题

    当涉及到共享数据时,问题很可能是因为共享数据修改所导致。

    如果共享数据是只读的,那么操作不会影响到数据,更不会涉及对数据的修改,所以所有线程都会获得同样的数据。

    但是,当一个或多个线修改共享数据时,就会产生很多麻烦。这种情况下,就必须

    小心,才能确保一切所有线程都工作正常

    例如破坏一个链表

    如图:

    条件竞争

    良心竞争:

    争条件的形成,取决于一个以上线程的相对执行顺序,每个线程都抢着完成自己的任务。大多数情况下,即使改变执行顺序,也是良性竞争,其结果可以接受。

    恶心竞争:

    例如,有两个线程同时向一个处理队列中添加任务,因为系统提供的保持不变,所以都不会有什么影响。量遭到破坏,才会产生条件竞争。

    并发中对数据的条件竞争通常表示为“”(problematic)条件竞争,们对问题的良性条件不感兴趣。

    C++标准中也定义了数据竞争(data race)这个术语,一种特殊的条件竞争:并发的

    去修改一个独立对象,数据竞争是(可怕的)定义行为(undefine behavior)的起

    因。

    避免恶性条件竞争

    这里提供一些方法来解决恶性条件竞争,最简单的办法就是对数据结构采用某种机制,确保只有进行修改的线程才能看到不变量被破坏时的中间状态。

    从其他访问线程的角度来看,修改不是已经完成了,就是还没开始。

     

    另一个选择是对数据结构和不变量的设计进行修改,修改完的结构必须能完成一系列不可分

    割的变化,也就是保证每个不变量保持稳定的状,这就是无锁编程

     

    另一种处理条件竞争的方式是,使用事务(transacting)的方式去处理数据结构的更新,这里的"处理"就如同对数据库进行更新一样。

    所需的一些数据和读取都存储在事务日志中,然后将之前的操作合为一步,再进行提交。

    当数据结构被另一个线程修改后,或处理已经重启的情况下,提交就会无法进行,这称作为“软件事务内存”(software transactional memory

    (STM))。理论研究中,这是一个很热门的研究领域。这个概念将不会在本书中再进行介绍,

    因为在C++中没有对STM进行直接支持。

    保护共享数据结构的最基本的方式,是使用C++标准库提供的互斥量(mutex)

     

     

     

     

     

     

    使用互斥量保护共享数据

    当程序中有共享数据,肯定不想让其陷入条件竞争,或是不变量被破坏。

    那么,将所有访问共享数据结构的代码都标记为互斥岂不是更好?这样任何一个线程在执行这些代码时,其他任何线程试图访问共享数据结构,就必须等到那一段代码执行结束。

    于是,一个线程就不可能会看到被破坏的不变量,除非它本身就是修改共享数据的线程。

    当访问共享数据前,使用互斥量将相关数据锁住,再当访问结束后,再将数据解锁。线程库需要保证,当一个线程使用特定互斥量锁住共享数据时,其他的线程想要访问锁住的数据,

    都必须等到之前那个线程对数据进行解锁后,才能进行访问。这就保证了所有线程能看到共享数据,而不破坏不变量。

     

    互斥量是C++中一种最通用的数据保护机制,但它不是“银蛋”;精心组织代码来正确的数据,并在接口内部避免竞争条件是非常重要的。但互斥量自身也有问

    题,也会造成,或是对数据保护的太多(或太少)。

     

    C++中使用互斥量

    C++中通过实例化 srd::mutex 创建互斥量,通过调用成员函数lock()进行上锁,unlock()进行解锁。

     

    不推荐中直接去调用成员函数,因为调用成员函数就意味着,必须记住在每个函数出口都要去unlock(),也包括异常的情况。

     

    C++标准库为互斥量提供了一个RAII法的模板 std::lack_guard ,其会在构造的时候提供的互斥量,并在行解,从而保证了一个已锁的互斥量总是会被正确的解

     

    std::mutex 和 std::lock_guard 都在 <mutex> 头文件中声明。

     

    实践调用成员函数

    //进程的锁定

    #include <iostream>

    #include <thread>

    #include <string>

    #include<windows.h>

    #include<mutex>

     

     

    using namespace std;

     

    //两个线程并行访问一个变量

     

    int g_num = 20;//找到或者找不到的标识

    mutex g_mutex;

    void goA(int num)

    {

        g_mutex.lock();//你访问的变量,在你访问期间,别人访问不了

     

        for (int i = 0; i < 15; i++)

        {

            g_num = 10;

            std::cout << "线程" << num << "   " << g_num << endl;

        }

        g_mutex.unlock();

    }

     

    void goB(int num)

    {

        for (int i = 0; i < 15; i++)

        {

            g_num = 11;

            std::cout << "线程" << num << "   " << g_num << endl;

        }

    }

     

     

    void main()

    {

        thread t1(goA, 1);

        thread t2(goB, 2);

        t1.join();

        t2.join();

        std::cin.get();

    }

     

     

     

     

     

     

     

     

     

     

     

    运行结果

     

     

     

    RAII语法的模板类lack_guard()

    RAII语法实现自动解锁

    //进程的锁定

    #include <iostream>

    #include <thread>

    #include<mutex>

     

     

    using namespace std;

     

    //两个线程并行访问一个变量

     

    int g_num = 20;//找到或者找不到的标识

    mutex g_mutex;

    void goA(int num)

    {

        lock_guard<std::mutex>guard(g_mutex);//自动解锁

        for (int i = 0; i < 15; i++)

        {

            g_num = 10;

            std::cout << "线程" << num << "   " << g_num << endl;

        }

    }

     

    void goB(int num)

    {

     

        for (int i = 0; i < 15; i++)

        {

            g_num = 11;

            std::cout << "线程" << num << "   " << g_num << endl;

        }

     

    }

     

     

    void main()

    {

        thread t1(goA, 1);

        thread t2(goB, 2);

        t1.join();

        t2.join();

        std::cin.get();

    }

    精心组织代码来保护共享数据

    用互斥量来保护数据,并不是仅仅在每一个成员函数中都加入一个 std::lock_guard 对象那么简单。

    一个迷失的指针或引用,将会让这种保护形同虚设。

    函数可能没在互斥量保护的区域内,存储着指针或者引用,这样就很危险。

    更危险的是:将保护数据作为一个运行时参数.

    如同下面:

     

    #include <iostream>

    #include <thread>

    #include<mutex>

     

    class some_data

    {

    public :

        int a;

        std::string b;

    public:

        void do_something()

        {

            std::cout << a;

        }

    };

     

     

    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);

            data.a = 10;

            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 在无保护的情况下访问保护数据

    }

     

    void main()

    {

        foo();

        std::cin.get();

    }

     

     

     

     

    例子中process_data看起来没有任何问题, std::lock_guard 对数据做了很好的保护,但调用

    用户提供的函数func①,就意味着foo能够绕过保护机制将函数 malicious_function 传递进去

    在没有锁定互斥量的情况下调用 do_something() 。

    这段代码的问题在于,它根本没有做到保护:只是将所有可访问的数据结构代码标记为互斥。

     

     

    发现接口内在的条件竞争

    因为使用了互斥量或其他机制保护了共享数据,就不必再为条件竞争所担忧吗?并不是这样,你依旧需要确定特定的数据受到了保护。

     

    回想之前双链表的例子,为了能让线程安全地删除一个节点,需要确保防止对这三个节点(待删除的节点及其前后相邻的节点)的发访问

     

    如果只对指向每个节点的指针进行访问保护,那就和没有使用互斥量一样,条件竞争仍会发生——整个数据结构和整个删除操作需要保护,但指针不需要保护。

     

    这种情况下最简单的解决方案就是使用互斥量来保护整个,尽管对链表的个别操作是安全的,但不意味着你就能走出困境;即使在一个很简单的接口中,依旧可能遇到条件竞争

     

    例如,构建一个类似于 std::stack 结构的栈除了构造函数和swap()以外,需要对 std::stack 提供五个操作:push()一个新元素进栈,pop()一个元素出栈,top()查看栈顶元素,empty()判断栈是否是空栈,size()了解栈中有多少个元素。

     

    即使修改了top(),使其返回一个拷贝而非引用,对内部数据使用一个互斥量进行保护,不过这个接口仍存在条件竞争。

    这个问题不仅存在于基于互斥量实现的接口中,在无锁实现的接口中,条件竞争依旧会产生。

     

    这是接口的问题,与其实现方式无关。

     

    一个给定操作需要两个或两个以上的互斥量时,另一个潜在的问题将出现:死锁(deadlock)。

    与条件竞争完全相反——不同于两个线程会互相等待,从而什么都没做。

     

     

     

    死锁

    但线程有对锁的竞争:一对线程需要对他们所有的互斥量做一些操作,其中每个线程都有一个互斥量,且等待另一个解锁。

     

    这样没有线程能工作,因为他们都在等待对方释放互斥量。这种情况就是死锁,它的最大问题就是由个或个以上的互斥量来一个操作。

     

     

    避免死锁的一般建议,就是让两个互斥量总以相同的顺序上锁:总在互斥量B之前锁住互斥量A,就永远不会死锁。某些情况下是可以这样用,因为不同的互斥量用于不同的地方。

     

    不过,事情没那么简单,比如:当有多个互斥量保护同一个类的独立实例时,一个操作对同一个类的两个不同实例进行数据的交换操作,为了保证数据交换操作的正确性,就要避免数据被并发修改,并确保每个实例上的互斥量都能锁住自己要保护的区域。

     

    不过,选择一个固定的顺序(例如,实例提供的第一互斥量作为第一个参数,提供的第二个互斥量为第二个参数),可能会适得其反:在参数交换了之后,两个线程试图在相同的两个实例间进行数据交换时,程序又死锁了!

     

    std::lock ——可以一次性住多个(个以上)的互斥量,并且没有副作用(死锁风险)

    交换操作中使用 std::lock() 和 std::lock_guard

    #include <iostream>

    #include <thread>

    #include<mutex>

    #include <string>

    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);

        }

    };

     

    ①    锁住两个互斥量,并且两个 std:lock_guard 实例已经创建好②③,还有一个

    互斥量。提供 std::adopt_lock 参数除了表示 std::lock_guard 对象已经上锁外,还表示现成的锁,而非尝试创建新的锁。

     

    这样,就能保证在大多数情况下,函数退出时互斥量能被正确的解锁(保护操作可能会抛出一个异常),也允许使用一个简单的“return”作为返回。还有,需要注意的是,当使用 std::lock 去锁lhs.m或rhs.m时,可能会抛出异常;这种情况下,异常会传播到 std::lock 之外。

     

    当 std::lock 成功的获取一个互斥量上的锁,并且当其尝试从另一个互斥量上再获取锁时,就会有异常抛出,第一个锁也会随着异常的产生而自动释放,所以 std::lock 要么将两个锁都锁住,要不一个都不锁。

     

     

    避免死锁的进阶

    虽然锁是产生死锁的一般原因,但也不排除死锁出现在其他地方。

    无锁的情况下,仅需要每个 std::thread 对象调用join(),两个线程就能产生死锁。这种情况下,没有线程可以继续运行,因为他们正在互相等待。这种情况很常见,一个线程会等待另一个线程,其他线程同时也会等待第一个线程结束,所以三个或更多线程的互相等待也会发生死锁。

     

    为了避免死锁,这里的指导意见为:当机会来临时,不要拱手让人(don’t wait for another thread if there’s achance it’s waiting for you)。

     

    以下提供一些的指导建议,如何识别死锁,并消除其他线程的等待。

     

    避免嵌套锁第一个建议往往是最简单的:

    一个线程已获得一个锁时,再别去获取第二个(don’t acquire alock if you already hold one)。

    如果能坚持这个建议,因为每个线程只持有一个锁,锁上就不会产生死锁。即使互斥锁造成死锁的最常见原因,也可能会在其他方面受到死锁的困扰(比如:线程间的互相等待)。

     

    当你需要获取多个锁,使用一个 std::lock 来做这件事(对获取锁的操作上锁),避免产生死锁。

     

    使用固定顺序获取锁当硬性条件要求你获取两个以上(包括两个)的锁,并且不能使用 std::lock 单独操作来获取它们;那么最好在每个线程上,用固定的顺序获取它们获取它们(锁)。

    获取两个互斥量时,避免死锁的方法:关键是如何在线程之间,以一致性的顺序获取锁。一些情况下,这种方式相对简单。

     

     

    unique_lock——灵活的锁

    unique_lock 介绍

    std::unqiue_lock 通过对不变量的放松(by relaxing the invariants),会比 std:lock_guard 更加灵活;一个 std::unique_lock 实现不会总是拥有与互斥量相关的数据类型。

     

    首先,就像你能将 std::adopt_lock 作为第二个参数传入到构造函数,对互斥所进行管理,你也可以把 std::defer_lock 作为第二个参数传递进去,为了表明互斥量在结构上应该保持解锁状态。

     

    这样,就可以被后面调用lock()函数的 std::unique_lock 对象(不是互斥量)所获取,或传递 std::unique_lock 对象本身到 std::lock() 中。清单3.6可以很容易被改写为清单3.9中的代

     

    码,使用 std::unique_lock 和 std::defer_lock ,而非 std::lock_guard 和 std::adopt_lock 。

     

    代码长度相同,且几乎等价,唯一不同的就是: std::unique_lock 会占用比较多的空间,并且比 std::lock_guard 运行的稍慢一些。保证灵活性是要付出代价的,这个代价就允许 std::unique_lock 实例不携带互斥量:该信息已被存储,且已被更新。

     

     

     

    unique_lock 构造函数

     

    default 构造函数

    新创建的 unique_lock 对象不管理任何 Mutex 对象。

    locking 初始化

    新创建的 unique_lock 对象管理 Mutex 对象 m,并尝试调用 m.lock() 对 Mutex 对象进行上锁,如果此时另外某个 unique_lock 对象已经管理了该 Mutex 对象 m,则当前线程将会被阻塞。

    try-locking 初始化

    新创建的 unique_lock 对象管理 Mutex 对象 m,并尝试调用 m.try_lock() 对 Mutex 对象进行上锁,但如果上锁不成功,并不会阻塞当前线程。

    deferred 初始化

    新创建的 unique_lock 对象管理 Mutex 对象 m,但是在初始化的时候并不锁住 Mutex 对象。 m 应该是一个没有当前线程锁住的 Mutex 对象。

    adopting 初始化

    新创建的 unique_lock 对象管理 Mutex 对象 m, m 应该是一个已经被当前线程锁住的 Mutex 对象。(并且当前新创建的 unique_lock 对象拥有对锁(Lock)的所有权)。

    locking 一段时间(duration)

    新创建的 unique_lock 对象管理 Mutex 对象 m,并试图通过调用 m.try_lock_for(rel_time) 来锁住 Mutex 对象一段时间(rel_time)。

    locking 直到某个时间点(time point)

    新创建的 unique_lock 对象管理 Mutex 对象m,并试图通过调用 m.try_lock_until(abs_time) 来在某个时间点(abs_time)之前锁住 Mutex 对像。

    copy [deleted] 

    unique_lock 对象不能被拷贝构造。

    移动(move)构造

    新创建的 unique_lock 对象获得了由 x 所管理的 Mutex 对象的所有权(包括当前 Mutex 的状态)。调用 move 构造之后, x 对象如同通过默认构造函数所创建的,就不再管理任何 Mutex 对象了。

    unique_lock 的构造函数参考

    #include <iostream>       // std::cout

    #include <thread>         // std::thread

    #include <mutex>          // std::mutex, std::lock, std::unique_lock

     

    using namespace std;

     

     

    // std::adopt_lock, std::defer_lock

    std::mutex foo, bar;

     

    void task_a() {

        std::lock(foo, bar);         // simultaneous lock (prevents deadlock)

        std::unique_lock<std::mutex> lck1(foo, std::adopt_lock);

        std::unique_lock<std::mutex> lck2(bar, std::adopt_lock);

        std::cout << "task a ";

        // (unlocked automatically on destruction of lck1 and lck2)

    }

     

    void task_b() {

        // foo.lock(); bar.lock(); // replaced by:

        std::unique_lock<std::mutex> lck1, lck2;

        lck1 = std::unique_lock<std::mutex>(bar, std::defer_lock);

        lck2 = std::unique_lock<std::mutex>(foo, std::defer_lock);

        std::lock(lck1, lck2);       // simultaneous lock (prevents deadlock)

        std::cout << "task b ";

        // (unlocked automatically on destruction of lck1 and lck2)

    }

     

     

    int main()

    {

        std::thread th1(task_a);

        std::thread th2(task_b);

     

        th1.join();

        th2.join();

     

        cin.get();

     

        return 0;

    }

    unique_lock 移动(move assign)赋值操作

    移动情况是锁的所有权需要从一个域转到另一个

    移动赋值(move assignment)之后,由 A所管理的 Mutex 对象及其状态将会被新的 std::unique_lock 对象取代。

    如果被赋值的对象之前已经获得了它所管理的 Mutex 对象的锁,则在移动赋值(move assignment)之前会调用 unlock 函数释放它所占有的锁

    调用移动赋值(move assignment)之后, A对象如同通过默认构造函数所创建的,也就不再管理任何 Mutex 对象了

    例如

    #include <iostream>       // std::cout

    #include <thread>         // std::thread

    #include <mutex>          // std::mutex, std::unique_lock

    #include<string>

    std::mutex mtx;           // mutex for critical section

     

    void print_fifty(std::string c) {

        std::unique_lock<std::mutex> lck;         // default-constructed

        lck = std::unique_lock<std::mutex>(mtx);  // move-assigned

        std::cout << c;

        std::cout << ' ';

    }

     

    int main()

    {

        std::thread th1(print_fifty, "Move OK !");

        th1.join();

        std::cin.get();

        return 0;

    }

    unique_lock 主要成员函数

    1、  上锁/解锁操作:locktry_locktry_lock_fortry_lock_until  unlock

     

    2、 修改操作:移动赋值(move assignment),交换(swap)与另一个 std::unique_lock 对象交换它们所管理的 Mutex 对象的所有权),释放(release)(返回指向它所管理的 Mutex 对象的指针,并释放所有权)

    3、 获取属性操作:owns_lock(返回当前 std::unique_lock 对象是否获得了锁)、operator bool()(与 owns_lock 功能相同,返回当前 std::unique_lock 对象是否获得了锁)、mutex(返回当前 std::unique_lock 对象所管理的 Mutex 对象的指针)

     

     

     

    std::unique_lock::lock

    上锁操作,调用它所管理的 Mutex 对象的 lock 函数。如果在调用  Mutex 对象的 lock 函数时该 Mutex 对象已被另一线程锁住,则当前线程会被阻塞,直到它获得了锁。

    该函数返回时,当前的 unique_lock 对象便拥有了它所管理的 Mutex 对象的锁。如果上锁操作失败,则抛出 system_error 异常。

    // unique_lock::lock/unlock

    #include <iostream>       // std::cout

    #include <thread>         // std::thread

    #include <mutex>          // std::mutex, std::unique_lock, std::defer_lock

     

    std::mutex mtx;           // mutex for critical section

     

    void print_thread_id(int id) {

        std::unique_lock<std::mutex> lck(mtx, std::defer_lock);

        // critical section (exclusive access to std::cout signaled by locking lck):

        lck.lock();

        std::cout << "thread #" << id << ' ';

        lck.unlock();

    }

     

    int main()

    {

        std::thread threads[10];

        // spawn 10 threads:

        for (int i = 0; i<10; ++i)

            threads[i] = std::thread(print_thread_id, i + 1);

     

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

        std::cin.get();

        return 0;

    }

    std::unique_lock::try_lock

    上锁操作,调用它所管理的 Mutex 对象的 try_lock 函数,如果上锁成功,则返回 true,否则返回 false。

    #include <iostream>       // std::cout

    #include <vector>         // std::vector

    #include <thread>         // std::thread

    #include <mutex>          // std::mutex, std::unique_lock, std::defer_lock

     

    std::mutex mtx;           // mutex for critical section

     

    void print_star() {

        std::unique_lock<std::mutex> lck(mtx, std::defer_lock);

     

     

        // print '*' if successfully locked, '#' otherwise:

        if (lck.try_lock())

            std::cout << '*';

        else

            std::cout << '#';

    }

     

    int main()

    {

        std::vector<std::thread> threads;

        for (int i = 0; i<500; ++i)

            threads.emplace_back(print_star);

     

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

       

        std::cin.get();

        return 0;

    }

     

    std::unique_lock::try_lock_for

    上锁操作,调用它所管理的 Mutex 对象的 try_lock_for 函数,如果上锁成功,则返回 true,否则返回 false。

    #include <iostream>       // std::cout

    #include <chrono>         // std::chrono::milliseconds

    #include <thread>         // std::thread

    #include <mutex>          // std::timed_mutex, std::unique_lock, std::defer_lock

     

    std::timed_mutex mtx;

     

    void fireworks() {

        std::unique_lock<std::timed_mutex> lck(mtx, std::defer_lock);

        // waiting to get a lock: each thread prints "-" every 200ms:

        while (!lck.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 << "* ";

    }

     

    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();

     

        std::cin.get();

        return 0;

    }

     

    std::unique_lock::owns_lock

    返回当前 std::unique_lock 对象是否获得了锁。

    #include <iostream>       // std::cout
    #include <vector>         // std::vector
    #include <thread>         // std::thread
    #include <mutex>          // std::mutex, std::unique_lock, std::try_to_lock
     
    std::mutex mtx;           // mutex for critical section
     
    void print_star () {
      std::unique_lock<std::mutex> lck(mtx,std::try_to_lock);
      // print '*' if successfully locked, 'x' otherwise: 
      if (lck.owns_lock())
        std::cout << '*';
      else                    
        std::cout << 'x';
    }
     
    int main ()
    {
      std::vector<std::thread> threads;
      for (int i=0; i<500; ++i)
        threads.emplace_back(print_star);
     
      for (auto& x: threads) x.join();
     
      return 0;
    }

    同步并发操作

    当你不仅想要保护数据,还想对单独的线程进行同步。例如,在第一个线程完成前,可能需要等待另一个线程执行完成。

    通常情况下,线程会等待一个特定事件的发生,或者等待某一条件达成(为true)。这可能需要定期检查“任务完成”标识,或将类似的东西放到共享数据中,但这与理想情况还是差很多。

    像这种情况就需要在线程中进行同步,C++标准库提供了一些工具可用于同步操作,形式上表现为

    条件变量(condition variables)和期望(futures)。

    等待一个事件或其他条件三种方式

    当一个线程等待一个线程完成任务时,它会有很多选择

     

    一、它可以持续的检查共享数据标志(用于做保护工作的互斥量),直

    到另一线程完成工作时对这个标志进行重设。

    不过,就是一种浪费:线程消耗宝贵的执行时间持续的检查对应标志,并且当互斥量被等待线程上锁后,其他线程就没有办法获取锁,这样线程就会持续等待。因为以上方式对等待线程限制资源,并且在完成时阻碍对标识的设置。

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

    二、个选择是在等待线程在检查间隙,使用 std::this_thread::sleep_for() 进行周期性的间歇

    例如

     

    bool flag;

    std::mutex m;

    void wait_for_flag()

    {

    std::unique_lock<std::mutex> lk(m);

    while(!flag)

    {

    lk.unlock(); // 1 解锁互斥量

    std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 2 休眠100ms

    lk.lock(); // 3 再锁互斥量

    }

    }

     

    这个实现就进步很多,因为当线程休眠时,线程没有浪费执行时间,但是很难确定正确的休

    眠时间。太短的休眠和没有休眠一样,都会浪费执行时间;太长的休眠时间,可能会让任务

    等待线程醒来。休眠时间过长是很少见的情况,因为这会直接影响到程序的行为,当在高节

    奏游戏(fast-paced game)中,它意味着丢帧,或在一个实时应用中超越了一个时间片。

     

    三、选择(也是优先的选择)是,使用C++标准库提供的工具去等待事件的发生。

    通过另一线程触发等待事件的机制是最基本的唤醒方式(例如:流水线上存在额外的任务时),这种机制就称为“条件变量”(condition variable)。

    从概念上来说,一个条件变量会与多个事件或其他条件相关,并且一个或多个线程会等待条件的达成。

    当某些线程被终止时,为了唤醒等待线程(允许等待线程继续执行)终止的线程将会向等待着的线程广播“条件达成”的信息。

     

     

     

     

     

     

     

     

    等待条件达成

    C++标准库对条件变量有两套实

    现: std::condition_variable 和 std::condition_variable_any 。

    这两个实现都包含在 <condition_variable> 头文件的声明中。

    两者都需要与一个互斥量一起才能工作(互斥量是为了同步);前者仅限于std::mutex 一起工作,而后者可以和任何满足最低标准的互斥量一起工作,从而加上了_any的后缀。

    因为 std::condition_variable_any 更加通用,这就可能从体积、性能,以及系统资源的使用方面产生额外的开销,所以 std::condition_variable 一般作为首选的类型,当对灵活性有硬性要求时,我们才会去考虑 std::condition_variable_any 。

    所以,如何使用 std::condition_variable 去处理之前提到的情况——当有数据需要处理时,

    如何唤醒休眠中的线程对其进行处理?以下清单展示了一种使用条件变量做唤醒的方式。

     

    #include <iostream>                // std::cout
    #include <thread>                // std::thread
    #include <mutex>                // std::mutex, std::unique_lock
    #include <condition_variable>    // std::condition_variable
     
    std::mutex mtx; // 全局互斥锁.
    std::condition_variable cv; // 全局条件变量.
    bool ready = false; // 全局标志位.
     
    void do_print_id(int id)
    {
        std::unique_lock <std::mutex> lck(mtx);
        while (!ready) // 如果标志位不为 true, 则等待...
            cv.wait(lck); // 当前线程被阻塞, 当全局标志位变为 true 之后,
        // 线程被唤醒, 继续往下执行打印线程编号id.
        std::cout << "thread " << id << '
    ';
    }
     
    void go()
    {
        std::unique_lock <std::mutex> lck(mtx);
        ready = true; // 设置全局标志位为 true.
        cv.notify_all(); // 唤醒所有线程.
    }
     
    int main()
    {
        std::thread threads[10];
        // spawn 10 threads:
        for (int i = 0; i < 10; ++i)
            threads[i] = std::thread(do_print_id, i);
     
        std::cout << "10 threads ready to race...
    ";
        go(); // go!
     
      for (auto & th:threads)
            th.join();
     
        return 0;
    }

     

    std::condition_variable 构造函数

    default (1)

    condition_variable();

    copy [deleted] (2)

    condition_variable (const condition_variable&) = delete;

    std::condition_variable 的拷贝构造函数被禁用,只提供了默认构造函数。

    std::condition_variable::wait() 介绍

    unconditional (1)

    void wait (unique_lock<mutex>& lck);

    predicate (2)

    template <class Predicate>

      void wait (unique_lock<mutex>& lck, Predicate pred);

    std::condition_variable 提供了两种 wait() 函数。

    当前线程调用 wait() 后将被阻塞(此时当前线程应该获得了锁(mutex),不妨设获得锁 lck),直到另外某个线程调用 notify_* 唤醒了当前线程。

    在线程被阻塞时,该函数会自动调用 lck.unlock() 释放锁,使得其他被阻塞在锁竞争上的线程得以继续执行。

    另外,一旦当前线程获得通知(notified,通常是另外某个线程调用 notify_* 唤醒了当前线程)wait() 函数也是自动调用 lck.lock(),使得 lck 的状态和 wait 函数被调用时相同。

     

    在第二种情况下(即设置了 Predicate),只有当 pred 条件为 false 时调用 wait() 才会阻塞当前线程,并且在收到其他线程的通知后只有当 pred true 时才会被解除阻塞.

     

    #include <iostream>                // std::cout

    #include <thread>                  // std::thread, std::this_thread::yield

    #include <mutex>                   // std::mutex, std::unique_lock

    #include <condition_variable>      // std::condition_variable

     

    std::mutex mtx;

    std::condition_variable cv;

     

    int cargo = 0;

    bool shipment_available()

    {

        return cargo != 0;

    }

     

    // 消费者线程.

    void consume(int n)

    {

        for (int i = 0; i < n; ++i) {

            std::unique_lock <std::mutex> lck(mtx);

            cv.wait(lck, shipment_available);

            std::cout << cargo << ' ';

            cargo = 0;

        }

    }

     

    int main()

    {

        std::thread consumer_thread(consume, 10); // 消费者线程.

     

                                                  // 主线程为生产者线程, 生产 10 个物品.

        for (int i = 0; i < 10; ++i) {

            while (shipment_available())

                std::this_thread::yield();          //线程调用yield()方法后,表明自己做的事已经完成,让出自己的cpu时间给其他线程使用

            std::unique_lock <std::mutex> lck(mtx);

            cargo = i + 1;

            cv.notify_one();

        }

     

        consumer_thread.join();

     

        std::cin.get();

        return 0;

    }

     

     

  • 相关阅读:
    A Bug's Life-----poj2492(关系并查集)
    食物链--poj1182(并查集含有关系)
    How Many Answers Are Wrong----hdu3038(并查集)
    Parity game---poj1733
    使用FFmpeg类库实现YUV视频序列编码为视频
    JNA调用DLL函数遇到的几个问题
    从一个MFC工程移植对话框类到另一个MFC工程
    X264 输出的统计值的含义(X264 Stats Output)
    毕业生必知二三事。给即将毕业的师弟师妹看看,很有用的~~~~
    网络视频传输的服务质量(QoS)
  • 原文地址:https://www.cnblogs.com/XiaoGuanYu/p/8168686.html
Copyright © 2011-2022 走看看