从C语言到C++_40(多线程相关)C++线程接口+线程安全问题加锁(shared_ptr+STL+单例)(上)

简介: 从C语言到C++_40(多线程相关)C++线程接口+线程安全问题加锁(shared_ptr+STL+单例)

此篇建议学了Linux系统多线程部分再来看。

1. C++多线程

       在C++11之前,涉及到多线程问题,都是和平台相关的,比如windows和linux下各有自己的接口,这使得代码的可移植性比较差。

       C++11中最重要的特性就是支持了多线程编程,使得C++在并行编程时不需要依赖第三方库,而且在原子操作中还引入了原子类的概念。


1.1 thread库

查下文档:


如图所示,C++11提供了thread库,thread是一个类,在使用的时候需要包含头文件pthread。

构造函数:

  • 默认构造函数thread()使用该构造函数创建的线程对象仅是创建对象,线程并没有被创建,也没有允许。
  • thread(Fn&& fn, Args&&... args),这是一个万能引用模板。使用该构造函数时,第一个参数是可调用对象,可以是左值也可以是右值,比如函数指针,仿函数对象,lambda表达式等等。后面的可变参数就是传给线程函数的实参,是一个参数包,也就是可变参数。
  • thread(const thread&) = delete,线程之间是禁止拷贝的。
  • thread(thread&& x),移动构造函数。

成员函数:

  • get_id,用来获取当前线程的tid值。调用该函数通常都是当前线程,但是当前的从线程从并没有自己的thread对象

       所以线程库由提供了一个命名空间,该空间中有上图所示的几个函数,可以通过命名空间来直接调用,如:

this_thread::get_id(); // 获取当前线程tid值

哪个线程执行这条语句就返回哪个线程的tid值,命名空间中的其他几个函数的用法也是这样。

  • yield调用该接口的线程会让其CPU,让CPU调度其他线程。
  • sleep_until调用该接口的线程会延时至一个确定的时间点。
  • sleep_for调用该接口的线程会延时一个时间段,如1s。
  • operator=(thread&& t),移动赋值。

将一个线程对象赋值给另一个线程对象,通常这么用:

  thread t1; // 仅创建对象,不创建线程
  t1 = thread(func); // t1线程函数并且执行

此时原本只创建的线程对象就有一个线程在跑了。

注意:只能赋右值,不能赋左值,因为赋值运算符重载被禁掉了,只有移动赋值


  • join,线程等待,用来回收线程资源。一般主线程会调用该函数,以t.join()的形式,t就是需要被等待的线程对象,此时主线程会阻塞在这里,直到从线程运行结束。

     如上面的多线程一样,必须使用join,否则线程资源不会回收,而且如果从线程运行的时间比主线程长的话,主线程会直接运行完并且回收所有资源,导致从线程被强制结束。


  • joinable,用来判断线程是否有效。

如果是以下任意情况则线程无效:

  1. 采用无参构造函数构造的线程对象
  2. 线程对象的状态已经转移给其他线程对象
  3. 线程已经调用 join 或者 detach 结束
  • detach,线程分离,从线程结束后自动回收资源。

其他的就不介绍了,用到的时候自行查文档即可。

要谨记:thread是禁止拷贝的,不允许拷贝构造以及赋值,但是可以移动构造和移动赋值


使用一下:

#include <iostream>
#include <thread>
using namespace std;
 
void Print(int n, int& x)
{
  for (int i = 0; i < n; ++i)
  {
    cout << this_thread::get_id() << ":" << i << endl;
    std::this_thread::sleep_for(std::chrono::milliseconds(100));
    ++x;
  }
}
int main()
{
  int count = 0;
  thread t1(Print, 10, ref(count)); // 可调用对象和其实参
  thread t2(Print, 10, ref(count));
 
  t1.join();
  t2.join();
 
  cout << count << endl;
 
  return 0;
}

       多次运行的结果不一样,可能会出现像第一行一样的抢着打印的问题(学了Linux多线程应该比较清楚),下面就应该想到加锁了。


1.2 mutex库

如上图所示,C++11提供了mutex库,mutex同样是一个类,在使用的时候要包含头文件mutex。

构造函数:

  • 只有默认构造函数mutex(),在创建互斥锁的时候不需要传任何参数。
  • mutex(const mutex&)=delete,禁止拷贝。

其他成员函数:

  • lock(),给临界区加锁,加锁成功继续向下执行,失败则阻塞等待。
  • unlock(),给临界区解锁。
  • try_lock(),给临界区尝试加锁,加锁成功返回true,加锁失败返回false。使用try_lock时,如果申请失败则不阻塞,跳过申请锁的部分,执行非临界区代码。来看伪代码:
mutex mtx;
 
if(mtx.try_lock())
{
  // 临界区代码
  // ......
}
else
{
  // 非临界区代码
  // ......
}

