目录
-
概念
- 线程是CPU调度的最小单位,每个进程可以包含多个线程,每个线程都是独立执行的,但在同一个进程内共享该进程的资源。
- 进程是内核直接提供的接口,先有实现,再定标准
- 线程是先定标准,再写的实现,所以所有的线程接口都是pthread_xxx
- man 7 pthreads
-
应用场景与涉及到的问题
- 线程在各种应用中广泛使用,特别是在需要同时处理多个任务或并发执行的程序中。例如,Web服务器可以通过为每个客户端请求创建一个线程来实现并发处理,从而提高服务响应速度。另外,图形界面程序中常常使用主线程负责用户界面的响应,而将耗时的任务放在其他线程中执行,以保持界面的流畅性。
- 有竞态条件、死锁和资源争用等问题。
-
多线程并发访问同一块内存可能会出现以下问题:
-
竞争条件:多个线程同时访问相同的内存地址(称为临界区),导致数据的不确定性,甚至可能导致程序崩溃。
-
死锁:若两个或多个线程同时试图锁定同一块内存区域,则可能会出现死锁情况,导致程序无法继续执行。
-
内存泄漏:若线程在访问内存后没有释放内存空间,则会导致内存泄漏,最终可能导致系统崩溃。
-
为避免这些问题
-
可以使用同步机制(例如锁、信号量、互斥量等)来控制对共享内存的访问,或者使用线程安全的数据结构。同时,在编写代码时,也应该尽量避免直接对内存进行操作,而是采用封装好的函数库和数据结构,以降低出错的概率。
-
-
-
同步机制的说明
-
锁(lock)是一种最基本的同步机制,它保证了临界区代码的互斥执行。在程序中,当线程进入某个临界区时,就需要获取锁,当线程执行完临界区后,就需要释放锁。如果没有获取到锁,线程则会阻塞等待。锁常用于简单的同步,如对资源的访问,但并不能解决复杂的同步问题。
-
互斥量(mutex)是一种更加灵活的同步机制,它可以在某些情况下代替锁。与锁相似,互斥量也是用于保护临界区代码的执行,但它可以支持更多的功能,如递归加锁和非阻塞地尝试加锁。与锁不同的是,互斥量可以是进程间共享的,而锁只能在同一个进程内进行同步。
-
信号量(Semaphore)是一种计数器,用于同步线程的执行。当一个线程需要访问某个共享资源时,它需要获取信号量的许可,当线程完成后,它会释放信号量。信号量有两种类型:二元信号量和计数信号量。二元信号量只有0和1两个值,它一般用于互斥操作;计数信号量则可以有多个取值,并且支持多线程的访问。
-
-
常用的线程同步机制
-
互斥锁(Mutex):
-
互斥锁是最常见的线程同步机制,通过对临界区代码进行加锁,确保同一时间只有一个线程可以访问临界资源。以下是使用互斥锁实现多线程加锁的基本步骤:
- 创建一个互斥锁对象。
- 在需要保护的临界区前调用锁定函数(如
pthread_mutex_lock
)对互斥锁进行加锁。 - 执行临界区代码。
- 在临界区代码执行完毕后调用解锁函数(如
pthread_mutex_unlock
)对互斥锁进行解锁。
-
读写锁(ReadWrite Lock):
-
读写锁允许多个线程同时读取共享数据,但只允许一个线程进行写操作。这在某些情况下可以提高性能。以下是使用读写锁实现多线程加锁的基本步骤:
- 创建一个读写锁对象。
- 在需要保护的读操作前调用读锁定函数(如
pthread_rwlock_rdlock
)对读写锁进行加读锁。 - 在需要保护的写操作前调用写锁定函数(如
pthread_rwlock_wrlock
)对读写锁进行加写锁。 - 执行读或写操作的代码。
- 在操作完成后调用解锁函数(如
pthread_rwlock_unlock
)对读写锁进行解锁。
-
条件变量(Condition Variable):
-
条件变量允许线程在满足特定条件之前等待,并在条件满足时被唤醒。它通常与互斥锁一起使用。以下是使用条件变量实现多线程加锁的基本步骤:
- 创建一个条件变量对象和一个与之关联的互斥锁对象。
- 在临界区代码执行之前,使用互斥锁对共享资源进行加锁。
- 检查条件是否满足,如果不满足,则调用等待函数(如
pthread_cond_wait
)将线程置于等待状态,并释放互斥锁。 - 当其他线程改变了条件并发出信号时,等待的线程将被唤醒,重新获取互斥锁,并重新检查条件。
- 在满足特定条件后,执行临界区代码。
- 临界区代码执行完毕后,释放互斥锁。
-
-
线程标识 pthread_self(3)
- pthread_t是一个结构,注意不能printf直接输出
- 也不能直接用等于==比较,pthread_equal(3)函数进行线程标识的比较
- 获取调用线程的线程标识:pthread_self(3)
-
线程的终止
- 从启动例程返回 return
- 调用pthread_exit(3)
- 被取消
- 进程终止
-
线程相关函数 pthread_xxx
- 创建 pthread_create(3)
- 终止 pthread_exit(3)
- 资源回收 pthread_join(3)
-
线程的同步
- 互斥量
- 数据类型 pthread_mutex_t
- pthread_mutex_init(3); 使用宏(仅限于定义的同时给值)
- pthread_mutex_lock(3);
- 如果互斥量已经是locked,那么再调用pthread_mutex_lock(3)会阻塞,直到成功将互斥量的状态改为locked
- pthread_mutex_unlock(3)
- pthread_mutex_destroy(3);
- 条件变量
- 数据类型 pthread_cond_t
- pthread_cond_init(3) 使用宏(仅限于定义的同时给值)
- 通知
- pthread_cond_broadcast(3)
- pthread_cond_signal(3)
- 等待条件变化
- pthread_cond_wait(3);
- 销毁
- pthread_cond_destroy(3);
- 实例
代码实现 (互斥锁、条件变量)
#ifndef __HEAD_H__
#define __HEAD_H__
#include <stdio.h>
#include <string.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#define LEFT 100
#define RIGHT 300
#define N 4
#define __lock(msg)\
do { pthread_mutex_lock(msg);} while(0)
#define __unlock(msg)\
do { pthread_mutex_unlock(msg);} while(0)
#define handler_error_en(en, msg)\
do { errno = en; perror(msg); exit(EXIT_FAILURE);} while(0)
#endif
#include "head.h"
/*
main线程发放任务 将数值赋值给task
工作线程取得task的值,并将0赋值给task
所有人物发放完成 task = -1
由于main线程和工作线程共5个线程都要竞争task,所以存取task的值就是临界区
临界区同步采用互斥量
*/
// 任务的标识
static int task;
// 互斥量的初始化,保证所有线程操作task同步
static pthread_mutex_t task_mut = PTHREAD_MUTEX_INITIALIZER;
// 条件变量
static pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
// 每个线程的任务
static int is_primer(int n);
static void *woker_thread(void *s);
int main(void)
{
pthread_t tids[N] = {};
int i, j;
int err;
// 四核操作 [启动四个任务线程]
for(i = 0; i < N; i++){
if(err = pthread_create(tids + i, NULL, woker_thread, NULL))
handler_error_en(err, "pthread_create()");
}
// 发放任务
for(i = LEFT; i <= RIGHT; i++){
// 共享空间--》抢锁
__lock(&task_mut);
while(task != 0){ // 通知
// 上一次发放的任务还没有取走
pthread_cond_wait(&cond, &task_mut);
}
// task == 0 上一次的任务一定取走
task = i;
// 条件改变 需要通知
pthread_cond_signal(&cond);
__unlock(&task_mut);
}
// 期待最后task为0, 然后main值赋值为-1
// 捕获异步事件 通知法 轮询法
__lock(&task_mut);
while(task > 0){
// 最后一个任务还没有取走
// __unlock(&task_mut); 轮询
// __lock(&task_mut);
// 通知 原子操作
pthread_cond_wait(&cond, &task_mut);
}
// task == 0
task = -1; // 所有任务发放完成
pthread_cond_broadcast(&cond);
__unlock(&task_mut);
// 收尸
for (i = 0; i < N; i++)
pthread_join(tids[i], NULL);
pthread_mutex_destroy(&task_mut);
pthread_cond_destroy(&cond);
return 0;
}
static void *woker_thread(void *s)
{
int get_job;
while(1){// 能者多劳
__lock(&task_mut);
// 任务还没有发放
while(task == 0){
pthread_cond_wait(&cond, &task_mut);
}
// 没有任务了
if(task == -1){
__unlock(&task_mut);
break;
}
// 任务来了
get_job = task;
task = 0;
pthread_cond_broadcast(&cond);
__unlock(&task_mut);
// 执行任务
if(is_primer(get_job))
printf("%d is a primer\n", get_job);
}
pthread_exit(NULL);
}
static int is_primer(int n)
{
int i;
for (i = 2; i <= n / 2; i++) {
if (n % i == 0)
return 0;
}
return 1;
}
-
线程的取消
- 向目标线程发送取消请求 pthread_cancel(3)
- 目标线程是否以及何时 (Whether and when )对请求做出反应取决于 state and type
- whether--->state
- enable(default)
- disable
- when--->type
- asynchronous 立即响应
- deferred(default) 延时取消,等下一次调用完取消点函数再响应取消
- 目标线程可以注册终止处理函数(有捆绑函数)
- pthread_cleanup_push(3)
- pthread_cleanup_pop(3) (有几个push 就有几个pop)
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
static void *new_thr_job(void *arg);
int main(void)
{
pthread_t tid;
int err;
// 创建新的线程
if (err = pthread_create(&tid, NULL, new_thr_job, NULL)) {
// 错误处理
// ...
}
// 主线程执行的代码
// ...
// 向新线程发送取消请求
pthread_cancel(tid);
// 等待工作线程的结束
pthread_join(tid, NULL);
printf("新线程已经终止\n");
return 0;
}
// 线程终止处理函数
static void clean_up1(void *s)
{
// 处理内容
// ...
}
static void clean_up2(void *s)
{
// ...
}
static void *new_thr_job(void *arg)
{
// 注册终止处理函数
pthread_cleanup_push(clean_up1, NULL);
pthread_cleanup_push(clean_up2, NULL);
// 线程执行的代码
// ...
// 终止线程
#if 0
从启动例程返回
调用pthread_exit(3)
被取消
进程终止
#endif
// return (void *)0; // NULL
pthread_exit((void *)0);
// 以下仅仅是为了匹配pthread_cleanup_push(3)
pthread_cleanup_pop(0);
pthread_cleanup_pop(0);
}
-
线程安全函数
-
线程安全是指多个线程访问共享数据时不会出现数据不一致、冲突或其他异常行为。要保证线程安全,可以采取以下方法:
互斥锁:使用互斥锁保护共享资源的访问。
原子操作:使用原子操作来对共享数据进行操作,确保操作的原子性。
无锁数据结构:使用无锁的数据结构来避免锁竞争。
线程本地存储:将共享数据拷贝一份给每个线程使用,避免竞争条件。 -
常见的线程安全函数类型:
-
原子操作函数:原子操作函数能够保证操作的原子性,即不会被中断。
__atomic_add_fetch
: 原子加法操作__atomic_sub_fetch
: 原子减法操作__atomic_compare_exchange
: 原子比较交换操作
-
互斥锁函数:互斥锁函数能够保证同一时间只有一个线程可以访问共享资源,其他线程需要等待。
pthread_mutex_init
: 初始化互斥锁pthread_mutex_destroy
: 销毁互斥锁pthread_mutex_lock
: 加锁互斥锁pthread_mutex_unlock
: 解锁互斥锁
-
条件变量函数:条件变量函数用于线程之间的等待和通知机制,可以实现线程的同步和通信。
pthread_cond_init
: 初始化条件变量pthread_cond_destroy
: 销毁条件变量pthread_cond_wait
: 等待条件变量满足pthread_cond_signal
: 唤醒等待条件变量的一个线程pthread_cond_broadcast
: 唤醒等待条件变量的所有线程
-
自旋锁函数:自旋锁函数使用忙等待的方式来保护共享资源,线程在获取锁失败时会循环等待,直到成功获取锁为止。
-
pthread_spin_lock:自旋锁的上锁
-
pthread_spin_unlock:自旋锁的解锁
-
-
线程局部存储函数(线程特定数据函数):线程局部存储函数可以为每个线程分配独立的内存空间,避免了线程间对局部变量的竞争。
pthread_key_create
: 创建线程特定数据键pthread_key_delete
: 删除线程特定数据键pthread_setspecific
: 设置线程特定数据值pthread_getspecific
: 获取线程特定数据值
-
-
-
线程和信号
- 同一个进程内的多个线程共享信号pending,信号的行为,有独立的信号屏蔽字
- 设置线程的信号屏蔽字pthread_sigmask(3) (与 sigprocmask(2) 用法基本一样)
- 等待信号到来sigwait(3)
- 示例代码
#include <pthread.h> #include <signal.h> void* thread_function(void* arg) { // 创建一个信号集,并将要屏蔽的信号添加到集合中 sigset_t signal_set; sigemptyset(&signal_set); sigaddset(&signal_set, SIGINT); // 屏蔽SIGINT信号 // 设置线程的信号屏蔽字 int ret = pthread_sigmask(SIG_BLOCK, &signal_set, NULL); if (ret != 0) { // 错误处理 // ... } // 线程执行的代码 // ... pthread_exit(NULL); } int main() { pthread_t thread_id; pthread_create(&thread_id, NULL, thread_function, NULL); // 主线程执行的代码 // ... pthread_join(thread_id, NULL); return 0; }
-
线程和IO
- 同一个进程内的多个线程共享打开的文件,使用的是同一个进程表项
- 如果多个线程对同一个文件需要定位读写,那么建议直接只用pread(3)代替lseek(2) + read(2), 用pwrite(2)代替lseek(2) + write(2)
- pread(3)仅适用于普通文件和某些类型的特殊文件
-
ssize_t pread(int fd, void *buf, size_t count, off_t offset);
pread
函数是原子的,它会保证读取操作在调用期间不受其他线程或进程的干扰。这使得多个线程可以同时从同一个文件读取数据,而不需要进行额外的同步。pread
函数不会影响文件偏移量,因此在读取完成后,文件偏移量仍然保持不变。pread
函数返回读取的字节数,可以根据返回值进行错误处理和判断是否读取到了文件的末尾。