C++ 11 中的并发多线程模式

目录

一  线程与进程概述

1. 操作系统的并发与并行 

2. 并发的两种方式的异同

2.1 多线程与多进程区别:

2.2 应用场景:

二  std::thread 

1. 概述

2. 常用函数介绍

3. std::thread 使用

三  线程加锁与同步

 1. 多线程打印

2. 多线程加锁打印 

3.多线程轮流打印 1 - 100

3.1  std::condition_variable 介绍

3.2  示例

4. 原子变量

四   线程获取结果   


一  线程与进程概述

1. 操作系统的并发与并行 

并发:并发指的是计算机上的多个任务在同一时间段内完成。

比如:我们在电脑上启动浏览器看 CSDN 博客,同时又启动了音乐播放器播放音乐,用微信QQ 与好友聊天等等,这些看着是同时发生的,但是对于计算机的 CPU 来讲,却是一会运行这个程序,一会又运行另外一个程序,只不过切换的很快,对我们来讲,这是同时发生的。

并行:并行指的是计算机上同一时刻同时运行多个程序。

如果我们的电脑是多核 CPU,那前面的例子,多个程序是可以实现同一时刻运行多个的。

2. 并发的两种方式的异同

计算机中的并发方式有两种:多线程多进程

进程:进程是操作系统分配和管理资源的最小单位

线程:线程是操作系统中任务调度的最小单位,是比进程更小的独立运行的单位,一个进程可以包含多个线程并发运行,线程也被称为轻量级进程。

2.1 多线程与多进程区别

  • 线程间切换比进程间切换的开销小:同一进程中的多个线程共享该进程中的内存等资源,每个线程所占的资源较少,只保留一些寄存器相关上下文切换的信息,因而线程间切换的开销较小
  • 创建销毁线程的开销比进程小:线程可以看成是轻量级的进程,因为每个线程所持有的资源少,因而创建销毁时的开销也要更小一些
  • 线程间通信方式不同:多线程因为都在同一进程中,共享进程的资源,使用相同的内存地址,因而多线程间的通信可以通过共享某个原子的全局变量,或者一块加锁的内存空间即可;而每个进程都是单独用于自己的内存资源,所以进程间的通信比线程要复杂一些,常见进程间通信方式有 socket,管道,消息队列,共享内存(mmap),信号
  • 多进程比多线程的程序健壮:由于每个进程的资源是相互独立的,所以某个进程崩溃是不会影响到其他进程的;然而,多线程是运行在同一个进程中,共享一份资源,因此某个线程若是崩溃,很容易导致整个进程崩溃掉

2.2 应用场景

1. 多进程应用场景:对独立资源保护要求比较高的场景,比如:游戏服务器中每个用户拥有自己的一些数据资源等,此时就可以使用多进程,每个进程代表一个用户

2. 多线程应用场景:互联网中的后端服务器,处理用户的大量请求时,常常采用多线程的方式

二  std::thread 

1. 概述

c 语言中引入了 pthread 来实现多线程,但是并不好用;因而 c++ 11 中引入了 std::thread 类来实现 c++ 中的线程

2. 常用函数介绍

函数说明
thread() noexcept默认构造函数,创建一个线程
thread( thread&& other ) noexcept移动构造函数,将 other 对象移到当前构造函数,构造出新对象
template< class F, class... Args >
explicit thread( F&& f, Args&&... args )
构造函数,f 为要运行的函数,args 为函数 f 的输入参数
thread( const thread& ) = delete拷贝构造函数被禁止使用
~thread()析构函数
join()等待该线程执行完毕后,调用线程再继续执行,会阻塞
detach()允许线程脱离调用线程独立执行,调用线程不必等待线程执行便可继续运行
joinable()判断当前线程是否可以 join
get_id()返回线程 id
thread& operator=( thread&& other )移动赋值函数

3. std::thread 使用

#include <iostream>
#include <string>
#include <thread>

