线程

进程是资源分配的基本单位,线程是调度的基本单位。进程有自己独立的地址空间,而多个线程共用同一个地址空间,因此线程间通信比较简单。虚拟地址空间的生命周期与主线程一致,与子线程无关。当子线程退出,需要回收子进程数据时,不能利用子线程栈,可以使用全局数据区、堆区、主线程栈回收。

  • 线程的私有资源:线程栈、寄存器等。
  • 线程的共享资源:代码段,堆区,全局数据区,文件描述符表等。

C线程库

1. 创建线程

每一个线程都有一个唯一的线程ID,类型为pthread_t,是一个无符号长整形数。

int pthread_create(
    pthread_t *thread,              // 传出参数,被创建线程的ID
    const pthread_attr_t *attr,     // 线程属性,一般情况下使用默认属性,使用NULL
    void *(*start_routine)(void *), // 函数指针,创建出的子线程执行的函数
    void *arg                       // 作为实参传递给函数指针指向的函数
);
// 返回值:线程创建成功返回0,创建失败返回对应的错误号

注意:函数的第三个参数的类型为函数指针,指向的线程的处理函数,其参数类型为(void *)。若线程函数为类的成员函数,则this指针会作为默认的参数被传进函数中,从而和线程函数的参数(void*)不能匹配,不能通过编译。由于类的静态成员函数没有this指针,不会出现问题。

2. 退出线程

让线程退出,但不释放虚拟地址空间,一般情况下针对于主线程。

void pthread_exit(
    void *retval    // 线程退出携带的数据,当前子线程的主线程会得到该数据
);

3. 回收线程

如果子线程在运行,调用该函数的线程就会阻塞,子线程退出后,函数解除阻塞进行资源回收。函数被调用一次,只能回收一个子线程,如果有多个子线程则需要循环进行回收。

int pthread_join(
    pthread_t thread, // 要被回收的子线程ID
    void **retval     // 传出参数,存储pthread_exit函数传递出的数据
);
// 返回值:线程回收成功返回0,回收失败返回错误号

4. 线程分离

一般情况下,程序中的主线程有其他任务,如果让主线程负责子线程的资源回收, 只要子线程不退出主线程就会一直被阻塞。子线程与主线程分离之后,子线程退出后其占用的内核资源就被系统的其他进程接管并回收了。

int pthread_detach(
	pthread_t thread	// 子线程ID
);

5. 线程取消

在线程A调用线程取消函数,指定杀死线程 B,这时候线程B仍在运行,直到在线程B中执行了系统调用(从用户区切换到内核区)。

int pthread_cancel(
	pthread_t thread	// 子线程ID
);
// 返回值:函数调用成功返 0,调用失败返回错误号

线程同步

1. 互斥锁

一个互斥锁变量只能被一个线程加锁,也只能被该线程解锁。被锁定之后其他线程再对互斥锁变量加锁就会被阻塞,直到这把互斥锁解锁,被阻塞的线程才能被解除阻塞。

// 互斥锁
pthread_mutex_t mutex;

初始化与释放互斥锁

// 初始化互斥锁
int pthread_mutex_init(
	pthread_mutex_t *restrict mutex,	// 互斥锁变量的地址
    const pthread_mutexattr_t *restrict attr	// 互斥锁的属性,一般使用默认属性,指定为NULL
);
// 释放互斥锁资源            
int pthread_mutex_destroy(
	pthread_mutex_t *mutex	// 互斥锁变量的地址
);

加锁与解锁

// 加锁
int pthread_mutex_lock(
	pthread_mutex_t *mutex	// 互斥锁变量的地址 
);
// 解锁
int pthread_mutex_unlock(
	pthread_mutex_t *mutex // 互斥锁变量的地址 
);

互斥锁封装类

#include <pthread.h>
#include <exception>

