【二】tinyWebServer实战:线程池实战

354 阅读2分钟

pthread

查看当前系统线程库

$ getconf GNU_LIBPTHREAD_VERSION
NPTL 2.17

POSIX Thread Library (NPTL)使Linux内核可以非常有效的运行使用POSIX线程标准写的程序。

在内核2.6以前的调度实体都是进程,内核并没有真正支持线程。它是能过一个系统调用clone()来实现的,这个调用创建了一份调用进程的拷贝,跟fork()不同的是,这份进程拷贝完全共享了调用进程的地址空间。Linux Thread 就是通过这个系统调用来提供线程在内核级的支持的(许多以前的线程实现都完全是在用户态,内核根本不知道线程的存在)。非常不幸的是,这种方法有相当多的地方没有遵循POSIX标准,特别是在信号处理,调度,进程间通信原语等方面。

很显然,为了改进LinuxThread必须得到内核的支持,并且需要重写线程库。为了实现这个需求,开始有两个相互竞争的项目:IBM启动的NGTP(Next Generation POSIX Threads)项目,以及Redhat公司的NPTL。在2003年的年中,IBM放弃了NGTP,也就是大约那时,Redhat发布了最初的NPTL。

NPTL最开始在redhat linux 9里发布,现在从RHEL3起内核2.6起都支持NPTL,并且完全成了GNU C库的一部分。

准备一个文件a.txt待写入。

$ echo -e "This is a.txt\n" >> a.txt
$ cat a.txt 
This is a.txt

$ 

信号量

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>
#include <sys/prctl.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h>
// 保护文件的信号量
sem_t mysem;

// 新的thread要调用的函数
void *writeFile(void *arg)
{
    printf("another thread...\n");
    char *filename = (char *)arg;
    // 设置线程名字
    prctl(PR_SET_NAME,"ANOTHER_THREAD");
    // 等待mysem非零,原子操作减一
    sem_wait(&mysem);
    int fd = open(filename,O_RDWR|O_APPEND);
    // 等待3s,模拟其他无关操作
    // 出现了粒度的概念,如果这三秒用不到fd,就不应该读取那么早
    // 在main函数的while循环中在等待这个被mysem信号量保护的文件
    // 它阻止了main中读取文件。
    sleep(3);
    // 写入一句话
    char *buf="thread1 add something...\n";
    write(fd,buf,strlen(buf));
    printf("fd %d is going to close.\n",fd);
    close(fd);
    // 信号量+1,不为零也就是广而告之:a.txt写其他东西可用了
    sem_post(&mysem);
    // 退出线程,参数为返回给pthread_join的参数
    pthread_exit((void *)"run");
}

int main()
{
    pthread_t another_thread;
    char *fname = "./a.txt";
	// 初始化信号量,设置为1,表示文件可用,sem_wait不会阻塞
    sem_init(&mysem,0,1);
    // 创建线程,参数无,线程进入的函数为writeFile,参数为文件名
    pthread_create(&another_thread,NULL,writeFile,(void *)fname);

    int fd = -1;
    int i = 0;
    int ret;
    while(true)
    {   
        sleep(1);
        //sem_wait(&mysem); 
        // 非阻塞sem_wait,方便我打印一句话提示使用者
        // 而不只是无反应的阻塞
        ret = sem_trywait(&mysem);
        // wait在等待信号量不为0,如果为0的话
        // 返回-1,并且错误变量errno设置为EAGAIN
        if(ret==-1 && errno==EAGAIN)
        {
            printf("main func wait %d sec...\n",++i);
            continue;
        }
        // 文件可用了,写一句话代表进去过
        fd = open(fname,O_RDWR|O_APPEND);
        char *buf="man func add something...\n";
        write(fd,buf,strlen(buf));
        close(fd);
        break;
    }
    puts("main func while loop exit...\n");
    char *msg;
    // 等待线程,得到thread的返回参数
    pthread_join(another_thread,(void **)&msg);
    printf("got thread1: %s\n",msg);
}

