linux多线程开发
线程概述

- 一个进程可包含多个线程,多个线程之间共享全局内存区
- 进程是CPU分配资源的最小单位,线程是操作系统调度执行的最小单位
- 使用ps -LF pid就可以查询进程下面的线程
线程和进程的区别

- 线程之间,它们都是共享虚拟地址空间的,只是各自又各自的代码区域,各自又各自的栈空间区域,

线程操作函数

- pthread_self()获取当前线程的ID
pthread_create()——创建新线程



pthread_exit()——终止线程

- 程序的return 0 或者exit(0) 都是退出了进程,进程都推了,那里面的线程也被销毁了,
- 使用pthread_exit() 只是退出了线程,其他线程不受影响

pthread_equal()——比较线程ID是否相等

pthread_jion()——和一个终止的线程进行连接
- 用于回收线程的资源
- 任何一个函数都可以调用这个去回收别人
- 是阻塞的,调用一次只能回收一个

- 由于每个线程都区分着堆空间,就是局部变量,所以线程返回这里不能是局部变量,下面这样就是错的

得用全局变量返回

- 主线程中

pthread_detach()——分离一个线程
- 分离的线程直接就释放资源了,不需要其他线程来回收
- 可以直接在主线程中去释放子线程

pthread_cancel()——取消一个线程
- 就是让线程终止,要终止的话需要一定条件,需要看
- 调用之后不是立刻就终止,执行到取消点的时候,才会终止

线程属性操作函数

- pthread_attr_ 后面其实有很多函数,可以通过man pthread_attr_ 加tab去查看,这里展示俩


线程同步

- 临界区可以这样理解,比如1-5行代码,三个线程abc都会执行,我们需要让1-5行代码为原子操作,就是说a在执行的时候,是一口气执行完的,不能说a执行到第2行时候,c把cpu抢过去执行
- 像下面的就是不安全的,没有同步
/*
使用多线程实现买票的案例。
有3个窗口,一共是100张票。
*/
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
// 全局变量,所有的线程都共享这一份资源。
int tickets = 100;
void * sellticket(void * arg) {
// 卖票
while(tickets > 0) {
usleep(6000);
printf("%ld 正在卖第 %d 张门票\n", pthread_self(), tickets);
tickets--;
}
return NULL;
}
int main() {
// 创建3个子线程
pthread_t tid1, tid2, tid3;
pthread_create(&tid1, NULL, sellticket, NULL);
pthread_create(&tid2, NULL, sellticket, NULL);
pthread_create(&tid3, NULL, sellticket, NULL);
// 回收子线程的资源,阻塞
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
pthread_join(tid3, NULL);
// 设置线程分离。
// pthread_detach(tid1);
// pthread_detach(tid2);
// pthread_detach(tid3);
pthread_exit(NULL); // 退出主线程
return 0;
}
互斥锁

- 这里用一个例子来理解,就是共用厕所,一个人在上厕所的时候,他就把门锁起来,其他人进不来,只能等着,他用完了,再解锁,其他人再来用
- a锁了厕所时,门锁只能由他来开
- 在等待锁释放的时候,会睡眠在那里



对上面存在问题的代码的改进: - 由于多个线程都需要用到这个锁,所以他需要时全局的

- 可以在主线程中初始化互斥锁

- 进程结束前需要释放掉这个锁

- 给临界区用锁保护起来

死锁

读写锁

- 这样就可以几个线程同时一起读一个全局,效率高于互斥锁
- 应用于读取比写数据多的情景下
读写锁操作函数
- 用法和互斥锁一样的