// 互斥锁
class locker
{
public:
    // 初始化互斥锁
    locker()
    {
        if (pthread_mutex_init(&m_mutex, NULL) != 0)
        {
            throw std::exception();
        }
    }
    // 释放互斥锁资源
    ~locker()
    {
        pthread_mutex_destroy(&m_mutex);
    }
    // 加锁
    // 若锁没有被锁定,此线程加锁成功,锁中记录该线程加锁成功。
    // 若锁被锁定,其他线程会加锁失败,都会阻塞在这把锁上。
    // 锁被解开后,阻塞在锁上的线程就解除阻塞,并且通过竞争的方式对锁进行加锁,没抢到锁的线程继续阻塞。
    bool lock()
    {
        return pthread_mutex_lock(&m_mutex) == 0;
    }
    // 解锁
    // 哪个线程加的锁,哪个线程才能解锁!
    bool unlock()
    {
        return pthread_mutex_unlock(&m_mutex) == 0;
    }
    // 获取互斥锁地址
    pthread_mutex_t *get()
    {
        return &m_mutex;
    }
private:
    // 互斥锁
    pthread_mutex_t m_mutex;
};

2. 信号量

信号量不一定是锁定某一个资源,也可以是流程上的概念。一个线程完成了某一个动作就通过信号量通知别的线程,别的线程再进行某些动作。信号量主要阻塞线程,不能保证线程安全,需要信号量和互斥锁一起使用。

// 信号量
#include <semaphore.h>
sem_t sem;

初始化与释放信号量

// 初始化信号量
int sem_init(	
	sem_t *sem, 	// 信号量变量地址
	int pshared, 	// 0:线程同步;非0:进程同步
	unsigned int value	// 信号量拥有的资源数
);
// 释放信号量资源
int sem_destroy(
	sem_t *sem	// 信号量变量地址
);

P/V操作

当线程执行P操作,如果信号量中的资源数大于0,线程不会阻塞,资源数-1;如果信号量中的资源数为0,线程被阻塞。

// P操作
int sem_wait(
	sem_t *sem	// 信号量变量地址
);

当线程执行V操作,如果当前有线程被阻塞,则解除阻塞资源数+1,否则直接+1。

// V操作
int sem_post(
	sem_t *sem	// 信号量变量地址
);

查看资源数

int sem_getvalue(
	sem_t *sem,		// 信号量变量地址
	int *sval		// 传出参数,传出资源数目
);

信号量封装类

#include <semaphore.h>
#include <exception>

// 信号量
class sem
{
public:
    // 初始化信号量
    sem()
    {
        // 用于线程同步,当前信号量拥有的资源数为0
        if (sem_init(&m_sem, 0, 0) != 0)
        {
            throw std::exception();
        }
    }
    // 初始化信号量
    sem(int num)
    {
        // 用于线程同步,当前信号量拥有的资源数为num
        if (sem_init(&m_sem, 0, num) != 0)
        {
            throw std::exception();
        }
    }
    // 释放信号量资源
    ~sem()
    {
        sem_destroy(&m_sem);
    }
    // 调用该函数会将sem中的资源数-1。
    // 若信号量中的资源数>0,线程不会阻塞。
    // 若信号量中的资源数减为0,资源被耗尽,线程被阻塞。
    bool wait()
    {
        return sem_wait(&m_sem) == 0;
    }
    // 调用该函数会将sem中的资源数+1。
    // 若有线程调用wait函数被阻塞,这时这些线程会解除阻塞,继续向下运行。
    bool post()
    {
        return sem_post(&m_sem) == 0;
    }

private:
    // 信号量
    sem_t m_sem;
};

可能造成死锁的情况

以生产者-消费者模型为例,以信号量与互斥锁共同实现线程同步。初始状态下,消费者线程没有信号量资源,假设某一个消费者线程先运行,调用先对互斥锁加锁,然后执行P操作,由于没有信号量资源,因此线程被阻塞。而其余的生产者线程与消费者线程由于没有抢到互斥锁,因此被阻塞在互斥锁上。到此,其余线程都被阻塞,程序产生了死锁。所以,实现互斥的P操作一定要在实现同步的P操作之后!

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

// 链表的节点
struct Node
{
    int number;
    struct Node *next;
};

// 生产者线程信号量
sem_t psem;
// 消费者线程信号量
sem_t csem;

// 互斥锁变量
pthread_mutex_t mutex;
// 指向头结点的指针
struct Node *head = NULL;

