C++多线程系统编程

对象生命周期管理

C++11标准库引入shared_ptrwak_ptr

  • shared_ptr是强引用(可以改变资源的引用计数),控制对象的生命期:只要有一个指向对象的shared_ptr存在,该对象就不会析构。
  • weak_ptr是弱引用(无法改变资源的引用计数),不控制对象的生命期:它知道对象是否还存活;可通过线程安全的lock()尝试提升为有效的shared_ptr
  • 智能指针解决交叉引用导致的资源泄露问题
cpp 复制代码
// 智能指针解决多线程访问共享对象时的安全问题
#include <iostream>
#include <memory>
#include <thread>

using namespace std;

class A
{
public:
    A() { cout << "A()" << endl; }
    ~A() { cout << "~A()" << endl; }
    void testA() { cout << "testA()" << endl; }
};


// void handler(A *q)
void handler(weak_ptr<A> pw) // 弱智能指针不会引起对象的引用计数改变!
{
    // std::this_thread::sleep_for(std::chrono::seconds(2));
    shared_ptr<A> sp = pw.lock();
    if (sp != nullptr) // 检测共享对象是否存活
    {
        sp->testA();
    }
    else
    {
        cout << "A 对象已经析构,不能再访问.." << endl;
    }
}

int main()
{
    // A *p = new A();
    shared_ptr<A> p(new A());

    thread t(handler, weak_ptr<A>(p));

    t.detach(); // 分离线程

    std::this_thread::sleep_for(std::chrono::seconds(2)); // 主线程睡眠2s

    return 0;
}

线程同步精要

线程同步的四项原则:

  1. 首要原则是尽量最低限度地共享对象,减少需要同步的场合。
  2. 其次是使用高级的并发编程构件,如TaskQueueProducer-Consumer Queue等。
  3. 不得已使用底层同步原语时,只用非递归的互斥其和条件变量,慎用读写锁(与简单的mutex相比,它实际上降低了性能),不要用信号量。
  4. 除了使用atomic整数外,不用"内核级"同步原语。

MutexLock:封装临界区,是一个简单的资源类,用RAII(对象创建时获取资源,对象销毁时释放资源)方式封装互斥器的创建与销毁。

