线程
进程是资源分配的基本单位,线程是调度的基本单位。进程有自己独立的地址空间,而多个线程共用同一个地址空间,因此线程间通信比较简单。虚拟地址空间的生命周期与主线程一致,与子线程无关。当子线程退出,需要回收子进程数据时,不能利用子线程栈,可以使用全局数据区、堆区、主线程栈回收。
- 线程的私有资源:线程栈、寄存器等。
- 线程的共享资源:代码段,堆区,全局数据区,文件描述符表等。
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循环语句反复判断条件,可以避免上述问题 。
#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/