// 生产者的回调函数
void *producer(void *arg)
{
    // 一直生产
    while (1)
    {
        // 生产者P操作
        sem_wait(&psem);
        /************ 加锁 ************/
        pthread_mutex_lock(&mutex);

        // 创建一个链表的新节点
        struct Node *pnew = (struct Node *)malloc(sizeof(struct Node));
        // 节点初始化
        pnew->number = rand() % 1000;
        // 添加到链表的头部
        pnew->next = head;
        // head指针前移
        head = pnew;
        printf("【Producer】number = %d, tid = %ld\n", pnew->number, pthread_self());

        /************ 解锁 ************/
        pthread_mutex_unlock(&mutex);

        // 消费者V操作
        sem_post(&csem);

        // 生产慢一点
        sleep(rand() % 3);
    }
    return NULL;
}

// 消费者的回调函数
void *consumer(void *arg)
{
    while (1)
    {
        // 消费者P操作
        sem_wait(&csem);
        /************ 加锁 ************/
        pthread_mutex_lock(&mutex);
        struct Node *pnode = head;
        printf("【Consumer】number: %d, tid = %ld\n", pnode->number, pthread_self());
        head = pnode->next;
        // 取出链表的头结点, 将其删除
        free(pnode);
        /************ 解锁 ************/
        pthread_mutex_unlock(&mutex);
        // 生产者V操作
        sem_post(&psem);

        sleep(rand() % 3);
    }
    return NULL;
}

int main()
{
    // 初始化信号量
    sem_init(&psem, 0, 5); // 生成者线程一共有5个信号量
    sem_init(&csem, 0, 0); // 消费者线程一共有0个信号量
    // 初始化互斥锁
    pthread_mutex_init(&mutex, NULL);

    // 创建5个生产者, 5个消费者
    pthread_t ptid[5];
    pthread_t ctid[5];
    for (int i = 0; i < 5; ++i)
    {
        pthread_create(&ptid[i], NULL, producer, NULL);
    }

    for (int i = 0; i < 5; ++i)
    {
        pthread_create(&ctid[i], NULL, consumer, NULL);
    }

    // 释放资源
    for (int i = 0; i < 5; ++i)
    {
        pthread_join(ptid[i], NULL);
    }

    for (int i = 0; i < 5; ++i)
    {
        pthread_join(ctid[i], NULL);
    }

    // 销毁信号量
    sem_destroy(&psem);
    sem_destroy(&csem);

    // 销毁互斥锁
    pthread_mutex_destroy(&mutex);

    return 0;
}

3. 条件变量

条件变量的主要作用是阻塞线程,不能完全保证线程安全,需要条件变量和互斥锁一起使用。

// 条件变量
pthread_cond_t cond;

初始化与释放条件变量

// 初始化条件变量
int pthread_cond_init(
	pthread_cond_t *restrict cond,	// 条件变量的地址
    const pthread_condattr_t *restrict attr	// 条件变量属性,一般使用默认属性,指定为NULL
);
// 释放资源        
int pthread_cond_destroy(
	pthread_cond_t *cond	// 条件变量的地址
);

阻塞与唤醒线程

如果线程已经对互斥锁上锁,而线程又阻塞在条件变量上, 其他线程访问该互斥锁会阻塞,会造成死锁。为了避免死锁,将该互斥锁解锁。当线程解除阻塞的时候,函数内部会帮助这个线程再次将这个互斥锁锁上,继续向下访问临界区。

// 线程阻塞函数
int pthread_cond_wait(
	pthread_cond_t *restrict cond, 	// 条件变量的地址
	pthread_mutex_t *restrict mutex	// 互斥锁的地址
);
// 线程唤醒函数, 至少有一个被解除阻塞
int pthread_cond_signal(	
	pthread_cond_t *cond	// 条件变量的地址
);

// 线程唤醒函数, 被阻塞的线程全部解除阻塞
int pthread_cond_broadcast(
	pthread_cond_t *cond	// 条件变量的地址
);

条件变量封装类

#include <exception>
#include <pthread.h>