C++11里面引入的管理锁的新类
这里就用到了RALL机制,让锁具有生命周期,锁他的作用域结束的时候,自动释放锁,避免出现如果程序异常,导致没有 unlock;
- lock_guard
std::mutex mut;
{
std::lock_guard<std::mutex> lockGuard(mut); // lock in lock_guard 构造函数
sharedVariable++;
} // unlock in lock_guard 析构函数
上面同样的功能,如果不使用RALL的话
mutex mut;
mut.lock();
sharedVariable++;
mut.unlock();
- unique_lock
类 unique_lock 也是通用互斥包装器,也有lock_guard一样的RAII机制,但它有更多功能,允许延迟锁定、锁定的有时限尝试、递归锁定、所有权转移和与条件变量一同使用。它可移动,但不可复制。它有类似std::mutex一样的接口,更加灵活方便.
std::mutex mut;
// exsample_1{
std::unique_lock<std::mutex> unilock(mut, std::defer_lock); // 只创建 unique_lock 对象
unilock.lock(); // 这里需要手动lock
sharedVariable++;
} // 如果里面没有调unlock,这里就unlock
// exsampl_2{
std::unique_lock<std::mutex> unilock(mut, std::adopt_lock); // 只创建 unique_lock 对象,已经调用了lock
sharedVariable++;
unilock.unlock(); // 这里可以直接解锁,也可以析构解锁
}
-
二者都是自释放锁;lock_guard 在时间和空间上都比unique_lock要快;lock_guard 功能单一,只能用作自释放锁;unique_lock具备lock_guard的所有能力,同时提供更多的能力,比如锁的成员函数都会被封装后导出,同时不会引入double lock和 double unlock;
-
那么为什么有时候需要unlock()?
因为lock()锁住的代码段越少,执行越快,整个程序运行效率越高。
锁头锁住的代码的多少称为锁的粒度,粒度一般用粗细来描述。
锁住的代码少,这个粒度叫细,执行效率高。
锁住的代码多,粒度叫粗,执行效率就低。
要学会尽量选择合适粒度的代码进行保护,力度太细,可能漏掉共享数据的保护,粒度太粗,影响效率。
选择合适的粒度,是高级程序员的能力和实力的体现。 -
如果确定使用unique_lock了,就不要再直接使用 mutex 的 lock 和 unlock ,可以直接使用 unique_lock 的 lock 和 unlock,混合使用会导致程序异常,原因是unique_lock 内部会维护一个标识用来记录自己管理的 锁 当前处于何种状态,如果直接使用 mutex的成员函数,unique_lock无法更新自己的状态,从而导致 double lock 和 double unlock(因为unique_lock一定会在析构的时候unlock),这两种情况都会导致崩溃。
条件变量

- 条件变量就是提供一种在线程之间协调工作的机制,不能用来解决线程数据安全的问题,要和锁配合使用
- 比如一个线程使用了pthread_cond_wait(),那他就会阻塞在这,要等到有其他线程通过pthrea_cond_signal()给他发了一个信号之后,才会唤醒他继续往下执行。
- 代码示例:
/*
条件变量的类型 pthread_cond_t
int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
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_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);
- 唤醒所有的等待的线程
*/
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
// 创建一个互斥量
pthread_mutex_t mutex;
// 创建条件变量
pthread_cond_t cond;
//创建一个链表用来做生产和消费的容器
struct Node{
int num;
struct Node *next;
};
// 头结点
struct Node * head = NULL;
void * producer(void * arg) {
// 生产者不断的创建新的节点,添加到链表中
while(1) {
pthread_mutex_lock(&mutex);
struct Node * newNode = (struct Node *)malloc(sizeof(struct Node));
newNode->next = head;
head = newNode;
newNode->num = rand() % 1000;
printf("add node, num : %d, tid : %ld\n", newNode->num, pthread_self());
// 只要生产了一个,就通知消费者消费
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);
usleep(100);
}
return NULL;
}
void * customer(void * arg) {
//消费者,删除一个节点
while(1) {
pthread_mutex_lock(&mutex);
// 保存头结点的指针
struct Node * tmp = head;
// 判断是否有数据
if(head != NULL) {
// 有数据
head = head->next;
printf("del node, num : %d, tid : %ld\n", tmp->num, pthread_self());
free(tmp);
pthread_mutex_unlock(&mutex);
usleep(100);
} else {
// 没有数据,需要等待
// 当这个函数调用阻塞的时候,会对互斥锁进行解锁
pthread_cond_wait(&cond, &mutex);
//当收到信号后不阻塞了,继续向下执行,会重新加锁,所以下面需要再解锁一下
pthread_mutex_unlock(&mutex);
}
}
return NULL;
}
int main() {
pthread_mutex_init(&mutex, NULL);//初始化互斥锁
pthread_cond_init(&cond, NULL);//初始化条件变量
// 创建5个生产者线程,和5个消费者线程
pthread_t ptids[5], ctids[5];
for(int i = 0; i < 5; i++) {
pthread_create(&ptids[i], NULL, producer, NULL);
pthread_create(&ctids[i], NULL, customer, NULL);
}
for(int i = 0; i < 5; i++) {
pthread_detach(ptids[i]);
pthread_detach(ctids[i]);
}
while(1) {
sleep(10);
}
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond);
pthread_exit(NULL);
return 0;
}
信号量
- 也称信号灯,像信号灯那么理解,通过灯的亮和灭来交互

本文详细介绍了Linux环境下多线程开发的基础概念和技术要点,包括线程与进程的区别、线程的操作函数、线程同步机制等内容,并通过具体示例阐述了互斥锁、条件变量及信号量的应用。

被折叠的 条评论
为什么被折叠?



