线程模块
1 pthread
1.1 为什么用pthread进行多线程开发
linux下编程为什么用pthread而不是C++11的thread类呢
- std::thread也是基于pthread实现的
- c++11的std::thread没有提供读写锁,服务器高并发条件下很多情况读多写少,没有读写锁性能损失很大
- 引用陈硕大佬的一个解释
如果只在 Linux 下编程,那么 C++11 的 thread 的附加值几乎为零(我认为它过度设计了,同时损失了一些功能),你自己把 Pthreads 封装成一个简单好用的线程库只需要两三百行代码,用十几个 pthreads 函数实现 4 个 class:thread、mutex、lock_guard、condvar,而且 RAII 的好处也享受到了。
本节和锁那一节的核心都在于 pthread 这个库
1.2 API
1.2.1 pthread_create
调用 pthread_create()
函数就可以创建一个线程。
它的函数原型如下:
#include <pthread.h> extern int pthread_create (pthread_t *__restrict __newthread, const pthread_attr_t *__restrict __attr, void *(*__start_routine) (void *), void *__restrict __arg)
参数说明:
第一个参数是 pthread_t* 也就是代表线程实体的指针
第二个参数为了设置线程的属性,一般为 NULL
第三个参数是线程运行时的函数,这是个函数指针
第四个参数也是一个指针,它是用来将数据传递进线程的运行函数
1.2.2 pthread_join
int pthread_join(pthread_t thread, void **retval);
pthread_join()函数等待线程指定的线程终止。如果该线程已经终止,那么pthread_join()将立即返回。由thread指定的线程必须是可接合的。
如果retval不是NULL,那么pthread_join()将目标线程的退出状态(即,目标线程提供给pthread_exit(3)的值)复制到retval所指向的位置。如果取消了目标线程,那么将PTHREAD_CANCELED放置在retval所指向的位置。
如果多个线程同时尝试连接同一个线程,结果是未定义的。如果取消了调用pthread_join()的线程,那么目标线程将保持可连接状态(也就是说,它不会被分离)。
如果join()放到主线程最后,就表示主线程要等子线程结束自己才能结束。
1.2.3 detach
detach()称为分离线程函数,使用detach()函数会让线程在后台运行,即说明主线程不会等待子线程运行结束才结束。
总结
- 在一个线程中,开了另一个线程去干另一件事,使用join函数后,原始线程会等待新线程执行结束之后,再去销毁线程对象。
- 这样有什么好处?---->因为它要等到新线程执行完,再销毁,线程对象,这样如果新线程使用了共享变量,等到新线程执行完再销毁这个线程对象,不会产生异常。
- 如果不使用join,使用detch,那么新线程就会与原线程分离,如果原线程先执行完毕,销毁线程对象及局部变量,并且新线程有共享变量或引用之类,这样新线程可能使用的变量,就变成未定义,产生异常或不可预测的错误。
- 所以,当你确定程序没有使用共享变量或引用之类的话,可以使用detch函数,分离线程。
- 但是使用join可能会造成性能损失,因为原始线程要等待新线程的完成,所以有些情况(前提是你知道这种情况,如上)使用detch会更好。
下面用一个代码来示例说明。
#include <stdio.h> #include <stdlib.h> #include <pthread.h> #include <unistd.h> //线程函数 void *test(void *ptr) { for (int i = 0; i < 10; i++) { printf("the pthread running ,count: %d\n", i); sleep(1); } } int main(void) { pthread_t pId; //创建子线程,线程id为pId int ret = pthread_create(&pId, NULL, test, NULL); if (ret != 0) { printf("create pthread error!\n"); exit(1); } for (int i = 0; i < 5; i++) { printf("main thread running ,count : %d\n", i); sleep(1); } printf("main thread will exit when pthread is over\n"); //等待线程pId的完成 pthread_join(pId, NULL); printf("pthread exit\n"); printf("main thread exit\n"); return 0; }
创建了名为 test.c 的文件。
这里还有一个重要的函数 pthread_join(),它的作用是挂起当前的线程,等待指定的线程运行完毕。在示例代码中主线程等待子线程执行完毕后才继续执行后面的代码。
我们现在可以编译然后执行它。
gcc -o test test.c -lpthread ./test
pthread 是一个动态库,编译的时候需要动态链接,不然程序会报错
1.3 实现思路
设计一个线程类,数据成员包含信号量,线程id、线程名、线程执行函数等。每次初始化对象时在析构函数中调用pthread_create()
创建一个线程,析构函数中调用pthread_detach()
将线程置为分离态。同时设计两个static变量储存当前运行线程的指针和名称。
成员变量
//线程类: 互斥量 信号量 禁止拷贝 class Thread : Noncopyable{ public: typedef std::shared_ptr<Thread> ptr; ... .... private: //线程id(int) run()中初始化,用户态的线程ID和内核线程ID不是一个概念 //调试时候需要拿到内核中的ID pid_t m_id = -1; //线程结构(unsigned long), pthread_create()中初始化 pthread_t m_thread = 0; std::function<void()> m_cb; //线程执行函数 std::string m_name; //线程名称 Semaphore m_semaphore; //信号量 };
static变量
//线程局部变量,thread_local是C++11引入的类型符 //存储当前运行线程指针 static thread_local Thread* t_thread = nullptr; //存储当前运行线程的名称 static thread_local std::string t_thread_name = "UNKNOW";
2 接口
2.1 构造函数
原理:在调用pthread_create()线程API的时候,传入参数 this指针 ,就能通过这个指针去访问到类中的成员。否则类成员静态方法中无法使用非静态成员变量。或者,另外写一个普通成员函数,在线程回调函数中去调用那个函数也能解决该问题
//功能:利用pthread库开启运行线程,并且置一个信号量去等待线程完全开启后再退出构造函数 Thread::Thread(std::function<void()> cb, const std::string& name) :m_cb(cb), m_name(name) { if(name.empty()) m_name = "UNKNOW"; //创建线程,这里初始化了m_thread //power:利用pthread库开启运行线程,并且置一个信号量去等待 线程完全开启后 再 退出构造函数 int rt = pthread_create(&m_thread, nullptr, &Thread::run, this); if(rt) { SYLAR_LOG_ERROR(g_logger) << "pthread_create thread fail, rt=" << rt << " name=" << name; throw std::logic_error("pthread_create error"); } //当执行完上面的API线程可能不会马上运行 手动等待一下直到线程完全开始运行 m_semaphore.wait(); }
2.2 析构函数
Thread::~Thread() { if(m_thread) { //析构时候不马上杀死线程 而是将其置为分离态,然后析构 pthread_detach(m_thread); } }
2.3 run()初始化并运行线程
必须为void* 类型的static函数。无类型指针保证线程能够接受任意类型的参数,到时候再强制转换
void* Thread::run(void* arg) { SYLAR_LOG_INFO(g_logger) << "thread::run() begin"; //pthread_create()的时候最后一个参数为run()的参数,构造函数调用时传入的是this指针 //接收传入的this指针 因为是个static方法 因此依靠传入this指针很重要 Thread* thread = (Thread*)arg; //初始化线程局部变量 t_thread = thread; t_thread_name = thread->m_name; //获取内核线程ID thread->m_id = sylar::GetThreadId(); //设置线程的名称,只能接收小于等于16个字符 pthread_setname_np(pthread_self(), thread->m_name.substr(0, 15).c_str()); //防止m_cb中智能指针的引用不被释放 交换一次会减少引用次数 std::function<void()> cb; cb.swap(thread->m_cb); //防止函数有智能指针时,它的引用一直不被释放掉 thread->m_semaphore.notify(); //确保线程已经完全初始化好了 再进行唤醒 cb(); //执行函数 return 0; }
2.4 join()回收线程
join()和析构函数中的detach()一般只会执行一个,他们的区别如第一章节所示。
void Thread::join() { if(m_thread) { int rt = pthread_join(m_thread, nullptr); //以阻塞态的形式等待线程终止 if(rt) { SYLAR_LOG_ERROR(g_logger) << "pthread_join thread fail, rt=" << rt << " name=" << m_name; throw std::logic_error("pthread_join error"); } m_thread = 0; } }