// 条件变量
class cond
{
public:
    // 初始化条件变量
    cond()
    {
        if (pthread_cond_init(&m_cond, NULL) != 0)
        {
            throw std::exception();
        }
    }
    // 释放条件变量资源
    ~cond()
    {
        pthread_cond_destroy(&m_cond);
    }
    // 线程阻塞函数,先把调用该函数的线程放入条件变量的请求队列。
    // 如果线程已经对互斥锁上锁,那么会将这把锁打开,这样做是为了避免死锁。
    // 在线程解除阻塞时,函数内部会帮助这个线程再次将锁锁上,继续向下访问临界区。
    bool wait(pthread_mutex_t *m_mutex)
    {
        int ret = 0;
        // 线程解除阻塞返回0 --> return true
        ret = pthread_cond_wait(&m_cond, m_mutex);
        return ret == 0;
    }
    // 将线程阻塞一定的时间长度,时间到达之后,线程就解除阻塞
    bool timewait(pthread_mutex_t *m_mutex, struct timespec t)
    {
        int ret = 0;
        ret = pthread_cond_timedwait(&m_cond, m_mutex, &t);
        return ret == 0;
    }
    // 至少有一个线程被解除阻塞
    bool signal()
    {
        return pthread_cond_signal(&m_cond) == 0;
    }
    // 被阻塞的线程全部解除阻塞
    bool broadcast()
    {
        return pthread_cond_broadcast(&m_cond) == 0;
    }

private:
    // 条件变量
    pthread_cond_t m_cond;
};

虚假唤醒问题

以生产者-消费者模型为例,以条件变量与互斥锁共同实现线程同步。当任务队列中无任务时,消费者线程被阻塞在条件变量上,当生产者线程生产出一个任务后,会唤醒所有消费者线程进行消费,其中一个消费者线程抢到CPU时间片开始进行消费,导致任务队列没有任务了。假如使用if条件语句进行判断,其他消费者线程抢到CPU时间片后继续向下执行,但此时任务队列已经没有任务了!使用while循环语句反复判断条件,可以避免上述问题 。

linux 多线程 python_c语言

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

// 链表的节点
struct Node
{
    int number;
    struct Node *next;
};

// 指向链表头结点的指针
struct Node *head = NULL;
// 条件变量
pthread_cond_t cond;
// 互斥锁
pthread_mutex_t mutex;

// 生产者的回调函数
void *producer(void *arg)
{
    // 一直生产
    while (1)
    {
        /****** 加锁 *****/
        pthread_mutex_lock(&mutex);

        // 创建一个链表的新节点
        struct Node *pnew = (struct Node *)malloc(sizeof(struct Node));
        // 节点初始化
        pnew->number = rand() % 1000;
        // 节点添加到链表的头部
        pnew->next = head;
        head = pnew;
        printf("【Producer】number = %d, tid = %ld\n", pnew->number, pthread_self());

        /****** 解锁 *****/
        pthread_mutex_unlock(&mutex);

        // 生产了任务, 通知所有消费者消费
        pthread_cond_broadcast(&cond);

        // 生产慢一点
        sleep(rand() % 3);
    }
    return NULL;
}

// 消费者的回调函数
void *consumer(void *arg)
{
    while (1)
    {
        /****** 加锁 *****/
        pthread_mutex_lock(&mutex);

        // 不能使用if(head == NULL)
        // 虚假唤醒问题
        while (head == NULL)
        {
            pthread_cond_wait(&cond, &mutex);
        }
        // 删除链表的头结点
        struct Node *pnode = head;
        printf("【Consumer】number: %d, tid = %ld\n", pnode->number, pthread_self());
        head = pnode->next;
        free(pnode);

        /****** 解锁 *****/
        pthread_mutex_unlock(&mutex);

        sleep(rand() % 3);
    }
    return NULL;
}

int main()
{
    // 初始化条件变量
    pthread_cond_init(&cond, NULL);
    // 初始化互斥锁变量
    pthread_mutex_init(&mutex, NULL);

    // 创建5个生产者, 5个消费者
    pthread_t ptid[5];
    pthread_t ctid[5];
    for (int i = 0; i < 5; ++i)
    {
        pthread_create(&ptid[i], NULL, producer, NULL);
    }

    for (int i = 0; i < 5; ++i)
    {
        pthread_create(&ctid[i], NULL, consumer, NULL);
    }

    // 释放资源
    // 阻塞等待子线程退出
    for (int i = 0; i < 5; ++i)
    {
        pthread_join(ptid[i], NULL);
    }

    for (int i = 0; i < 5; ++i)
    {
        pthread_join(ctid[i], NULL);
    }

    // 销毁条件变量
    pthread_cond_destroy(&cond);
    // 销毁互斥锁
    pthread_mutex_destroy(&mutex);

    return 0;
}