void  foo()
{
    std::this_thread::sleep_for(std::chrono::milliseconds(50));
    std::cout << "threadId: " << std::this_thread::get_id() << ", running thread. " << std::endl;
}

void testThreadFoo()
{
    std::thread t1;
    std::cout << "before starting, joinable: " << t1.joinable() << '\n';

    std::cout << "thread join ....... " << std::endl;
    std::vector<std::thread> vec;
    for (int i = 0; i < 3; i++)
    {
        std::thread t2(foo);
        std::cout << "after starting, joinable: " << t1.joinable() << '\n';
        vec.push_back(std::move(t2));
    }

    for(auto& th : vec)
    {
        th.join();
    }

    std::cout << "thread detatch ....... " << std::endl;
    std::thread th3(foo);
    th3.detach();
    std::cout << "after detach, joinable: " << t1.joinable() << '\n';
    std::this_thread::sleep_for(std::chrono::milliseconds(500));

}


int main(int argc, char *argv[])
{
    testThreadFoo();
    std::cout << "end main ......" << std::endl;
    return 0;
}

输出:

三  线程加锁与同步

题目:用多线程的方式 按照顺序依次打印 1 - 100 

 1. 多线程打印

 示例代码:

#include <iostream>
#include <string>
#include <thread>

int count = 0;

void printCount()
{
    std::this_thread::sleep_for(std::chrono::milliseconds(5));
    std::cout << "threadId: " << std::this_thread::get_id() << ", count: " << count << std::endl;
}

void func1()
{

    while (count < 100)
    {
        printCount();
        count++;
    }
}

void testThread()
{
    std::vector<std::thread>  vec;
    for (int i = 0; i < 5; i++)
    {
        vec.emplace_back(func1);
    }

    for(auto& th : vec)
    {
        th.join();
    }
}

int main(int argc, char *argv[])
{
    std::cout << "enter into main ......" << std::endl;
    testThread();
    std::cout << "end main ......" << std::endl;
    return 0;
}

输出(输出太长,只输出部分):

问题:从输出的截图中,可以清晰的看出不同线程打印出了相同的数字,这是因为在多个线程里调用 printCount 函数时, count 的值还未来得及自增(count++),便被打印了出来,因此不同线程却打印了相同的数字,这与题目中按照顺序打印数字是不相符合的。

在实际的应用程序中,我们也会有类似的问题,某一段代码一个时间段内只允许某个线程单独执行,完成后其他线程才能执行此段代码。

如何解决这样的问题呢?大多数语言都引入了加锁机制与原子变量的概念。

2. 多线程加锁打印 

为了多个线程在同一时间段内同时执行某段代码,多数语言引入了加锁的机制。

这里我们复习一下操作系统的锁机制:

锁的分类:互斥锁、自旋锁、共享锁、条件锁

互斥锁:是一种控制多线程对于共享资源的互斥访问的信号量,当一个线程获取到该共享资源的控制权后会将该共享资源加互斥锁,那么在线程操作完共享资源前,锁都是未释放的状态,当其他线程也尝试获取该共享资源时,会检查共享资源互斥锁是否释放,若是释放的话,才能获取该共享资源的控制权,否则线程就进入了阻塞状态

自旋锁:与互斥锁不同之处在于,当其他线程尝试获取共享资源的时候,检查锁的状态若是处于未释放状态时,线程不会进入阻塞状态,而会持续检查锁的状态,直到获取共享资源成功。

     问题:    自旋锁一直不停的获取共享资源的机制会不会很浪费资源?    

     :互斥锁与自旋锁的应用场景不同。我们需要知道一点,线程间的切换是有一定的开销的,若是对共享资源的操作开销远远大于线程间切换,此时采用互斥锁比较划算;若是对共享资源的操作开销远远小于线程间切换,此时采用自旋锁比较划算。

共享锁:往往与互斥锁一同使用,作为读写锁。所谓读写锁,允许多个线程读共享资源,此时加共享锁;若是线程写的话,需要加写锁(互斥锁),不允许其他线程获取共享资源