MutexLockGuard:封装临界区的进入和退出,即加锁和解锁。(类似于C++11标准中的<mark>lock_guard</mark>

cpp 复制代码
#include "noncopyable.h"
#include "CurrentThread.h"

#include <assert.h>
#include <pthread.h>

class MutexLock : noncopyable
{
public:
    MutexLock()
        : holder_(0)
    {
        // 初始化互斥锁
        pthread_mutex_init(&mutex_, NULL);
    }

    ~MutexLock()
    {
        // 断言当前没有线程持有锁
        assert(holder_ == 0);
        // 销毁互斥锁
        pthread_mutex_destroy(&mutex_);
    }

    bool isLockedByThisThread()
    {
        // 检查当前线程是否持有锁
        return holder_ == CurrentThread::tid();
    }

    void assertLocked()
    {
        assert(isLockedByThisThread());
    }

    void lock() // 仅供MutexLockGuard调用,严禁用户代码调用
    {
        pthread_mutex_lock(&mutex_);
        holder_ == CurrentThread::tid();
    }

    void unlock() // 仅供MutexLockGuard调用,严禁用户代码调用
    {
        holder_ = 0;
        pthread_mutex_unlock(&mutex_);
    }

    pthread_mutex_t* getPthreadMutex() // 仅供Condition调用,严禁用户代码调用
    {
        return &mutex_;
    }

private:
    pthread_mutex_t mutex_; // 互斥锁
    pid_t holder_; // 持有锁的线程ID
};

class MutexLockGuard : noncopyable
{
public:
    explicit MutexLockGuard(MutexLock& mutex)
        : mutex_(mutex)
    {
        mutex_.lock();
    }

    ~MutexLockGuard()
    {
        mutex_.unlock();
    }
    
private:
    MutexLock& mutex_;
};
  • 以上代码仅展示互斥锁的简单封装,并未达到工业强度(mutex创建为PTHREAD_MUTEX_DEFAULT类型而不是PTHREAD_MUTEX_NORMAL类型,严格应该指定;没有检查返回值,assert()在release build中是空语句。)
cpp 复制代码
// 用MutexLockGuard代替普通的互斥锁模拟车站卖票

MutexLock mutex_;
std::mutex mtx; // 全局互斥锁 -> 保证不会出现竞态条件

void sellTicket(int index) // 模拟卖票的线程函数
{
	while (ticketCount > 0)
	{
        {
            MutexLockGuard lock(mutex_);
            // lock_guard<std::mutex> lock(mtx); // 构造时获取锁,出作用域{}析构锁(解锁)
            if (ticketCount > 0) // 锁 + 双重判断 -> 防止三个线程在ticketCount为1时都再卖一次票
            { 
                // 临界区代码段 -> 原子操作 -> 线程间互斥操作
                cout << "窗口:" << index << "卖出第:" << ticketCount << "张票" << endl;
                ticketCount--;
		    }
        }
		// mtx.lock(); // 上锁
		// if (ticketCount > 0) // 锁 + 双重判断 -> 防止三个线程在ticketCount为1时都再卖一次票
		// { 
		// 	// 临界区代码段 -> 原子操作 -> 线程间互斥操作
		// 	cout << "窗口:" << index << "卖出第:" << ticketCount << "张票" << endl;
		// 	ticketCount--;
		// }
		// mtx.unlock(); // 解锁
		std::this_thread::sleep_for(std::chrono::milliseconds(100));
	}
}
  • ticketCount为1时,可能多个线程都会进入while循环抢互斥锁,而此时只需卖1张票即可,故添加双重判断以避免可能产生的编程错误。

线程同步的四项原则,尽量用高层同步设施(线程池、队列、倒计时)。 使用普通互斥器和条件变量完成剩余的同步任务,采用RAII方式

多线程服务器

文中的"多线程服务器"是指运行在Linux操作系统上的独占式网络应用程序。 Linux下进程间通信(IPC)的方式很多,陈硕大神首选使用Socket(主要指TCP):可跨主机,具有伸缩性。

进程可理解为"内存中正在允许的程序",每个进程都有自己独立的地址空间。

线程是CPU调度的最小单位,特点是共享地址空间,从而可以高效地共享数据。

陈硕大神推荐的C++多线程服务端编程模式为one loop per thread + thread pool。 one loop per thread :程序中的每个IO线程有一个event loop(Reactor),用于处理读写和定时事件。Event loop代表了线程的主循环,需要让哪个线程干活,就把timer或IO channel(如TCP连接)注册到哪个线程的loop里即可。 thread pool :对于没有IO而仅有计算任务的线程,使用event loop有点浪费,陈硕大神使用的是blocking queue实现的任务队列TaskQueue

在需要限制CPU占用率的场景下可以考虑采用单线程程序,而多线程程序的适用场景有:

  • 有多个CPU可用(单核机器上多线程无性能优势);
  • 线程间有共享数据,即内存中的全局状态;
  • 延迟latency和吞吐量throughput同样重要;
  • 具有可预测的性能。随着负载增加,性能缓慢下降,超过某个临界点之后会急速下降;
  • 多线程能有效地划分责任与功能,让每个线程的逻辑比较简单,任务单一。

多线程服务程序中的线程大致分为3类

  1. IO线程:主循环是IO multiplexing,阻塞地等待在select/poll/epoll_wait系统调用上。
  2. 计算线程:主循环是blocking queue,阻塞地等待在condition variable上。
  3. 第三方库所用的线程,比如logging或database connection。
cpp 复制代码
// 在Muduo网络库中陈硕大神基于MutexLockGuard和Condition
// 实现了线程安全的任务队列TaskQueue
// 在此基于C++11封装一个blocking的简单任务队列TaskQueue
// C++11提供的unique_ptr、lock_guard等非常方便封装vector、queue等
// 成为线程安全的容器。

#pragma once

#include "noncopyable.h"

#include <deque>
#include <mutex>
#include <memory>
#include <condition_variable>

template<typename T>
class BlockingQueue : noncopyable
{
public:
    BlockingQueue()
        : mutex_(), 
          queue_()
    {}

    void put(const T& x)
    {
        std::unique_lock<std::mutex> lock(mutex_);
        queue_.push_back(x);
        notEmpty_.notify_all();
    }

    T take()
    {
        std::unique_lock<std::mutex> lock(mutex_);
        while (queue_.empty())
        {
            notEmpty_.wait(lock);
        }
        T front(std::move(queue_.front())); // 取出队列前端的元素
        queue_.pop_front();
        return std::move(front);
    }

    size_t size() const
    {
        std::lock_guard<std::mutex> lock(mutex_);
        return queue_.size();
    }

private:
    // // mutable修饰符用于C++中,表示即使在一个const成员函数中,该变量仍可修改
    mutable std::mutex mutex_; // 互斥锁
    std::deque<T> queue_;
    std::condition_variable notEmpty_; // 用于表示队列非空
};

多线程编程精要

C++的标准库容器和std::string都不是线程安全的,一方面是避免不必要的性能开销,另一方面是单个成员函数的线程安全并不具备可组合性。 pthread_t的值容易重复,在Linux系统中,陈硕大神建议使用gettid()系统调用的返回值作为线程id。

cpp 复制代码
safe_vector<int> vec; // 全局可见 -> 其每个成员函数都是线程安全的

if (!vec.empty()) // 未加锁保护
{
    int x = vec[0]; // 在多线程情况下不安全
}

// 可能在if语句判空后,别的线程清空vec的元素,造成vec[0]失效

线程创建的几条原则:

  1. 程序库不应该在未提前告知的情况下创建自己的"背景线程"。
  2. 尽量使用相同的方式创建线程,例如Mudue::Thread
  3. 在进入main()函数之前不应该启动线程。
  4. 程序中线程的创建最好能在初始化阶段全部完成。

高效的多线程日志

"日志",即文本的、供人阅读的日志,通常用于故障诊断和追踪,亦可用于性能分析。

muduo日志库采用的是双缓冲技术,基本思路是准备两块buffer:A和B,前端负责往buffer A填数据(日志信息),后端负责将buffer B的数据写入文件。当buffer A写满之后,交换A和B,让后端将buffer A的数据写入文件,而前端则往buffer B填入新的日志信息,如此往复。用两个buffer的好处是在新建日志消息的时候不必等待磁盘文件操作,也避免每条新日志消息都触发(唤醒)后端日志线程。

实际实现采用了四个缓冲区,可以进一步减少或避免日志前端的等待。

cpp 复制代码
// muduo/base/AsyncLogging.h

// LargeBuffer大小为4MB,可以存至少1000条日志消息
typedef boost::ptr_vector<LargeBuffer> BufferVector;
// ptr_vector::auto_type类似于std::unique_ptr
typedef BufferVector::auto_type BufferPtr
muduo::MutexLock mutex_; // 用于保护后面的四个数据成员
muduo::Condition cond_;
BufferPtr currentBuffer_; // 当前缓冲
BufferPtr nextBuffer_; // 预备缓冲
BufferVector buffers_; // 将写入文件的已填满的缓冲
cpp 复制代码
// muduo/base/AsyncLogging.cc

// 前端在生成一条日志消息的时候会调用AsyncLogging::append()
void AsyncLogging::append(const char* logline, int len)
{
  muduo::MutexLockGuard lock(mutex_);
  if (currentBuffer_->avail() > len)
  {
    // 当前缓冲剩余的空间足够大,直接把日志消息拷贝(追加)到当前缓冲中
    currentBuffer_->append(logline, len);
  }
  else
  {
    // 否则,当前缓冲区已写满,把它送入(移入)buffers_
    buffers_.push_back(std::move(currentBuffer_));

    // 试图将预备缓冲移用为当前缓冲
    if (nextBuffer_)
    {
      currentBuffer_ = std::move(nextBuffer_);
    }
    else
    {
      currentBuffer_.reset(new Buffer); // Rarely happens
    }
    // 追加日志消息,并通知(唤醒)后端开始写入日志数据
    currentBuffer_->append(logline, len);
    cond_.notify();
  }
}

// 接收方(后端)实现
void AsyncLogging::threadFunc()
{
    // 准备好两块空闲的buffer,以备在临界区内交换
    BufferPtr newBuffer1(new Buffer);
    BufferPtr newBuffer2(new Buffer);
    BufferVector buffersToWrite;
    while (running_)
    {
        // swap out what need to be written, keep CS short
        {
            muduo::MutexLockGuard lock(mutex_);
            if (buffers_.empty())
            {
                cond_.waitForSeconds(flushInterval_);
            }
            // 条件满足时,将缓冲区currentBuffer_移入buffers_
            buffers_.push_back(std::move(currentBuffer_));
            // 并将空闲的newBuffer1移为当前缓冲
            currentBuffer_ = std::move(newBuffer1);
            // 将buffers_与buffersToWrite交换
            buffersToWrite.swap(buffers_);
            if (!nextBuffer_)
            {
                // 用newBuffer2替换nextBuffer_,保证前端始终有一个预备buffer可供调配
                nextBuffer_ = std::move(newBuffer2);
            }
        }
        // 临界区外安全的访问buffersToWrite,将日志数据写入文件
        // 重新填充newBuffer1和newBuffer2
        // 这样下次执行时还有两个空闲buffer可用于替换前端的当前缓冲和预备缓冲
    }
    // flush output
}

在前后端之间高效传递日志消息的办法不止一种,比方说使用常规的muduo::BlockingQueue<std::string>muduo::BoundedBlockingQueue<std::string>在前后端之间传递日志消息,其中每个std::string是一条消息。这种做法每条日志消息都要分配内存,特别是在前端线程分配的内存要由后端释放,因此对malloc的实现要求较高,需要针对多线程特别优化。 muduo库的异步日志实现用了一个全局锁。尽管临界区很小,但是如果线程数目较多,锁争用也可能影响性能。一种解决办法是像Java的ConcurrentHashMap那样用多个桶子bucket,前端写日志的时候再按线程id哈希到不同的bucket中,以减少contention。

C++11多线程

  • "语言级别"→ 跨平台 - windows/linux/max
  • "语言层面"→ thread -> windows - createThread; linux - pthread_create(实质调用的仍是各系统的接口)

基本用法

  1. 如何创建启动一个线程 -> 传入线程所需的线程函数和参数,线程自动开启std::thread t(threadFunction);
  2. 子线程如何结束 -> 子线程函数在执行完成,线程就结束了
  3. 主线程如何处理子线程 -> (1)t.join():等待t线程结束,当前线程继续往下运行;(2)t.detach():将t线程设置为分离线程,主线程结束,整个线程结束,所有的子线程都自动结束。
cpp 复制代码
#include <iostream>
#include <thread>
#include <chrono>

using namespace std;

void download()
{
    // 模拟下载, 总共耗时500ms,阻塞线程500ms
    this_thread::sleep_for(chrono::milliseconds(500));
    cout << "child thread: " << this_thread::get_id() << ", download success...." << endl;
}

void func(int num, string str)
{
    cout << "child thread: " << this_thread::get_id << endl;
    cout << "num: " << num << ", str: " << str << endl;
}

int main()
{
    // 打印主线程ID
    cout << "main thread ID: " << this_thread::get_id() << endl;

    // 创建子线程
    thread t(download);

    // join() -> 阻塞函数:若download()未完成,则主线程阻塞至任务执行完毕;若download()完成,则继续执行
    t.join(); 

    // 若程序要求固定顺序,则可用join()函数来控制 → 例如download()完成后再创建一个子线程
    thread t1(func, 13, "lucky number");

    // detach() -> 线程分离函数
    t1.detach();

    // 让主线程休眠, 等待子线程执行完毕 -> 防止线程分离后,func()未完成,主线程就退出并销毁子线程
    this_thread::sleep_for(chrono::seconds(5));

    // 获取当前计算机的CPU核心数
    int num = thread::hardware_concurrency();
    cout << "CPU number: " << num << endl;

    return 0;
}

临界区互斥锁

  • 多线程程序执行的结果是一致的,不会随着CPU对线程的不同调用顺序而产生不同的运行结果
  • 若多线程程序会产生不同的运行结果,则称为竞态条件
  1. 当多个线程均涉及到临界区资源的修改时,可用mutex对临界区资源进行加锁解锁,以保证程序正常执行!
cpp 复制代码
#include <iostream>
#include <thread>
#include <mutex>
#include <list>
using namespace std;

// C++ thread 模拟车站三个窗口卖票的程序

int ticketCount = 100; // 车站有100张车票,由三个窗口一起卖票
std::mutex mtx; // 全局互斥锁 -> 保证不会出现竞态条件

// 模拟卖票的线程函数
void sellTicket(int index)
{
    while (ticketCount > 0)
    {
        mtx.lock(); // 上锁
        if (ticketCount > 0) // 锁 + 双重判断 -> 防止三个线程在ticketCount为1时都再卖一次票
        { 
            // 临界区代码段 -> 原子操作 -> 线程间互斥操作
            cout << "窗口:" << index << "卖出第:" << ticketCount << "张票" << endl;
            ticketCount--;
        }
        mtx.unlock(); // 解锁
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }
}

int main()
{
    list<std::thread> tlist;
    // 创建线程 -> 模拟车站卖票
    for (int i = 0; i < 3; ++i)
    {
        tlist.push_back(std::thread(sellTicket, i));
    }

    for (std::thread& t : tlist)
    {
        t.join();
    }

    cout << "所有窗口卖票结束..." << endl;
    
    return 0;
}
  1. 一般加锁,若程序在临界区中return,则可能无法解锁,造成死锁的问题。lock_guard是在作用域中加锁,而出作用域会自动解锁,若临界区中return相当于出了作用域,故会自动解锁,不会因return造成死锁问题!
cpp 复制代码
// 模拟卖票的线程函数
void sellTicket(int index)
{
    while (ticketCount > 0)
    {
        {
            lock_guard<std::mutex> lock(mtx); // 构造时获取锁,出作用域{}析构锁(解锁)
            if (ticketCount > 0) // 锁 + 双重判断 -> 防止三个线程在ticketCount为1时都再卖一次票
            {
                // 临界区代码段 -> 原子操作 -> 线程间互斥操作
                cout << "窗口:" << index << "卖出第:" << ticketCount << "张票" << endl;
                ticketCount--;
            }
        }
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }
}
  1. lock_guard定义处会调用构造函数加锁,离开定义域的话lock_guard会被销毁,调用析构函数解锁 → 若作用域较大,则锁的颗粒度较大,很大程度上影响效率。通信中常用unique_lock!
  • 一般加锁、解锁不涉及return时,可以考虑使用mutex!
  • lock_guard不可能用在函数参数传递或返回过程中,只能用于简单代码段的互斥操作中!
  • unique_lock不仅可以用于简单代码段的互斥操作,亦可用在函数的调用过程中!

同步通信

多线程编有两个问题:

  1. 线程间的互斥(竞态条件 -> 临界区代码段"加锁")
  2. 线程间的同步通信 -> 生产者、消费者模型

若生产者还未生产"产品",消费者无法消费 -> 故需进行线程同步("线程间通话")

cpp 复制代码
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
#include <chrono> // 包含 chrono 以支持 sleep_for

using namespace std;

mutex mtx; // 互斥锁
condition_variable cv; // 条件变量

class Queue
{
public:
    Queue() : max_size(5) {} // 假设队列的最大容量为 5

    void put(int val)
    {
        unique_lock<mutex> lck(mtx);
        // 队列未满时生产者生产 -> 否则等待
        while (que.size() >= max_size)
        {
            cv.wait(lck);
        }
        que.push(val);

        cv.notify_all(); // 生产完通知其它线程
        cout << "生产者生产了:" << val << " 号物品" << endl;
    }

    int get()
    {
        unique_lock<mutex> lck(mtx);
        // 队列非空时消费者消费 -> 否则等待
        while (que.empty())
        {
            cv.wait(lck);
        }
        int val = que.front();
        que.pop();

        cv.notify_all(); // 消费完通知其它线程
        cout << "消费者消费了:" << val << " 号物品" << endl;
        return val;
    }

private:
    queue<int> que;
    int max_size; // 队列的最大容量
};

void producer(Queue* que)
{
    for (int i = 1; i <= 10; ++i)
    {
        que->put(i);
        this_thread::sleep_for(chrono::milliseconds(100));
    }
}

void consumer(Queue* que)
{
    for (int i = 1; i <= 10; ++i)
    {
        que->get();
        this_thread::sleep_for(chrono::milliseconds(100));
    }
}

int main()
{
    Queue que; // 线程共享的队列

    thread t1(producer, &que);
    thread t2(consumer, &que);

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

    return 0;
}
  • cv.notify_all() -> 通知其它在cv上等待的线程
  • 其它在cv上等待的线程一旦收到通知:等待状态 -> 阻塞状态 -> 获取互斥锁 -> 继续执行程序!

无锁队列CAS

  • 若临界区代码段较大,用互斥锁可能会比较"重",不利于效率提升
cpp 复制代码
#include <iostream>
#include <thread>
#include <mutex>
#include <atomic> // 包含原子类型
#include <list>
#include <chrono>
using namespace std;

// volative -> 防止多线程对共享变量进行缓存 -> 访问的都是原始内存的变量
volatile std::atomic_bool isReady = false;
volatile std::atomic_int m_count = 0;

void task()
{
    while (!isReady)
    {
        std::this_thread::yield(); // 线程让出当前时间片,等待下一次调度
    }

    for (int i = 0; i < 100; ++i)
    {
        m_count++;
    }
}

int main()
{
    list <std::thread> tlist;
    for (int i = 0; i < 10; ++i)
    {
        tlist.push_back(std::thread(task));
    }

    std::this_thread::sleep_for(std::chrono::seconds(3));
    isReady = true; // 等待子线程"工作"完后,置true,防止子线程一直调度

    for (std::thread& t : tlist)
    {
        t.join();
    }
    cout << "m_count:" << m_count << endl;

    return 0;
}
  • 多线程缓存可以加快线程运行的效率
  • volatile -> 无需多线程缓存共享变量 -> 某线程修改共享变量后及时反应!

Linux多线程

线程是轻量级的进程(LWP:light weight process),在Linux环境下线程的本质仍是进程。可理解为:进程是资源分配的最小单位,线程是操作系统调度执行的最小单位。

基本函数

cpp 复制代码
#include <pthread.h>

// 返回线程ID的函数
pthread_t pthread_self(void); // ID类型为pthread_t -> 无符号长整型数

/*
    线程创建函数 -> 一旦调用,即可获得一个子线程
    thread -> 传出参数,if 创建成功,则将线程ID存入指针指向的内存
    attr -> 线程属性,一般情况默认NULL即可
    start_routine -> 函数指针,创建出的子线程的处理动作
    arg -> 作为实参传递到start_routine指针指向的函数内部
    线程创建成功则返回0
*/
int pthread_create(pthread_t *thread, const pthread_attrt_t *attr, void *(*start_routine)(void *), void *arg));

