线程模块

1 pthread

1.1 为什么用pthread进行多线程开发

linux下编程为什么用pthread而不是C++11的thread类呢

  1. std::thread也是基于pthread实现的
  2. c++11的std::thread没有提供读写锁,服务器高并发条件下很多情况读多写少,没有读写锁性能损失很大
  3. 引用陈硕大佬的一个解释

    如果只在 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;
        }
    }
全部评论

相关推荐

头像
不愿透露姓名的神秘牛友
04-08 00:50
点赞 评论 收藏
转发
1 10 评论
分享
牛客网
牛客企业服务