条件锁:条件锁就是所谓的条件变量,当某个线程因为某个条件变量未满足时,而陷入了阻塞状态,一旦条件满足,就会以信号量的方式唤醒 因为该条件而陷入阻塞状态的线程。

c++ 中的互斥锁:std::mutex 与 std::unique_lock 共同使用

c++ 中的共享锁:

C++关于锁的总结(一) - 封fenghl - 博客园 (cnblogs.com)

示例代码:

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

std::mutex  mtx;

int count = 0;

void printCount()
{
    std::this_thread::sleep_for(std::chrono::milliseconds(5));
    std::cout << "threadId: " << std::this_thread::get_id() << ", count: " << count << std::endl;
}

void func2()
{
    while (count < 100)
    {
        std::unique_lock<std::mutex> lck(mtx);
        if (count > 100)
            break;
        printCount();
        count++;
    }
}

void testThread()
{
    std::vector<std::thread>  vec;
    for (int i = 0; i < 5; i++)
    {
        vec.emplace_back(func2);
    }

    for(auto& th : vec)
    {
        th.join();
    }
}

int main(int argc, char *argv[])
{
    std::cout << "enter into main ......" << std::endl;
    testThread();
    std::cout << "end main ......" << std::endl;
    return 0;
}

输出(部分):

从截图中可以看出,加了互斥锁以后,多个线程获取共享资源后,可以实现按照顺序打印 1 - 100 数字。

3.多线程轮流打印 1 - 100

在原来的题目上做些修改,现在要求线程之间轮流打印 1 -100 。普通的加锁行为是满足不了该要求的,因为我们无法限定哪个线程来打印哪些数字。

为了解决这样的问题,我们引入了条件锁,也就是条件变量,假设两个线程 1 与 2 轮流打印奇数与偶数,线程1 与 线程2  获取获取到锁后,会继续判断当前条件变量,若是 count 为奇数,则线程1 继续打印,线程 2 进入阻塞状态,相反亦然。

c++ 中的条件变量是: std::condition_variable

3.1  std::condition_variable 介绍

函数说明
condition_variable()默认构造函数
condition_variable( const condition_variable& ) = delete禁止拷贝构造函数
void notify_one() noexcept唤醒一个被当前条件阻塞的线程
void notify_all() noexcept唤醒所有被当前条件阻塞的线程
template< class Predicate >
void wait( std::unique_lock<std::mutex>& lock, Predicate pred )
阻塞当前线程,直到条件变量被唤醒

std::condition_variable - cppreference.com

3.2  示例

示例代码1

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

std::mutex  mtx;
std::condition_variable condition;
int count = 0;

void printCount()
{
    std::this_thread::sleep_for(std::chrono::milliseconds(5));
    std::cout << "threadId: " << std::this_thread::get_id() << ", count: " << count << std::endl;
}

void printCont(int num, int waitVal)
{
    while (true)
    {
        std::unique_lock<std::mutex> lck(mtx);
        condition.wait(lck,[=](){return count % num == waitVal;}); // 条件变量
        if (count > 100)
            break;
        printCount();
        count++;
        condition.notify_all();
    }
}

void testThread2()
{
    std::vector<std::thread>  vec;
    int n = 5;
    for (int i = 0; i < n; i++)
    {
        vec.emplace_back(printCont, n, i);
    }

    for(auto& th : vec)
    {
        th.join();
    }
}

int main(int argc, char *argv[])
{
    std::cout << "enter into main ......" << std::endl;
    testThread2();
    std::cout << "end main ......" << std::endl;
    return 0;
}

输出:

前面有点问题,出现了线程死锁的问题,导致程序夯住了。

两个线程轮流打印1 - 100

示例代码2

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

std::mutex  mtx;
std::condition_variable condition;
bool   flag = true;
int count = 0;

void printCount1()
{
    while (count < 100)
    {
        std::unique_lock<std::mutex> lck(mtx);
        condition.wait(lck,[](){return flag;});
        printCount();
        count++;
        flag = false;
        condition.notify_one();
    }
}