/*
    线程退出函数 -> 一旦调用,线程退出
    不会导致虚拟地址空间的释放(主要针对主线程)
    retval -> 线程退出时携带的数据,若不需携带则指定为NULL
*/
void pthread_exit(void *retval);

/*
    线程回收函数 -> 每次只能回收一个子线程
    thread -> 要被回收的子线程的线程ID
    retval -> 传出参数,二级指针,指向一级指针的地址,该地址中存储了pthread_exit()传递出的数据,若不需要,则指定为NULL
    线程回收成功则返回0
*/
int pthread_join(pthread_t thread, void **retval);

/*
    线程分离函数 -> 子线程和主线程分离后,其退出时占用的内核资源由其他进程接管并回收
    分离后主线程中的线程回收函数无法回收子线程资源
    thread -> 要与主线程分离的子线程ID
    线程分离成功则返回0
*/
int pthread_detach(pthread_t thread);

基本用法

cpp 复制代码
// sample.c -> 主线程接收子线程传出的数据
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<pthread.h>

void* callback(void* arg)
{
    printf("子线程:%ld\n", pthread_self()); // 打印子线程的线程ID

    int *a = (int*) arg; // 强制类型转换
    *a = 100;

    pthread_exit(&a); // 线程退出,并将地址传给主线程的pthread_join()函数

    return NULL;
}

