c++多线程

1. 线程库的使用

创建进程

# include<iostream>
# include<thread> //线程库头文件

using namespace std;

void thread_1()
{
    cout<<"子线程1"<<endl;
}

int thread_2(int x)
{
    cout<<"子线程2"<<endl;
    return x;
}


int main()
{

    //thread函数创建一个新线程
    //first是线程名,括号里第一个参数是函数名,第二个参数是给第一个函数的参数
    thread first(thread_1); 
    thread second(thread_2 , 100);
    
    //若没有下面这两行,程序会报错
    //因为若没有下面这两行程序会一直执行下去。
    //很有可能在主线程return 0后,子线程还没有继续进行
    first.detach();
    second.join();

    return 0;
}

join和detach

first.join(); //主进程会等待子进程first执行完毕后才继续执行
second.detach(); //主进程不会等待子进程,子线程在后台运行

joinable

判断线程是否可以join,返回bool类型

若对一个不能使用join或detach的线程强行使用join或detach会报一个system_error

所以在一个比较严谨的项目中会先判断一下是否可以使用

int main()
{
    thread first(thread_1);
    thread second(thread_2 , 100);
    first.detach();
    //second.detach();
    if(second.joinable()) 
    {
        second.join();
    }
    cout<<"主进程";
    return 0;
}

2. 互斥锁mutex(互斥量)

如下面的代码

当两个线程都对一个全局变量进行操作时候。若a = 3,两个线程恰好在同一时刻执行a++,那么a会等于4而不是我们下意识想到的5

为了避免这个情况,我们使用互斥锁mutex

当线程遇到lock()
若没上锁,则上锁,其他线程无法访问
若已上锁,则等待解锁

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

using namespace std;

int a = 1;
mutex mtx;

void f()
{
    for(int i=1;i<=1000;i++)
    {
        mtx.lock();   //上锁
        a++;
        mtx.unlock();  //解锁
    }
}

int main()
{
    thread t1(f);
    thread t2(f);
    t1.join();t2.join();

    cout<<a;
    return 0;
}

3. 原子操作

原子操作就是: 不可中断的一个或者一系列操作, 也就是不会被线程调度机制打断的操作, 运行期间不会有任何的上下文切换(context switch).

讲第二个例子中的a改成atomic 类型,删去锁操作依旧可以达到相同的目的,实现线程安全

atomic<int> a = 1

void f()
{
    for(int i=1;i<=1000;i++)
    {
        a++;
    }
}

int main()
{
    thread t1(f);
    thread t2(f);
    t1.join();t2.join();

    cout<<a;
    return 0;
}

4. 互斥量死锁

下面的两个线程可能会发生死锁:
f1占有m1资源,f2占有m2资源,然后两个都在等待对方释放资源

mutex m1;
mutex m2;
void f1()
{
    for(int i=1;i<=1000;i++)
    {
        m1.lock();
        m2.lock();
        m1.unlock();
        m2.unlock();
    }
}

void f2()
{
    for(int i=1;i<=1000;i++)
    {
        m2.lock();
        m1.lock();
        m1.unlock();
        m2.unlock();
    }
}

只需要将f2改成下面这样就好了

void f2()
{
    for(int i=1;i<=1000;i++)
    {
        m1.lock();
        m2.lock();
        m1.unlock();
        m2.unlock();
    }
}

5. unique lock

实现自动解锁

int a = 1;
mutex mtx;

void f()
{
    for(int i=1;i<=1000;i++)
    {
        unique_lock<mutex>lg(mtx);  
        a++;
    }
}

int main()
{
    thread t1(f);
    thread t2(f);
    t1.join();t2.join();

    cout<<a;
    return 0;
}

6. 条件变量condition_variable

与互斥量搭配使用,当同一个条件变量执行notify_one或notify_all会被唤醒

condition_variable cv;
mutex mtx;
bool flag;

void wait()
{
    cv.wait(mtx);      //等待被唤醒就好了
    cv.wait(mtx,flag);  //被唤醒后如果flag为false还是会继续阻塞
}