打印结果如下:

$ g++ sem_file.cc  -pthread
$ ./a.out 
another thread...
main func wait 1 sec...
main func wait 2 sec...
main func wait 3 sec...
fd 3 is going to close.
main func while loop exit...

got thread1: run
$ cat a.txt
This is a.txt

thread1 add something...
man func add something...

查看线程名(最后一行),果然已经是我们设置的线程名了。

[hqinglau@centos ~]$ ps -Taux | grep a.out
hqinglau 18220  0.0  0.0  23036   864 pts/0    Sl+  22:24   0:00 ./a.out
hqinglau 18233  0.0  0.0 112816   992 pts/1    S+   22:24   0:00 grep --color=auto a.out
[hqinglau@centos ~]$ cat /proc/18220/task/18220/stat
18220 (a.out) S ...
[hqinglau@centos ~]$ cat /proc/18220/task/18221/stat
18221 (ANOTHER_THREAD) S ...

互斥锁和条件变量也差不多是这个用法。下面着重讲下线程池。

线程池

线程池

一个基本的思路就是,维护一个任务队列,然后把任务分配给某一个线程,分配机制可以是轮询或者锁竞争等方式。

下面以锁竞争为例讲解一个实例。

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>
#include <sys/prctl.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h>
#include <assert.h>
#include <string>
#include <iostream>
#include <queue>
using namespace std;


template <typename T>
class ThreadPool
{
public:
    ThreadPool(int n_threads):threadnum(n_threads),stop(false)
    {
        assert(n_threads>0);
        mutex = PTHREAD_MUTEX_INITIALIZER;
        threads = new pthread_t[n_threads];
        for (int i=0;i<n_threads;i++)
        {
            pthread_create(threads+i,NULL,worker,this);
            if (pthread_detach(threads[i]))
            {
                delete[] threads;
                throw std::exception();
            }
        }
        
    }
    ~ThreadPool()
    {
        delete []threads;
    }
    bool addTask(T d)
    {
        pthread_mutex_lock(&mutex);
        taskQueue.push(d);
        pthread_mutex_unlock(&mutex);
    }
private:
    static void *worker(void *arg);
    void loop()
    {
        while(!stop)
        {
            pthread_mutex_lock(&mutex);
            if(taskQueue.empty())
            {
                pthread_mutex_unlock(&mutex);
                continue;
            }
            // 这里一般会是一个接口,也就是一个process函数
            // 这边pop之后,直接调用队列中对象的process函数
            // 这里简单printf一下
            T data = taskQueue.front();
            taskQueue.pop();
            
            //display(data);
            int fd = open("/home/hqinglau/vscodeFile/server/pthread/a.txt",O_RDWR|O_APPEND);
            write(fd,((string)data).c_str(),((string)data).size());
            close(fd);
            pthread_mutex_unlock(&mutex);
        }
    }

private:
    int threadnum;
    pthread_t *threads;
    queue<T> taskQueue;
    pthread_mutex_t mutex;
    bool stop;
};

template <typename T>
void * ThreadPool<T>::worker(void *arg)
{
    ThreadPool *p = (ThreadPool *)arg;
    p->loop();
}

int main()
{
    file_mutex = PTHREAD_MUTEX_INITIALIZER;
    ThreadPool<string> *pool = new ThreadPool<string>(3);
    char buf[10];
    string data;
    for(int i=0;i<10;i++)
    {
        sprintf(buf,"data %d",i);
        data = buf;
        string d=buf;
        pool->addTask(d);
    }
    while(1);
    //delete pool;
    return 0;
}

实现功能:写入文件(没必要,只是为了示例)

[hqinglau@centos pthread]$ ./a.out 
^C
[hqinglau@centos pthread]$ cat a.txt
data 0data 1data 2data 3data 4data 5data 6data 7data 8data 9