Skip to content

Files

Latest commit

7e49207 · Apr 5, 2023

History

History
executable file
·
627 lines (494 loc) · 12.5 KB

c++11多线程.md

File metadata and controls

executable file
·
627 lines (494 loc) · 12.5 KB

c++11 多线程

目录


hello_world

  • 一个非常简单的hello world级别的例子。
#include <thread>
using namespace std;

int func()
{
    cout<<"int func(int)"<<endl;
    return 100;
}

int main()
{
    std::thread t(func);
    t.join();
    return 0;
}

# g++ main.cc -std=c++11 -lpthread
  • 带参数的方式启动线程
int func(int val, string str)
{
    cout<<"int func(int) , str : "<<str<<endl;
    sleep(100);
    return 100;
}

int main()
{
    string str = "hello";
    std::thread t(func,100,str);
    
    t.join();   //join 表示主线程在这个地方等待和回收子线程t1
}

int main()
{
    string str = "hello";
    std::thread t(func,100,str);
    t.detach();

    // 一旦选择了detach模式,就不会执行joinable
    if(t.joinable())
    {
        t.join();
    }
}
  • 获得返回值
void func(int & res)
{
    res = 100;
}

int main()
{
    int res = 0;
    thread t1(func,ref(res));

    sleep(1);            //主线程等待一下子线程的计算
    cout<<res<<endl;

    t1.join();
}
  • 通过调用类型构造,将带有函数调用符类型的实例传入
class background
{
public:
	void operator()()  //这个是重载了() 的运算符,调用的时候是 b() 
	{
		cout << "operator ()" << endl;
	}
};

int main()
{
	background b;           //b()调用operator()
	std::thread t1(b); 
    // std::thread t1((background())); // 或写成这样
	t1.join();

    return 0;
}
  • 给线程参数传递引用的

ref()

// 其他
void func(string & msg)
{
	cout << msg << endl;
}

string str = "hello world";
thread t1(func , str);      //虽然传递的是引用,但是一个线程修改,另一个线程是看不见的

thread t1(func , ref(str));  //修改成这样就可以了
  • 并发度
cout << thread::hardware_concurrency() << endl;
  • 观察多线程的命令:
ps -T -p 进程pid

pidstat -t -p 进程pid -u 1

top -H -p pid
  • gdb调试
1. 查看线程
info threads

2. 跳转到某个线程
thread 2/3/4

3. 
set scheduler-locking off|on|step

估计是实际使用过多线程调试的人都可以发现,在使用step或者continue命令调试当前被调试线程的时候,其他线程也是同时执行的,怎么只让被调试程序执行呢?
通过这个命令就可以实现这个需求。
off 不锁定任何线程,也就是所有线程都执行,这是默认值。 
on 只有当前被调试程序会执行。 
step 在单步的时候,除了next过一个函数的情况(熟悉情况的人可能知道,这其实是一个设置断点然后continue的行为)以外,只有当前线程会执行。

数据竞争和资源互斥

  • 使用mutex,作为资源互斥。

    #include <thread>
    #include <string>
    #include <mutex>
    using namespace std;
    
    mutex mu;
    
    void share_print(string str, int id)
    {
        mu.lock();                  // lock , 这样确保cout的这段代码不被打断
        cout <<"str "<< str << "  " << id << endl;
        mu.unlock();
    }
    
    void thread_func1()
    {
        for (int i = 0; i < 10; i++)
        {
            share_print("thread t1", i);
        }
    }
    
    void thread_func2()
    {
        for (int i = 0; i < 10; i++)
        {
            share_print("thread t2", i);
        }
    }
    
    
    int main()
    {
        thread t1(thread_func1);
        thread t2(thread_func2);
    
        t1.join();
        t2.join();
    }
    
  • 使用guard

    这样写的风格更加符合软件工程的要求

    void share_print(string str, int id)
    {
        lock_guard<mutex> guard(mu);
        cout <<"str "<< str << "  " << id << endl;
    }
    

    但是,可以顺着这个思路去优化。比如,我们希望cout的资源,永远是线程安全的,这个可以设计一个类,把cout封装起来。让它以后使用的时候,就不必要考虑是否加锁了。在这个类的内存设计加锁的代码。

  • 使用类封装希望线程安全的资源

    #include <string>
    #include <mutex>
    #include <fstream>
    using namespace std;
    
    // 使用类封装线程安全的资源
    class LogFile
    {
    private:
        mutex mu;
        ofstream f;
    public:
        LogFile()
        {
            f.open("log.txt");
        }
        void shared_print(string str, int id)
        {
            lock_guard<mutex> locker(mu);
            f << "thread " << id << "  str " << str << endl;
        }
    };
    
    void thread_func1(LogFile & logfile)
    {
        for (int i = 0; i < 10; i++)
        {
            logfile.shared_print("thread t1", i);
        }
    }
    
    void thread_func2(LogFile & logfile)
    {
        for (int i = 0; i < 10; i++)
        {
            logfile.shared_print("thread t2", i);
        }
    }
    
    int main()
    {
        LogFile logfile;
        thread t1(thread_func1,ref(logfile));   //这个ref很重要,表示传递的是引用
        thread t2(thread_func2,ref(logfile));
        t1.join();
        t2.join();
        return 0;
    }
    

