08_多线程编程

同步和互斥:

  • 互斥:多线程中互斥是指多个线程访问同一资源时同时只允许一个线程对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。
  • 同步:多线程同步是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问。在大多数情况下,同步已经实现了互斥,特别是所有写入资源的情况必定是互斥的。少数情况是指可以允许多个访问者同时访问资源。

一、互斥锁

在多任务操作系统中,同时运行的多个任务可能都需要使用同一种资源。为了同一时刻只允许一个任务访问资源,需要用互斥锁对资源进行保护

互斥锁是一种简单的加锁的方法来控制对共享资源的访问,互斥锁只有两种状态,即上锁( lock )解锁( unlock )

(1)互斥锁操作基本流程

  1. 访问共享资源前,对互斥锁进行加锁;
  2. 完成加锁后访问共享资源;
  3. 对共享资源完成访问后,对互斥锁进行解锁。

对互斥锁进行加锁后,任何其他试图再次对互斥锁加锁的线程将会被阻塞,直到锁被释放。

(2)互斥锁特性

  • 原子性:互斥锁是一个原子操作,操作系统保证如果一个线程锁定了一个互斥锁,那么其他线程在同一时间不会成功锁定这个互斥锁;
  • 唯一性:如果一个线程锁定了一个互斥锁,在它解除锁之前,其他线程不可以锁定这个互斥锁;
  • 非忙等待:如果一个线程已经锁定了一个互斥锁,第二个线程又试图去锁定这个互斥锁,则第二个线程将被挂起且不占用任何CPU资源,直到第一个线程解除对这个互斥锁的锁定为止,第二个线程则被唤醒并继续执行,同时锁定这个互斥锁。

(3)示例

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <string.h>
 
 char *pTestBuf = nullptr; // 全局变量
 
 /* 定义互斥锁 */
pthread_mutex_t mutex;
 
void *ThrTestMutex(void *p)
{
    pthread_mutex_lock(&mutex);     // 加锁
    {
        pTestBuf = (char*)p;
        sleep(1);
    }
    pthread_mutex_unlock(&mutex);   // 解锁
}
 
int main()
{   
    /* 初始化互斥量, 默认属性 */
    pthread_mutex_init(&mutex, NULL);
 
    /* 创建两个线程对共享资源访问 */
    pthread_t tid1, tid2;
    pthread_create(&tid1, NULL, ThrTestMutex, (void *)"Thread1");
    pthread_create(&tid2, NULL, ThrTestMutex, (void *)"Thread2"); 
 
    /* 等待线程结束 */
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL); 
 
    /* 销毁互斥锁 */
    pthread_mutex_destroy(&mutex);  
 
    return 0;
}

二、读写锁

读写锁允许更高的并行性,也叫共享互斥锁;互斥量要么是加锁状态,要么就是解锁状态,而且一次只有一个线程可以对其加锁。

读写锁可以有3种状态:读模式下加锁状态、写模式加锁状态、不加锁状态

一次只有一个线程可以占有写模式的读写锁,但是多个线程可以同时占有读模式的读写锁,即允许多个线程读但只允许一个线程写。

当读操作较多,写操作较少时,可用读写锁提高线程读并发性。

(1)读写锁特性

  1. 如果有线程读数据,则允许其它线程执行读操作,但不允许写操作;
  2. 如果有线程写数据,则其它线程都不允许读、写操作;
  3. 如果某线程申请了读锁,其它线程可以再申请读锁,但不能申请写锁;
  4. 如果某线程申请了写锁,其它线程不能申请读锁,也不能申请写锁;
  5. 读写锁适合于对数据的读次数比写次数多得多的情况。

(2)读写锁创建和销毁

#include <pthread.h>
int phtread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
  • 参数:rwlock:读写锁,attr:读写锁属性
  • 返回值:成功返回0,出错返回错误码

(3)读写锁加锁解锁

#include <pthread.h>
/** 加读锁 */
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
/** 加写锁 */
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
/** 释放锁 */
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
  • 参数:rwlock:读写锁
  • 返回值:成功返回 0;出错,返回错误码

(4)示例

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <string.h>

/* 定义读写锁 */
pthread_rwlock_t rwlock;

/* 定义共享资源变量 */
int g_nNum = 0;

/* 读操作 其他线程允许读操作 不允许写操作 */
void *fun1(void *arg)  
{  
    while(1)  
        {  
            pthread_rwlock_rdlock(&rwlock);  
            {
                printf("read thread 1 == %d\n", g_nNum);
            }      
            pthread_rwlock_unlock(&rwlock);

            sleep(1);
        }
}  