补充:读写锁

如果程序中所有的线程都对共享资源做写操作,使用读写锁没有优势,和互斥锁是一样的。如果程序中的线程对共享资源的操作有读操作也有写操作,读操作越多,读写锁更有优势。

  • 使用读写锁的读锁锁定了临界区,线程对临界区的访问是并行的,读锁是共享的。
  • 使用读写锁的写锁锁定了临界区,线程对临界区的访问是串行的,写锁是独占的。
  • 使用读写锁分别对两个临界区加了读锁和写锁,两个线程要同时访问者两个临界区,访问写锁临界区的线程继续运行,访问读锁临界区的线程阻塞,因为写锁比读锁的优先级高。
// 读写锁
pthread_rwlock_t rwlock;

初始化与释放读写锁

// 初始化读写锁
int pthread_rwlock_init(
	pthread_rwlock_t *restrict rwlock,	// 读写锁的地址
    const pthread_rwlockattr_t *restrict attr	// 读写锁属性,一般使用默认属性,指定为NULL
);
// 释放读写锁资源
int pthread_rwlock_destroy(
	pthread_rwlock_t *rwlock	// 读写锁的地址
);

加锁与解锁

对读写锁加读锁,如果读写锁是打开的,那么加锁成功。如果读写锁已经锁定了读操作,可以加锁成功,因为读锁是共享的。如果读写锁已经锁定了写操作,线程会被阻塞。

// 加读锁
int pthread_rwlock_rdlock(
	pthread_rwlock_t *rwlock	// 读写锁的地址
);

对读写锁加写锁,如果读写锁是打开的,那么加锁成功。如果读写锁已经锁定了读操作或者锁定了写操作,线程会被阻塞。

// 加写锁
int pthread_rwlock_wrlock(
	pthread_rwlock_t *rwlock	// 读写锁的地址
);
// 解锁,锁定读操作、写操作都可用解锁
int pthread_rwlock_unlock(
	pthread_rwlock_t *rwlock	// 读写锁的地址
);

示例

// 8个线程操作同一个全局变量,3个线程不定时执行写操作,5个线程不定时执行读操作。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>

// 全局变量
int number = 0;

// 定义读写锁
pthread_rwlock_t rwlock;

// 写线程的处理函数
void *writeNum(void *arg)
{
    while (1)
    {
        /************ 加锁 ************/
        pthread_rwlock_wrlock(&rwlock);
        int cur = number;
        cur++;
        number = cur;
        printf("写操作完毕, number = %d, tid = %ld\n", number, pthread_self());
        /************ 解锁 ************/
        pthread_rwlock_unlock(&rwlock);
        // 慢一点
        usleep(rand() % 100);
    }

    return NULL;
}

// 读线程的处理函数
void *readNum(void *arg)
{
    while (1)
    {
        /************ 加锁 ************/
        pthread_rwlock_rdlock(&rwlock);
        printf("读操作完毕, number = %d, tid = %ld\n", number, pthread_self());
        /************ 解锁 ************/
        pthread_rwlock_unlock(&rwlock);
        usleep(rand() % 100);
    }
    return NULL;
}

int main()
{
    // 初始化读写锁
    pthread_rwlock_init(&rwlock, NULL);

    // 3个写线程, 5个读的线程
    pthread_t wtid[3];
    pthread_t rtid[5];
    // 3个写线程
    for (int i = 0; i < 3; ++i)
    {
        pthread_create(&wtid[i], NULL, writeNum, NULL);
    }
    // 5个读的线程
    for (int i = 0; i < 5; ++i)
    {
        pthread_create(&rtid[i], NULL, readNum, NULL);
    }

    // 释放资源
    for (int i = 0; i < 3; ++i)
    {
        pthread_join(wtid[i], NULL);
    }

    for (int i = 0; i < 5; ++i)
    {
        pthread_join(rtid[i], NULL);
    }

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

    return 0;
}

参考:https://subingwen.cn/linux/thread-sync
参考:https://subingwen.cn/linux/thread/