死锁

死锁出现的情况就是操作系统课本上面讲的基本那些东西。

还有一个问题就是,如何发现死锁?

可以attach或gdb core文件,查看bt,函数调用堆栈。如果发现,阻塞在lock的地方,就基本上是可以发生程序死锁了。

once_lock

条件变量

如果不使用条件变量的话,只是用锁,会写出这样很低效率的代码。

deque<int> q;
mutex mu;

void thread_func1()
{
	int count = 10;
	while (count > 0)
	{
		mu.lock();
		q.push_front(count);                //互斥区
        cout<<"t1 push a value from t1 "<<count<<endl;
		mu.unlock();
        sleep(1);
        count--;
	}
}

void thread_func2()
{
	int data = 0;
	while (data != 1)
	{
		mu.lock();
		if(!q.empty())
        {
            data = q.back();
            q.pop_back();

            mu.unlock();
            cout<<"t2 got a value from t2 "<<data<<endl;
        }
        else
        {
            cout<<"proc"<<endl;
            mu.unlock();
        }
	}
}


int main()
{
	thread t1(thread_func1);
	thread t2(thread_func2);

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

	return 0;
}

加入条件变量之后,可以避免很多的无效的循环

#include <condition_variable>
#include <deque>
using namespace std;

deque<int> q;
condition_variable cond;
mutex mu;

void thread_func1()
{
	int count = 10;
	while (count > 0)
	{
		mu.lock();
		q.push_front(count);                //互斥区
        cout<<"t1 push a value from t1 "<<count<<endl;
		mu.unlock();
        cond.notify_one();

        sleep(1);
        count--;
	}
}

void thread_func2()
{
	int data = 0;
	while (data != 1)
	{
		unique_lock<mutex> locker(mu);
        cond.wait(locker);          // cond.wait(locker,[]() {return !q.empty(); });  //防止被为唤醒,只有满足 !q.empty(); 的时候,才会被唤醒v
		// if(!q.empty())           //这些是可以去掉的代码,因为此时q一定不是空的,因为wait是被生产者唤醒的
        // {
            data = q.back();
            q.pop_back();
            mu.unlock();
            cout<<"t2 got a value from t2 "<<data<<endl;
        // }
        // else
        // {
        //     cout<<"proc"<<endl;
        //     mu.unlock();
        // }
	}
}


int main()
{
	thread t1(thread_func1);
	thread t2(thread_func2);

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

	return 0;
}

future