/* 读操作,其他线程允许读操作,不允许写操作 */
void *fun2(void *arg)
{    
    while(1)
        {
            pthread_rwlock_rdlock(&rwlock);  
            {
                printf("read thread 2 == %d\n", g_nNum);
            }      
            pthread_rwlock_unlock(&rwlock);

            sleep(1);
        }
} 

/* 写操作,其它线程都不允许读或写操作 */
void *fun3(void *arg)
{    
    while(1)
        {
            pthread_rwlock_wrlock(&rwlock);
            {
                g_nNum++;        
                printf("write thread 1\n");
            }
            pthread_rwlock_unlock(&rwlock);
            sleep(1);
        }
} 
/* 写操作,其它线程都不允许读或写操作 */ 
void *fun4(void *arg)
{    
    while(1)
        {  
            pthread_rwlock_wrlock(&rwlock);  
            {
                g_nNum++;  
                printf("write thread 2\n");  
            }
            pthread_rwlock_unlock(&rwlock); 

            sleep(1);  
        }  
}  

int main(int arc, char *argv[])  
{  
    pthread_t ThrId1, ThrId2, ThrId3, ThrId4;  

    pthread_rwlock_init(&rwlock, NULL);  // 初始化一个读写锁  

    /* 创建测试线程 */
    pthread_create(&ThrId1, NULL, fun1, NULL);  
    pthread_create(&ThrId2, NULL, fun2, NULL);  
    pthread_create(&ThrId3, NULL, fun3, NULL);  
    pthread_create(&ThrId4, NULL, fun4, NULL);  

    /* 等待线程结束,回收其资源 */
    pthread_join(ThrId1, NULL);  
    pthread_join(ThrId2, NULL);  
    pthread_join(ThrId3, NULL);  
    pthread_join(ThrId4, NULL);  

    pthread_rwlock_destroy(&rwlock);      // 销毁读写锁  

    return 0;  
}

运行结果:

image

三、自旋锁

自旋锁与互斥锁功能相同,唯一不同的就是互斥锁阻塞后休眠不占用CPU,而自旋锁阻塞后不会让出CPU,会一直忙等待,直到得到锁;

自旋锁在用户态较少用,而在内核态使用的比较多;

自旋锁的使用场景:锁的持有时间比较短,或者说小于2次上下文切换的时间;

(1)自旋锁函数

linux中的自旋锁用结构体spinlock_t 表示,定义在include/linux/spinlock_type.h。

自旋锁的接口函数全部定义在include/linux/spinlock.h头文件中,实际使用时只需include<linux/spinlock.h>即可。

(2)示例

include<linux/spinlock.h>
spinlock_t lock;      //定义自旋锁
spin_lock_init(&lock);   //初始化自旋锁
spin_lock(&lock);      //获得锁,如果没获得成功则一直等待
{
    .......         //处理临界资源
}
spin_unlock(&lock);     //释放自旋锁

四、条件变量

条件变量用来阻塞一个线程,直到条件发生;

通常条件变量和互斥锁同时使用;

条件变量使线程可以睡眠等待某种条件满足;

条件变量是利用线程间共享的全局变量进行同步的一种机制。

条件变量的逻辑:一个线程挂起去等待条件变量的条件成立,而另一个线程使条件成立。

(1)原理

线程在改变条件状态之前先锁住互斥量。如果条件为假,线程自动阻塞,并释放等待状态改变的互斥锁。如果另一个线程改变了条件,它发信号给关联的条件变量,唤醒一个或多个等待它的线程。如果两进程共享可读写的内存,条件变量可以被用来实现这两进程间的线程同步。

(2)示例

#include <stdio.h>  
#include <stdlib.h>  
#include <unistd.h>  
#include <pthread.h> 

pthread_cond_t taxicond = PTHREAD_COND_INITIALIZER;  
pthread_mutex_t taximutex = PTHREAD_MUTEX_INITIALIZER;  

void *ThrFun1(void *name)  
{  
    char *p = (char *)name;  

    // 加锁,把信号量加入队列,释放信号量
    pthread_mutex_lock(&taximutex); 
    {
        pthread_cond_wait(&taxicond, &taximutex);  
    } 
    pthread_mutex_unlock(&taximutex);  

    printf ("ThrFun1: %s now got a signal!\n", p);  
    pthread_exit(NULL);  
}  

void *ThrFun2(void *name)  
{  
    char *p = (char *)name;  
    printf ("ThrFun2: %s cond signal.\n", p);    // 发信号
    pthread_cond_signal(&taxicond);  
    pthread_exit(NULL);  
}  