mutex不能递归使用,如下面伪代码所示:

    void Func(int n)
  {
    lock(); // 加锁
    // 临界区代码
    // ......
    Func(n - 1); // 递归调用
    unlock(); // 解锁
  }


在递归中不能使用这样的锁,会造成死锁。

最上面例子的正确使用如下:

#include <iostream>
#include <thread>
#include <mutex>
using namespace std;
 
void Print(int n, int& x, mutex& mtx)
{
  for (int i = 0; i < n; ++i)
  {
    mtx.lock();
    cout << this_thread::get_id() << ":" << i << endl;
    std::this_thread::sleep_for(std::chrono::milliseconds(100));
    ++x;
    mtx.unlock();
  }
}
 
int main()
{
  mutex m;
  int count = 0;
  thread t1(Print, 10, ref(count), ref(m));
  thread t2(Print, 10, ref(count), ref(m));
 
  t1.join();
  t2.join();
  cout << count << endl;
  return 0;
}

后面再来看看怎么实现交错打印的效果,再看看另一种用法:(lambda)

int main()
{
  mutex mtx;
  int x = 0;
  int n = 10;
  thread t1([&](){
    for (int i = 0; i < n; ++i)
    {
      mtx.lock();
      cout << this_thread::get_id() << ":" << i << endl;
      std::this_thread::sleep_for(std::chrono::milliseconds(100));
      ++x;
      mtx.unlock();
    }
  });
 
  thread t2([&](){
    for (int i = 0; i < n; ++i)
    {
      mtx.lock();
      cout << this_thread::get_id() << ":" << i << endl;
      std::this_thread::sleep_for(std::chrono::milliseconds(100));
      ++x;
      mtx.unlock();
    }
  });
 
  t1.join();
  t2.join();
  cout << x << endl;
  return 0;
}

上面代码的问题:如果加锁解锁之间存在抛异常就死锁了,这时就要用到RAII锁。


1.3 RAII锁

lock_guard是一个类,采用了RAII方式来加锁解锁——将锁的生命周期和对象的生命周期绑定在一起。看下在Linux篇章写的代码:(把锁封装了)

#pragma once
#include <iostream>
#include <pthread.h>
 
class Mutex
{
public:
    Mutex(pthread_mutex_t* mtx) 
        :_pmtx(mtx)
    {}
    void lock()
    {
        pthread_mutex_lock(_pmtx);
        std::cout << "进行加锁成功" << std::endl;
    }
    void unlock()
    {
        pthread_mutex_unlock(_pmtx);
        std::cout << "进行解锁成功" << std::endl;
    }
    ~Mutex()
    {}
protected:
    pthread_mutex_t* _pmtx;
};
 
class lockGuard // RAII风格的加锁方式
{
public:
    lockGuard(pthread_mutex_t* mtx) // 因为不是全局的锁,所以传进来,初始化
        :_mtx(mtx)
    {
        _mtx.lock();
    }
    ~lockGuard()
    {
        _mtx.unlock();
    }
protected:
    Mutex _mtx;
};

看库里的构造函数:

  • lock_guard(mutex_type& m),在创建这个对象的时候需要传入一把锁,在构造函数中,进行了加锁操作。
  • lcok_guard(const lock_guard&)=delete,该对象禁止拷贝,因为互斥锁就不可以拷贝。

析构函数的作用就是将lock_guard对象的资源释放,也就是进行解锁操作。

lock_guard只有构造函数和析构函数,使用该类对象加锁时不需要我们去关心锁的释放,但是它不能在对象生命周期结束之前主动解锁。

看一下unique_lock:

unique_lock也是一种RAII的加锁对象,它和lock_guard的功能一样,将锁的生命周期和对象的生命周期绑定在一起,但是又有区别。

  • unique_lock(mutex_type& m),这个和lock_guard的用法一样,在构造函数中加锁。
  • unique_lock(const unique_lock&)=delete,同样禁止拷贝。

析构函数中和lock_guard一样,也是进行解锁操作。

  • lock,加锁。
  • unlock,解锁。
  • try_lock,尝试加锁。

lock_guard中就没有这几个接口,所以unique_lock可以在析构之前主动解锁,主动解锁后仍然可以再主动加锁,这一点lock_guard是不可以的。

  • try_lock_for,尝试加锁一段时间,时间到后自动解锁。
  • try_lock_until,尝试加锁到指定时间,时间到来后自动解锁。

用法很多,需要使用的时候可以结合库文档来使用。用一下lock_guard和lambda的另一种用法:

int main()
{
  mutex mtx;
  int n = 10;
  int m;
  cin >> m;
 
  vector<thread> v(m);
  for (int i = 0; i < m; ++i)
  {
    // 移动赋值给vector中线程对象
    v[i] = thread([&](){
      for (int i = 0; i < n; ++i)
      {
        {
          lock_guard<mutex> lk(mtx);
          cout << this_thread::get_id() << ":" << i << endl;
        }
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
      }
    });
  }
  for (auto& t : v)
  {
    t.join();
  }
  return 0;
}