如果不使用future,如果获得子线程的返回值?

  • 方式1:

    可以将返回值存放在引用里面,但是但是另外一个问题是,并不知道什么时候,可以计算出结果。

    void cal_fib(int n,int &result)
    {
        int arr[n];
        arr[0] = 0;
        arr[1] = 1;
    
        for(int i=2;i<=n;i++)
        {
            arr[i]=arr[i-1]+arr[i-2];
            usleep(40000);
        }
    
        int res = arr[n];
        result = res;
    }
    
    
    int main()
    {
        int n = 30;
        int result = 0;
        thread t1(cal_fib,n,ref(result));   // 讲返回值保存在引用里面,但是另外一个问题是,并不知道什么时候,可以计算出结果
    
        for(int i=0;i<5;i++)
        {
            cout<<result<<endl;
            sleep(1);
        }
    
        t1.join();
        return 0;
    }
    
  • 方式2:

    使用condition避免无效的等待。

    #include <condition_variable>
    using namespace std;
    
    condition_variable cond;
    mutex mu;
    void cal_fib(int n,int &result)
    {
        int arr[n];
        arr[0] = 0;
        arr[1] = 1;
    
        for(int i=2;i<=n;i++)
        {
            arr[i]=arr[i-1]+arr[i-2];
            usleep(40000);
        }
    
        mu.lock();
        int res = arr[n];
        result = res;
        mu.unlock();
        cond.notify_one();
    }
    
    
    int main()
    {
        int n = 30;
        int result = 0;
        thread t1(cal_fib,n,ref(result)); 
    
        unique_lock<mutex> locker(mu);
        cond.wait(locker);    // void wait(std::unqiue_lock<std::mutex> & lock);   condition_variable::wait 定义
        cout<<result<<endl;
    
        // for(int i=0;i<5;i++)
        // {
        //     cout<<result<<endl;
        //     sleep(1);
        // }
    
        t1.join();
        return 0;
    }
    
  • future 终于登场了。future这里体现出了异步的感觉。

#include <future>

int cal_fib(int n)
{
    int arr[n];
    arr[0] = 0;
    arr[1] = 1;
    for(int i=2;i<=n;i++)
    {
        arr[i]=arr[i-1]+arr[i-2];
        usleep(50000);
    }
    return arr[n];
}


int main()
{

    int n = 20;
    future<int> fu = async(cal_fib,n);
    int result = fu.get();     //主线程会阻塞在这里,直到计算出结果

    cout<<result<<endl;

	return 0;
}
  • promise

    上面提到了future,这是一个未来的值。如果获取的时候,通过 fu.get() 来计算出来。

    promise 是保证我未来会给一个值,然后通过future现在拿到这个值。 举一个简单的例子:

    int cal_fib(int n, promise<int> &p)
    {
        int arr[n];
        arr[0] = 0;
        arr[1] = 1;
        for(int i=2;i<=n;i++)
        {
            arr[i]=arr[i-1]+arr[i-2];
            usleep(50000);
        }
        p.set_value(arr[n]);            // 设置一个值
        return 0;
    }
    
    int main()
    {
        promise<int> p;                            // 保证未来会传递一个值 
        future<int> futureObj = p.get_future();
        future<int> fu = async(cal_fib, 30, ref(p)); 
    
        cout << "wait result ... " << endl;
        int result = futureObj.get();              
        cout<<result<<endl;
        return 0;
    }
    
  • packaged_task

  • 信号量 wait_until

    condition_variable::wait_until 判断信号量是否等到了某个时间

    std::condition_variable cv;
    bool done;
    std::mutex m;
    
    bool wait_loop()
    {
        std::cout << "enter wait_loop ... " << std::endl;
        auto const timeout= std::chrono::steady_clock::now()+
        std::chrono::milliseconds(5000);
        std::unique_lock<std::mutex> lk(m);
        while(!done)
        {
            std::cout << "enter while ... " << std::endl;
            if(cv.wait_until(lk,timeout)==std::cv_status::timeout)
            {
                std::cout << "break" << std::endl;
                break;
            }
        }
        return done;
    }
    
  • 内存模型

    c++11中引入了内存模型的相关操作,本质上是对两个问题的解决。

    1. cpu cache 多个核心修改不可见的问题

    2. reorder 编译器交换代码顺序的问题

    这两个问题导致一些在单线程环境下视乎不可能出现的问题,成为了可能。

    写的很好的一篇文章 :

    https://www.codedump.info/post/20191214-cxx11-memory-model-1/

    https://www.codedump.info/post/20191214-cxx11-memory-model-2/#acquire-release

    防止走丢,保存了一个pdf的快照:

    pdf1

    pdf2


参考