进程创建子线程后,主线程和子线程共用一个虚拟地址空间,但是各自在内核区中有独立的 PCB
通过PID查该进程下的线程LWP
ps -Lf [PID]
多进程和多线程比较:
共享的资源
多进程
- .text
- 文件描述符
- 内存映射区
…多线程
- 堆
- 全局变量
…
线程更为节省资源
创建进程时,子进程的代码和父进程一样; 而创建线程,则需要指定这个线程去执行一个具体的函数.
// 创建线程
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
// 退出单个线程
void pthread_exit(void *retval);
/**
* @author IYATT-yx
* @brief 循环创建多个线程
*/
#include <stdio.h>
#include <pthread.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
// 创建子线程数
#define N 9
void *info(void *num)
{
printf("%d子: %lu\n", *(int *)num + 1, pthread_self());
return NULL;
}
int main(void)
{
pthread_t pth[N];
for (int i = 0; i < N; ++i)
{
// 每次创建线程都专门给它创建一块堆区用于保存它的序号
int *temp = (int *)malloc(sizeof(int) * N);
*temp = i;
int errnum = pthread_create(&pth[i], NULL, info, (void *)temp);
{
// 返回值非 0 时打印错误信息,此处的错误代码不是 errno ,不能使用perror()
if (errnum != 0)
{
printf("%s\n", strerror(errnum));
}
}
}
printf("父: %lu\n", pthread_self());
// 退出当前线程(主),且不影响其它线程
pthread_exit(NULL);
}
从运行结果可以注意到很明显的一点,就是打印输出(执行线程)的顺序和创建线程的顺序不一样,这个其实也很容易想通,线程创建好后,只有抢到CPU才能被执行,所以执行顺序和创建顺序并不能保证对应.
另外在 for 循环创建线程的时候,没有直接将 i 传给创建的线程,此处也是一个坑,各线程共用一个 i ,在创建子线程的时候将 i 的地址传进去,每个线程都是获取到同样的地址,即在共用 i ,但是子线程不一定立即执行, 可能当 for 循环到某次的时候, i 地址对应一个循环的次数, 然后此时执行了多个进程, 而看到的现象就是几个进程前面的序号有输出一样的情况,无法区分顺序. (可以自行试验)
想到的一种解决方式就是,在堆区为每个线程申请一块地址来保存创建它的序号. 每次创建一个线程前,都 malloc 一块地址, 并将此次循环的序号保存进去, 然后创建线程, 将对应的堆区地址传给子线程. 这样每个线程都是使用的不同位置来保存他们的创建序号, 不管这些线程是不是在某次 for 循环下执行的,只要执行的时候去取他们分别对应序号堆区的值出来即可, 因为不是同一块地址, 也不会再出现值相同无法区分的情况.
// 阻塞等待线程退出,获取线程退出状态
int pthread_join(pthread_t thread, void **retval);
/**
* @author IYATT-yx
* @brief 回收子线程,并获取退出信息
*/
#include <stdio.h>
#include <pthread.h>
#include <string.h>
#include <stdlib.h>
#define str "正常退出"
void *info(void *num)
{
char *s = (char *)malloc(sizeof(str));
printf("子: %lu\n", pthread_self());
strcpy(s, str);
// 退出时要返回的信息需要是全局或者在堆区的,而局部变量在栈区,当前所在函数一结束则会被释放,主线程pthread_join将无法正常获取
pthread_exit(s);
return num;
}
int main(void)
{
pthread_t pth;
int errnum = pthread_create(&pth, NULL, info, NULL);
if (errnum != 0)
{
printf("%s\n", strerror(errnum));
return -1;
}
void *s = NULL;
pthread_join(pth, &s);
printf("子线程退出信息: %s\n", (char *)s);
}
// 线程分离
// 调用该函数后不需要pthread_join(),子线程自动回收自己的PCB
// 也可以直接在创建线程的时候设置线程分离属性(pthread_create()第二个参数)
int pthread_detach(pthread_t thread);
// 杀死线程
// 在主线程调用它来杀死子线程
// 在要杀死的线程中至少要有一次系统调用,作为停止它的一个取消点
int pthread_cancel(pthread_t thread);
// 可用于作为线程的取消点,无其它作用效果
void pthread_testcancel(void);
// // // // // //
// 线程属性操作
// // // // // //
// 线程属性对象
pthread_attr_t attr;
// 线程属性对象初始化
int pthread_attr_init(pthread_attr_t *attr);
/**
* @brief . 设置线程分离属性
* @param attr 线程属性
* @param detachstate :
* PTHREAD_CREATE_DETACHED 分离
* PTHREAD_CREATE_JOINABLE 非分离
*/
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
// 销毁线程属性对象
int pthread_attr_destroy(pthread_attr_t *attr);
/**
* @author IYATT-yx
* @brief 创建分离属性的线程
*/
#include <stdio.h>
#include <pthread.h>
#include <string.h>
void *info(void *num)
{
printf("我是子线程\n");
return num;
}
int main(void)
{
// 设置线程分离
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
// 创建线程
pthread_t pth;
int errnum = pthread_create(&pth, &attr, info, NULL);
if (errnum != 0)
{
printf("%s\n", strerror(errnum));
return -1;
}
// 销毁属性对象
pthread_attr_destroy(&attr);
// 退出主线程
pthread_exit(NULL);
}
互斥锁
// 创建互斥锁
pthread_mutex_t mutex;
// 初始化互斥锁
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
// 加锁,未加锁则加锁,已加锁则 阻塞, 解锁后, 不阻塞
int pthread_mutex_lock(pthread_mutex_t *mutex);
// 加锁,未加锁则加锁,已加锁则返回,不阻塞
int pthread_mutex_trylock(pthread_mutex_t *mutex);
// // // // // // // // // // // // // // // // //
// // // // // // // // // // // // // // // // //
// 用法示例
if (pthread_mutex_trylock(&mutex) == 0)
{
// 尝试加锁,成功了,则访问共享资源
}
else
{
// 错误处理 或者 等一会再尝试加锁
}
// // // // // // // // // // // // // // // // //
// // // // // // // // // // // // // // // // //
// 解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);
// 销毁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
原子操作: CPU处理一个指令时,线程或进程在处理完这个指令前不会失去CPU
在加锁和解锁之间的部分称为临界区,临界区在实际执行效果上就类似于原子操作
/**
* @author IYATT-yx
* @brief 线程同步
*/
#define _GNU_SOURCE
#include <stdio.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
// 子线程中的循环次数
#define MAX 15000
// 线程共享变量
long g_num;
void *pthread1(void *param)
{
for (long i = 0; i < MAX; ++i)
{
long temp = g_num;
++temp;
g_num = temp;
printf("1: %ld\n", g_num);
// 人为制造挂起态,增大两进程交替执行的概率
usleep(10);
}
return param;
}
void *pthread2(void *param)
{
for (long i = 0; i < MAX; ++i)
{
// 频繁赋值操作,增大数据混乱概率,便于观察到现象
long temp = g_num;
++temp;
g_num = temp;
printf("2: %ld\n", g_num);
usleep(10);
}
return param;
}
int main(void)
{
pthread_t pth[2];
int errnum;
errnum = pthread_create(&pth[0], NULL, pthread1, NULL);
if (errnum != 0)
{
printf("1: %s\n", strerror(errnum));
return -1;
}
errnum = pthread_create(&pth[1], NULL, pthread2, NULL);
if (errnum != 0)
{
printf("2: %s\n", strerror(errnum));
return -1;
}
pthread_join(pth[0], NULL);
pthread_join(pth[1], NULL);
}
根据代码的本意,两个线程本来是各自要循环15000次自增的,累计起来最终应该要自增到30000,然而结果却不足30000.
程序中两个线程都并非是连续执行, 而是可能随机交替的(下图程序的一段输出内容也可以看出),
而这样可能发生一种情况,一个线程开始执行,CPU从内存读取 g_num 的值, 并进行自增运算,然后中间失去CPU执行权,且运算的结果没来得及写入内存,暂时放进了高速缓存. 此时另外一个线程获得CPU执行权,从内存读取 g_num 的值, 则这个进程自增的数据起点和上一个进程开始一样,而并没有接在上一个进程临时暂停执行的结果上,而当这个线程失去CPU时,也可能发生这种情况,或者没发生. 但是当上一个线程又恢复执行的时候,可能将它上次在高速缓存保存的数据写入内存,又覆盖了上次正常写入内存的数据,导致数据混乱, 从概率上都是可能发生的,只要有一次存在都可能导致输出结果和预期不同.
而互斥锁可以解决这个问题,当一个线程获取CPU执行权,则上锁,在运行到上锁和解锁之间的部分(临界区)时失去CPU的话,即使其它线程获得CPU也无法执行临界区,当原来的线程再次获得执行权的时候,则继续执行,执行到解锁处. 此时如果其它线程获取到执行权,则能正常接替上一个线程执行的结果,不会出现数据混乱.
当然使用互斥锁的一个特征就是 串行, 原本是多个线程可同时执行操作的,现在变为一个线程做完以后才能由另外一个线程执行操作, 效率较低.
/**
* @author IYATT-yx
* @brief 互斥锁
*/
#define _GNU_SOURCE
#include <stdio.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
// 子线程中的循环次数
#define MAX 15000
// 创建互斥锁
pthread_mutex_t mutex;
// 线程共享变量
long g_num;
void *pthread1(void *param)
{
for (long i = 0; i < MAX; ++i)
{
// 上锁
// 上锁 和 解锁 部分之间为 临界区
pthread_mutex_lock(&mutex);
long temp = g_num;
++temp;
g_num = temp;
printf("1: %ld\n", g_num);
// 解锁
pthread_mutex_unlock(&mutex);
usleep(10);
}
return param;
}
void *pthread2(void *param)
{
for (long i = 0; i < MAX; ++i)
{
pthread_mutex_lock(&mutex);
long temp = g_num;
++temp;
g_num = temp;
printf("1: %ld\n", g_num);
pthread_mutex_unlock(&mutex);
usleep(10);
}
return param;
}
int main(void)
{
pthread_t pth[2];
int errnum;
// 初始化互斥锁
pthread_mutex_init(&mutex, NULL);
errnum = pthread_create(&pth[0], NULL, pthread1, NULL);
if (errnum != 0)
{
printf("1: %s\n", strerror(errnum));
return -1;
}
errnum = pthread_create(&pth[1], NULL, pthread2, NULL);
if (errnum != 0)
{
printf("2: %s\n", strerror(errnum));
return -1;
}
pthread_join(pth[0], NULL);
pthread_join(pth[1], NULL);
// 销毁
pthread_mutex_destroy(&mutex);
}
死锁
- 1.自己锁自己: 加锁操作后没有解锁
- 2.
读写锁
/**
* 读写锁:
* 读锁: 对内存做读操作
* 写锁: 对内存进行写操作
*/
/**
* 特性:
* 1.读时共享(并行处理)
* 2.写时独占
* 3.读写不能同时, 写的优先级更高
*
* ① 线程1有写锁,线程2请求读锁: 线程2阻塞
* ② 线程1有读锁,线程2请求写锁: 线程2阻塞
* ③ 线程1有读锁,线程2请求读锁: 线程2加锁成功
* ④ 线程1有读锁,线程2请求写锁,然后线程3请求读锁:
* 1阻塞, 3阻塞
* 1解锁, 2加写锁成功, 3阻塞
* 2解锁, 3加读锁成功
* ⑤ 线程1有写锁, 线程2请求读锁, 然后线程3请求写锁:
* 2,3阻塞
* 1解锁, 3加写锁成功, 2继续阻塞
* 3解锁, 2加读锁成功
*/
// 读写锁变量
pthread_rwlock_t lock;
// 初始化读写锁
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);
// 加读锁
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
// 尝试加读锁
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
// 加写锁
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
// 尝试加写锁
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
// 解锁
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
// 销毁
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
/**
* @author IYATT-yx
* @brief 读写锁
*/
#define _GNU_SOURCE
#include <stdio.h>
#include <pthread.h>
#include <stdbool.h>
#include <unistd.h>
#include <string.h>
// 多线程共享的变量
long g_num;
// 读写锁
pthread_rwlock_t lock;
// 写线程
void *wr(void *arg)
{
(void)arg;
while (true)
{
// 加写锁
pthread_rwlock_wrlock(&lock);
++g_num;
printf("写: %ld\t停止程序请按 Ctrl + C\n", g_num);
// 解锁
pthread_rwlock_unlock(&lock);
usleep(1000);
}
return NULL;
}
// 读线程
void *rd(void *arg)
{
(void)arg;
while (true)
{
pthread_rwlock_rdlock(&lock);
printf("读: %ld\t停止程序请按 Ctrl + C\n", g_num);
pthread_rwlock_unlock(&lock);
usleep(1000);
}
return NULL;
}
int main(void)
{
// 初始化读写锁
pthread_rwlock_init(&lock, NULL);
pthread_t pth[8];
// 写线程
for (int i = 0; i < 4; ++i)
{
int errnum = pthread_create(&pth[i], NULL, wr, NULL);
if (errnum != 0)
{
printf("写: %s\n", strerror(errnum));
return -1;
}
}
// 读线程
for (int i = 4; i < 8; ++i)
{
int errnum = pthread_create(&pth[i], NULL, rd, NULL);
if (errnum != 0)
{
printf("读: %s\n", strerror(errnum));
return -1;
}
}
// 销毁
pthread_rwlock_destroy(&lock);
// 退出主线程
pthread_exit(NULL);
}
条件变量 (不是锁)
条件不满足时,阻塞线程; 满足时,通知阻塞的线程开始工作
// 变量类型
pthread_cond_t cond;
// 初始化
int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
// 阻塞等待一个条件变量
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
// 限时等待一个条件变量
int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);
// 唤醒至少一个阻塞在条件变量上的线程
int pthread_cond_signal(pthread_cond_t *cond);
// 唤醒全部阻塞在条件变量上的线程
int pthread_cond_broadcast(pthread_cond_t *cond);
// 销毁
int pthread_cond_destroy(pthread_cond_t *cond);
/**
* @author IYATT-yx
* @brief 生产者与消费者模型简述 (条件变量的使用)
* 用一个简单的链表代表商品,生产者线程在头部插入新节点,消费者从头部读取一个数据,就删除该数据所在的节点.
* 而生产者线程和消费者线程的执行是随机的,假如生产者线程被执行了,那么先用互斥锁加锁,
* 结果发现没有商品(链表为空),则利用 pthread_cond_wait 阻塞同时解锁. 当生产者线程获得执行权时,
* 加上互斥锁,创建新节点,然后使用 pthread_cond_signal 发出信号,当消费者中的阻塞的 pthread_cond_wait
* 收到信号,则进行解锁同时解除阻塞,消费者线程输出头部的数据同时删除该节点.
*/
#include <stdio.h>
#include <pthread.h>
#include <stdbool.h>
#include <stdlib.h>
#include <unistd.h>
// 节点
struct sNode
{
int data;
struct sNode *nextPtr;
};
typedef struct sNode Node;
// 链表头
Node *head = NULL;
//互斥锁
pthread_mutex_t mutex;
// 条件变量
pthread_cond_t cond;
// 生产者线程
void *producer(void *arg)
{
(void)arg;
while (true)
{
Node *new = (Node *)malloc(sizeof(Node));
new->data = rand() % 5000;
// 互斥锁保护共享数据
pthread_mutex_lock(&mutex);
new->nextPtr = head;
head = new;
printf("生产者: %d\n", new->data);
// 解 互斥锁
pthread_mutex_unlock(&mutex);
// 通知消费者线程中的 pthread_cond_wait 解除阻塞
pthread_cond_signal(&cond);
sleep((unsigned)rand() % 3);
}
return NULL;
}
// 消费者线程
void *customer(void *arg)
{
(void)arg;
while (true)
{
// 加锁
pthread_mutex_lock(&mutex);
if (head == NULL)
{
// 阻塞,同时对互斥锁解锁;当收到消费者线程的pthread_cond_signal通知后,加锁解除阻塞
pthread_cond_wait(&cond, &mutex);
}
Node *nd = head;
head = head->nextPtr;
printf("消费者: %d\t关闭程序请按 Ctrl+C\n", nd->data);
free(nd);
pthread_mutex_unlock(&mutex);
}
return NULL;
}
int main(void)
{
// 初始化
pthread_mutex_init(&mutex, NULL);
pthread_cond_init(&cond, NULL);
pthread_t pth1, pth2;
pthread_create(&pth1, NULL, producer, NULL);
pthread_create(&pth2, NULL, customer, NULL);
pthread_join(pth1, NULL);
pthread_join(pth2, NULL);
// 销毁
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond);
}
信号量 (信号灯)
高级的互斥锁,互斥锁是单锁(串行), 信号量是多锁(并行)
// 类型
sem_t sem;
/**
* @brief 初始化
* @param sem sem_t类型
* @param pshared
* 0 线程同步
* 1 进程同步
* @param value 最多操作共享数据的线程个数
*/
int sem_init(sem_t *sem, int pshared, unsigned int value);
// 加锁
int sem_wait(sem_t *sem);
// 尝试加锁
int sem_trywait(sem_t *sem);
// 限时尝试加锁
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
// 解锁
int sem_post(sem_t *sem);
// 销毁
int sem_destroy(sem_t *sem);
/**
* @author IYATT-yx
* @brief 生产者与消费者模型 (信号量的使用)
*/
#include <stdio.h>
#include <semaphore.h>
#include <stdbool.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
struct sNode
{
int data;
struct sNode *nextPtr;
};
typedef struct sNode Node;
Node *head = NULL;
sem_t produceSem;
sem_t customerSem;
void *producer(void *arg)
{
(void)arg;
while (true)
{
sem_wait(&produceSem);
Node *new = (Node *)malloc(sizeof(Node));
new->data = rand() % 5000;
new->nextPtr = head;
head = new;
printf("生产者: %d\n", new->data);
sem_post(&customerSem);
sleep((unsigned)rand() % 4);
}
return NULL;
}
void *customer(void *arg)
{
(void)arg;
while (true)
{
sem_wait(&customerSem);
Node *nd = head;
head = head->nextPtr;
printf("消费者: %d\t关闭程序请按 Ctrl+C\n", nd->data);
free(nd);
sem_post(&produceSem);
}
return NULL;
}
int main(void)
{
pthread_t pth[2];
// 初始化信号量
sem_init(&produceSem, 0, 4);
sem_init(&customerSem, 0, 0);
pthread_create(&pth[0], NULL, producer, NULL);
pthread_create(&pth[1], NULL, customer, NULL);
pthread_join(pth[0], NULL);
pthread_join(pth[1], NULL);
sem_destroy(&produceSem);
sem_destroy(&customerSem);
}
哲学家就餐问题
有五位哲学家围坐在一张圆形餐桌旁,做以下两件事情之一:吃饭,或者思考。吃东西的时候,他们就停止思考,思考的时候也停止吃东西。桌子中间有一大碗饭,每两个哲学家之间有一根筷子。因为用一只筷子很难吃饭,所以设定哲学家必须用一双筷子吃东西。他们只能使用自己左右手边的那两根筷子。
哲学家从来不交谈,这就很危险,可能产生死锁,每个哲学家都拿着左手的筷子,永远都在等右边的筷子(或者相反)。即使没有死锁,也有可能发生资源耗尽。例如,假设规定当哲学家等待另一只筷子超过五分钟后就放下自己手里的那一根筷子,并且再等五分钟后进行下一次尝试。这个策略消除了死锁(系统总会进入到下一个状态),但仍然有可能发生“活锁”。如果五位哲学家在完全相同的时刻进入餐厅,并同时拿起左边的筷子,那么这些哲学家就会等待五分钟,同时放下手中的筷子,再等五分钟,又同时拿起这些筷子。
在实际的计算机问题中,缺乏筷子可以类比为缺乏共享资源。一种常用的计算机技术是资源加锁,用来保证在某个时刻,资源只能被一个程序或一段代码访问。当一个程序想要使用的资源已经被另一个程序锁定,它就等待资源解锁。当多个程序涉及到加锁的资源时,在某些情况下就有可能发生死锁。例如,某个程序需要访问两个文件,当两个这样的程序各锁了一个文件,那它们都在等待对方解锁另一个文件,而这永远不会发生。