void printCount2()
{
    while (count < 100)
    {
        std::unique_lock<std::mutex> lck(mtx);
        condition.wait(lck,[](){return !flag;});
        printCount();

        count++;
        flag = true;
        condition.notify_one();
    }
}

void testThread1()
{
    std::thread th1(printCount1);
    std::thread th2(printCount2);

    th1.join();
    th2.join();
}

int main(int argc, char *argv[])
{
    std::cout << "enter into main ......" << std::endl;
    testThread2();
    std::cout << "end main ......" << std::endl;
    return 0;
}

输出:

4. 原子变量

       c++ 中的 atomic 原子,化学上原子在最小的不可变颗粒,用在计算机世界,就代表某个原子变量的操作在一段时间内只能有一个线程操作,即是线程安全的。相比与加锁的方式,原子变量的性能更好。

std::atomic - cppreference.com

示例代码:

#include <iostream>
#include <string>
#include <thread>
#include <mutex>
#include <atomic>

std::atomic_int atomaticVal(0);
int  count = 0;

void printVal()
{
    for (int i = 0; i < 10000; i++)
    {
        atomaticVal++;
        count++;
    }
}

void testThread3()
{
    std::vector<std::thread>  vec;
    int n = 10;
    for (int i = 0; i < n; i++)
    {
        vec.emplace_back(printVal);
    }

    for(auto& th : vec)
    {
        th.join();
    }
    std::cout << "atomaticVal: " << atomaticVal << std::endl;
    std::cout << "count: " << count << std::endl;

}

int main(int argc, char *argv[])
{
    std::cout << "enter into main ......" << std::endl;

    testThread3();

    std::cout << "end main ......" << std::endl;
    return 0;
}

输出

可以看出,原子变量的自增是线程安全的,非原子变量的自增不是线程安全的。

四   线程获取结果   

前面介绍的线程执行的函数都是无返回值的,如果线程执行的函数有返回值的话,应该怎么办呢?

介绍一下 c++11 引入的 std::future 与 std::package_task

详情见:

std::future - cppreference.com

std::packaged_task - cppreference.com

示例代码如下:

#include <thread>
#include <mutex>
#include <condition_variable>
#include <atomic>
#include <future>
#include<functional>

int  addFunc(int a, int b)
{
   std::this_thread::sleep_for(std::chrono::milliseconds(50));
   return a + b;
}

int sumFunc(int n)
{
    int result = 0;

    for(int i = 1; i <= n; i++)
    {
        std::this_thread::sleep_for(std::chrono::milliseconds(5));
        result += i;
    }

    return result;
}

void futureTest()
{
    std::packaged_task<int()>  task1 = std::packaged_task<int()>(std::bind(&addFunc, 2, 5));
    std::future<int> resultFuture = task1.get_future();
    task1();
    std::cout << resultFuture.get() << std::endl;


    std::packaged_task<int(int, int)>  task2 = std::packaged_task<int(int, int)>(addFunc);
    std::future<int> resultFuture2 = task2.get_future();
    std::thread  td(std::move(task2), 1, 5);
    td.join();
    std::cout << resultFuture2.get() << std::endl;


    std::packaged_task<int(int)>  task3 = std::packaged_task<int(int)>(sumFunc);
    std::packaged_task<int(int)>  task4 = std::packaged_task<int(int)>(sumFunc);

    std::future<int> resultFuture3 = task3.get_future();
    std::future<int> resultFuture4 = task4.get_future();

    std::thread  th3(std::move(task3), 100);
    std::thread  th4(std::move(task4), 200);
    th3.join();
    th4.join();

    std::cout << resultFuture3.get() << std::endl;
    std::cout << resultFuture4.get() << std::endl;
}

int main(int argc, char *argv[])
{
    std::cout << "enter into main ......" << std::endl;

    futureTest();

    std::cout << "end main ......" << std::endl;
    return 0;
}

输出:

  • 26
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值