int main()
{
    pthread_t tid; // 线程tid

    int a; // 测试变量a

    pthread_create(&tid, NULL, callback, &a); // 创建子线程

    printf("主线程:%ld\n", pthread_self()); // 打印主线程的线程ID

    pthread_join(tid, NULL); // 利用线程回收函数

    printf("a = %d\n", a);

    pthread_exit(NULL); // 线程退出函数
    
    return 0;
}
/*
    linux下执行命令
    gcc sample.c -lpthread -o ./bin/app
    ./bin/app
*/

线程同步的4种方式

cpp 复制代码
// 4种同步方式 -> linux环境
#include <pthread.h>

// 互斥锁 -> 一般情况下,每一个共享资源对应一把互斥锁,锁的个数与线程的个数无关
pthread_mutex_t mutex; // 互斥锁的类型
/*
    互斥锁初始化函数和释放函数
    restrict mutex 和 mutex -> 互斥锁变量的地址
    attr -> 属性,默认指定为NULL
    函数调用成功会返回0
*/
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
int pthread_mutex_destroy(pthread_mutex_t *mutex);
/* 
    pthread_mutex_init(&mutex, NULL);
    ...
    pthread_mutex_destroy(&mutex);
*/
int pthread_mutex_lock(pthread_mutex_t *mutex); // 加锁函数
int pthread_mutex_unlock(pthread_mutex_t *mutex); // 解锁函数