int main (int argc, char **argv)  
{  
    pthread_t Thread1, Thread2;  
    pthread_attr_t threadattr;
    pthread_attr_init(&threadattr);  // 线程属性初始化

    // 创建三个线程 
    pthread_create(&Thread1, &threadattr, ThrFun1, (void *)"Thread1");  
    sleep(1);  

    pthread_create(&Thread2, &threadattr, ThrFun2, (void *)"Thread2");  
    sleep(1);   

    pthread_join(Thread1, NULL);
    pthread_join(Thread2, NULL);

    return 0;  
}

运行结果:

ThrFun2 cond signal

五、线程池

在计算机体系结构中有许多池式结构:内存池、数据库连接池、请求池、消息队列、对象池等;池式结构解决的主要问题为缓冲问题,起到的是缓冲区的作用。

线程池解决的问题:

  • 解决任务处理
  • 阻塞IO
  • 解决线程创建于销毁的成本问题
  • 管理线程

线程池应用之一:日志存储

线程池的主要作用:异步解耦

在服务器保存日志至磁盘上时,性能往往压在磁盘读写上,而引入线程池利用异步解耦可以解决磁盘读写性能问题。

(1)线程池框架

线程池中比较关键的参数有:

  • 工作队列
  • 任务队列
  • 线程池的池
  • pthread_create中的回调函数

线程池的框架如下所示:

image

如图所示,将整个框架以及任务添加接口定义为线程池的“池”,那么在这个池子中重要的就是工作队列任务队列、以及决定工作队列中的thread到底应该工作还是休息的回调函数。

(2)工作队列

定义worker结构体:worker中要有create_pthread函数的id参数,还需要有控制每一个worker live or die的标志terminate,我们最好再设置一个标志表示这个worker是否在工作。最后,我们要知道这个worker隶属于那个线程池。

struct NWORKER{
    pthread_t threadid;		//线程id
    bool terminate;			//是否需要结束该worker的标志
    int isWorking;			//该worker是否在工作
    ThreadPool *pool;		//隶属于的线程池
}

(3)任务队列

一个任务就是一个函数:

struct NJOB{
        void (*func)(void *arg);     //任务函数
        void *user_data;			 //函数参数
    };

(4)线程池

工作队列中的每个worker都在等待一个任务队列看其是否有任务到来,所以很容易得出结论我们必须要在线程池中实现两把锁:一把是用来控制对任务队列操作的互斥锁,另一把是当任务队列有新任务时唤醒worker的条件锁。

有了这两把锁,线程池中再加点必要的一些数字以及对线程池操作的函数,那么这个类就写完了。实现代码如下:

class ThreadPool{
private:
    struct NWORKER{
        pthread_t threadid;
        bool terminate;
        int isWorking;
        ThreadPool *pool;
    } *m_workers;

    struct NJOB{
        void (*func)(void *arg);     //任务函数
        void *user_data;
    };
public:
    //线程池初始化
    //numWorkers:线程数量
    ThreadPool(int numWorkers, int max_jobs);
    //销毁线程池
    ~ThreadPool();
    //面向用户的添加任务
    int pushJob(void (*func)(void *data), void *arg, int len);

private:
    //向线程池中添加任务
    bool _addJob(NJOB* job);
    //回调函数
    static void* _run(void *arg);
    void _threadLoop(void *arg);

private:
    std::list<NJOB*> m_jobs_list;
    int m_max_jobs;							//任务队列中的最大任务数
    int m_sum_thread;						//worker总数
    int m_free_thread;						//空闲worker数
    pthread_cond_t m_jobs_cond;           //线程条件等待
    pthread_mutex_t m_jobs_mutex;         //为任务加锁防止一个任务被两个线程执行等其他情况
};

函数本身:

//run为static函数
void* ThreadPool::_run(void *arg) {
    NWORKER *worker = (NWORKER *)arg;
    worker->pool->_threadLoop(arg);
}

至于threadLoop的实现,由于线程是要一直存在的,一个while(true)的循环肯定少不了了。这个循环中具体做什么,不断检查任务队列中是否有job:

  1. 如果有,则取出这个job,并将该job从任务队列中删除,且执行job中的func函数;
  2. 如果没有,调用pthread_cond_wait函数等待job到来时被唤醒;
  3. 若当前worker的terminate为真,则退出循环结束线程。

对job操作前别忘了加锁,函数实现如下:

void ThreadPool::_threadLoop(void *arg) {
    NWORKER *worker = (NWORKER*)arg;
    while (1){
        //线程只有两个状态:执行\等待
        //查看任务队列前先获取锁
        pthread_mutex_lock(&m_jobs_mutex);
        //当前没有任务
        while (m_jobs_list.size() == 0) {
        	//检查worker是否需要结束生命
            if (worker->terminate) break;
            //条件等待直到被唤醒
            pthread_cond_wait(&m_jobs_cond,&m_jobs_mutex);
        }
        //检查worker是否需要结束生命
        if (worker->terminate){
            pthread_mutex_unlock(&m_jobs_mutex);
            break;
        }
        //获取到job后将该job从任务队列移出,免得其他worker过来重复做这个任务
        struct NJOB *job = m_jobs_list.front();
        m_jobs_list.pop_front();
		//对任务队列的操作结束,释放锁
        pthread_mutex_unlock(&m_jobs_mutex);

        m_free_thread--;
        worker->isWorking = true;
        //执行job中的func
        job->func(job->user_data);
        worker->isWorking = false;

        free(job->user_data);
        free(job);
    }

    free(worker);
    pthread_exit(NULL);
}

添加任务:

bool ThreadPool::_addJob(NJOB *job) {
	//尝试获取锁
    pthread_mutex_lock(&m_jobs_mutex);
    //判断队列是否超过任务数量上限
    if (m_jobs_list.size() >= m_max_jobs){
        pthread_mutex_unlock(&m_jobs_mutex);
        return false;
    }
    //向任务队列添加job
    m_jobs_list.push_back(job);
    //唤醒休眠的线程
    pthread_cond_signal(&m_jobs_cond);
    //释放锁
    pthread_mutex_unlock(&m_jobs_mutex);
	return true;
}

面向用户的添加任务:

封装一层面向用户的添加任务函数,一来方便线程池的使用者,二来也能隐藏内部实现

//面向用户的添加任务
int ThreadPool::pushJob(void (*func)(void *), void *arg, int len) {
    struct NJOB *job = (struct NJOB*)malloc(sizeof(struct NJOB));
    if (job == NULL){
        perror("malloc");
        return -2;
    }

    memset(job, 0, sizeof(struct NJOB));

    job->user_data = malloc(len);
    memcpy(job->user_data, arg, len);
    job->func = func;

    _addJob(job);

    return 1;
}

构造函数:

构造函数所做的工作就是根据用户传入的参数创建线程,并且初始化一些属性值。值得注意的是,我们最好在创建完线程后,调用pthread_detach函数,这样能让我们的worker都能很好的结束一切:

ThreadPool::ThreadPool(int numWorkers, int max_jobs = 10) : m_sum_thread(numWorkers), m_free_thread(numWorkers), m_max_jobs(max_jobs){   //numWorkers:线程数量
    if (numWorkers < 1 || max_jobs < 1){
        perror("workers num error");
    }
    //初始化jobs_cond
    if (pthread_cond_init(&m_jobs_cond, NULL) != 0)
        perror("init m_jobs_cond fail\n");

    //初始化jobs_mutex
    if (pthread_mutex_init(&m_jobs_mutex, NULL) != 0)
        perror("init m_jobs_mutex fail\n");

    //初始化workers
    m_workers = new NWORKER[numWorkers];
    if (!m_workers){
        perror("create workers failed!\n");
    }
	//初始化每个worker
    for (int i = 0; i < numWorkers; ++i){
        m_workers[i].pool = this;
        int ret = pthread_create(&(m_workers[i].threadid), NULL, _run, &m_workers[i]);
        if (ret){
            delete[] m_workers;
            perror("create worker fail\n");
        }
        if (pthread_detach(m_workers[i].threadid)){
            delete[] m_workers;
            perror("detach worder fail\n");
        }
        m_workers[i].terminate = 0;
    }
}

析构函数:

析构函数无非就是做释放资源的事情,注意,由于我们detach了我们创造的线程,所以我们必须手动唤醒所有在条件等待的线程,并将worker的terminate值置为true:

ThreadPool::~ThreadPool(){
	//terminate值置1
    for (int i = 0; i < m_sum_thread; i++){
        m_workers[i].terminate = 1;
    }
    //广播唤醒所有线程
    pthread_mutex_lock(&m_jobs_mutex);
    pthread_cond_broadcast(&m_jobs_cond);
    pthread_mutex_unlock(&m_jobs_mutex);
    delete[] m_workers;
}

测试:

#include "threadpool.hpp"

void testFun(void* arg){
    printf("i = %d\n", *(int *)arg);
}


int main(){
    ThreadPool *pool = new ThreadPool(1000, 2000);

    printf("线程池初始化成功\n");
    int i = 0;
    for (i = 0; i < 1000; ++i) {
        pool->pushJob(testFun, &i, sizeof(int));
    }
}

 

posted @ 2025-09-25 11:26  碧蓝i之海  阅读(15)  评论(0)    收藏  举报