生产者消费者:

# include<iostream>
# include<thread>
# include<mutex>
# include<string>
# include<queue>
# include<vector>
# include<functional>
# include<condition_variable>

std::queue<int> g_queue;  //任务队列
std::mutex mtx;
std::condition_variable g_cv;

void Producer()  
{
    for(int i=0;i<10;i++)  //生产者每次生产10个任务
    {
        std::unique_lock<std::mutex>lock(mtx); //每个任务加入队列前要上锁
        g_queue.push(i);
        std::cout<<"producer : "<<i<<std::endl;
        g_cv.notify_one(); //加入队列后随机让一个消费者进程来取任务
    }
}

void Consumer()
{
    while(1)  //消费者进程是一个死循环,要不断地执行任务
    {
        std::unique_lock<std::mutex>lock(mtx);  //取任务时也要上锁
        g_cv.wait(lock,[](){
          return !g_queue.empty();//若任务队列为空则互斥量被唤醒也要继续等待
        });
        std::cout<<"consumer : " <<g_queue.front()<<std::endl;
        g_queue.pop(); //将任务取出
    }
}
int main()
{
    std::thread t1(Producer) , t2(Consumer);
    t1.join();t2.join(); //因为消费者是一个死循环,所以只需要一个线程就可以完成所有的任务
    return 0;

}

7. 线程池

线程池有线程数组和任务队列
每个线程都去取任务并完成

# include<iostream>
# include<thread>
# include<mutex>
# include<string>
# include<queue>
# include<vector>
# include<functional>
# include<condition_variable>

class ThreadPool{

private:
    std::vector<std::thread> threads;   //线程数组
    std::queue<std::function<void()>> tasks;    //任务队列

    std::mutex mtx;
    std::condition_variable condition;

    bool stop;      //线程池什么时候终止

public:
    ThreadPool(int numThreads) : stop(false)    //构造函数,每个线程池有numThreads个线程,stop初始化为false
    {
        for(int i=0;i<numThreads;i++)   
        {
            threads.emplace_back([this]{        //每个线程都是一个函数,用lambda表达式表示
                while(1)                        
                {
                    std::unique_lock<std::mutex>lock(mtx);  
                    condition.wait(lock , [this]{               //线程什么时候去取任务取决于什么时候往里面加任务
                        return !tasks.empty() || stop;         //唤醒时如果任务队列不为空或者stop为true(线程终止)的时候就继续往下走
                    });

                    if(stop && tasks.empty()) return ;          //如果线程真的终止了并且任务队列为空则该线程终止执行

                    std::function<void()> task(std::move(tasks.front()));  //将任务队列最前面的任务取出来赋值给 function<void()>类型的task
                    tasks.pop();    
                    lock.unlock();  //取到任务后解锁,让其他线程去取任务    
                    task();         //运行该任务
                }
            });
        }
    }

    ~ThreadPool()
    {
        {
            std::unique_lock<std::mutex> lock(mtx);
            stop  = true;   //将线程池终止,因为stop是一个共享内存所以也要上锁
        }
        
        condition.notify_all();  //终止时告诉所有进程,让他们完成任务
        for(auto &t : threads)  //将所有任务队列的任务都完成
        {
            t.join();   
        }
    }

    template<class F , class... Args>
    void enqueue(F&&f , Args&& ...args)
    {
        std::function<void()>task = std::bind(std::forward<F>(f) , std::forward<Args>(args)...);
        {
            std::unique_lock<std::mutex>lock(mtx);
            tasks.emplace(std::move(task));
        }
        condition.notify_one();
    }

};

int main()
{
    ThreadPool pool(5);
    for(int i=0;i<10;i++)
    {
        pool.enqueue([i]{
            std::cout<<"task : "<<i<<" is running"<<std::endl;
            std::cout<<"task : "<<i<<" is done"<<std::endl;
        });
    }
    return 0;
}
posted @ 2024-03-08 14:24  拾墨、  阅读(2)  评论(0编辑  收藏  举报