// 读写锁 -> if 所有进程都是读操作,那么读是并行的(但是使用互斥锁,读操作也是串行的)
pthread_rwlock_t rwlock; // 读写锁的类型

// 条件变量 -> 生产者 & 消费者模型,实质:线程阻塞
pthread_cond_t cond; // 条件变量类型
/*
    线程阻塞函数 -> 线程调用则被阻塞,被唤醒后继续执行
    restrict cond -> 条件变量的地址
    restrict mutex -> 互斥锁的地址
    线程唤醒函数 -> 唤醒阻塞在条件变量上的线程,至少一个解除阻塞
    cond -> 条件变量的地址
*/
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
int pthread_cond_signal(pthread_cond_t *cond);

// 信号量函数 -> 生产者 & 消费者
#include<semaphore.h>
sem_t sem; // 信号量类型
/*
    信号量初始化和释放函数
    sem -> 信号量变量的地址
    pshared -> 0:线程同步;非0:进程同步
    value -> 初始化信号量拥有的资源数
*/
int sem_init(sem_t *sem, int pshared, unsigned int value);
int sem_destroy(sem_t *sem);
// wait和post函数
int sem_wait(sem_t *sem); // 检测是否需要等待,若有资源可用,则资源数 --- 1;否则阻塞
int sem_post(sem_t *sem); // 一般工作完使用,资源数 + 1
相关推荐
码农新猿类2 小时前
服务器本地搭建
linux·网络·c++
GOTXX2 小时前
【Qt】Qt Creator开发基础:项目创建、界面解析与核心概念入门
开发语言·数据库·c++·qt·图形渲染·图形化界面·qt新手入门
徐行1103 小时前
C++核心机制-this 指针传递与内存布局分析
开发语言·c++
序属秋秋秋3 小时前
算法基础_数据结构【单链表 + 双链表 + 栈 + 队列 + 单调栈 + 单调队列】
c语言·数据结构·c++·算法
mldl_4 小时前
(个人题解)第十六届蓝桥杯大赛软件赛省赛C/C++ 研究生组
c语言·c++·蓝桥杯
一个小白14 小时前
C++ 用红黑树封装map/set
java·数据库·c++
Lenyiin4 小时前
《 C++ 点滴漫谈: 三十三 》当函数成为参数:解密 C++ 回调函数的全部姿势
c++·回调函数·lenyiin
埜玊5 小时前
C++之 多继承
c++
1024熙6 小时前
【C++】——lambda表达式
开发语言·数据结构·c++·算法·lambda表达式
mahuifa7 小时前
(2)VTK C++开发示例 --- 绘制多面锥体
c++·vtk·cmake·3d开发