从C语言到C++_40(多线程相关)C++线程接口+线程安全问题加锁(shared_ptr+STL+单例)(中):https://developer.aliyun.com/article/1522534

目录
相关文章
|
2天前
|
存储 Linux C语言
c++进阶篇——初窥多线程(二) 基于C语言实现的多线程编写
本文介绍了C++中使用C语言的pthread库实现多线程编程。`pthread_create`用于创建新线程,`pthread_self`返回当前线程ID。示例展示了如何创建线程并打印线程ID,强调了线程同步的重要性,如使用`sleep`防止主线程提前结束导致子线程未执行完。`pthread_exit`用于线程退出,`pthread_join`用来等待并回收子线程,`pthread_detach`则分离线程。文中还提到了线程取消功能,通过`pthread_cancel`实现。这些基本操作是理解和使用C/C++多线程的关键。
|
4天前
|
存储 安全 程序员
c++理论篇——初窥多线程(一) 计算机内存视角下的多线程编程
c++理论篇——初窥多线程(一) 计算机内存视角下的多线程编程
|
4天前
|
Java
你还在单线程里奋斗?来看看 Java 多线程创建的魅力!
【6月更文挑战第19天】单线程程序中任务依次执行导致效率低,而Java多线程可并行处理任务,提高效率。在示例中,多线程版本并行运行三个任务,减少总耗时,展示出多线程在处理并发和提升响应速度上的优势。使用`Thread`类创建线程,通过`start()`启动,`join()`确保所有线程执行完毕,揭示了多线程在编程中的强大潜力。
|
4天前
|
Java 开发者
线程的诞生之路:Java多线程创建方法的抉择与智慧
【6月更文挑战第19天】Java多线程编程中,开发者可选择继承Thread类或实现Runnable接口。继承Thread直接但受限于单继承,适合简单场景;实现Runnable更灵活,支持代码复用,适用于如银行转账这类需多线程处理的复杂任务。在资源管理和任务执行控制上,Runnable接口通常更优。
|
4天前
|
Java
Java 多线程新手必读:线程的创建技巧与陷阱
【6月更文挑战第19天】Java多线程初学者须知:创建线程可通过继承`Thread`或实现`Runnable`接口。继承`Thread`限制单继承,实现`Runnable`更灵活。记得调用`start()`而非`run()`启动线程,避免并发问题时需正确同步共享资源。示例代码展示两种创建方式及未同步导致的问题。
|
4天前
|
安全 Java
【极客档案】Java 线程:解锁生命周期的秘密,成为多线程世界的主宰者!
【6月更文挑战第19天】Java多线程编程中,掌握线程生命周期是关键。创建线程可通过继承`Thread`或实现`Runnable`,调用`start()`使线程进入就绪状态。利用`synchronized`保证线程安全,处理阻塞状态,注意资源管理,如使用线程池优化。通过实践与总结,成为多线程编程的专家。
|
4天前
|
Java 开发者
告别单线程时代!Java 多线程入门:选继承 Thread 还是 Runnable?
【6月更文挑战第19天】在Java中,面对多任务需求时,开发者可以选择继承`Thread`或实现`Runnable`接口来创建线程。`Thread`继承直接但限制了单继承,而`Runnable`接口提供多实现的灵活性和资源共享。多线程能提升CPU利用率,适用于并发处理和提高响应速度,如在网络服务器中并发处理请求,增强程序性能。不论是选择哪种方式,都是迈向高效编程的重要一步。
|
4天前
|
Java 开发者
震惊!Java多线程的惊天秘密:你真的会创建线程吗?
【6月更文挑战第19天】Java多线程创建有两种主要方式:继承Thread类和实现Runnable接口。继承Thread限制了多重继承,适合简单场景;实现Runnable接口更灵活,可与其它继承结合,是更常见选择。了解其差异对于高效、健壮的多线程编程至关重要。
|
5天前
|
Java 程序员
Java多线程编程是指在一个进程中创建并运行多个线程,每个线程执行不同的任务,并行地工作,以达到提高效率的目的
【6月更文挑战第18天】Java多线程提升效率,通过synchronized关键字、Lock接口和原子变量实现同步互斥。synchronized控制共享资源访问,基于对象内置锁。Lock接口提供更灵活的锁管理,需手动解锁。原子变量类(如AtomicInteger)支持无锁的原子操作,减少性能影响。
18 3
|
4天前
|
Java
JAVA多线程深度解析:线程的创建之路,你准备好了吗?
【6月更文挑战第19天】Java多线程编程提升效率,通过继承Thread或实现Runnable接口创建线程。Thread类直接继承启动简单,但限制多继承;Runnable接口实现更灵活,允许类继承其他类。示例代码展示了两种创建线程的方法。面对挑战,掌握多线程,让程序高效运行。