UNIX高级编程【深入浅出】 线程

目录

概念

应用场景与涉及到的问题

多线程并发访问同一块内存可能会出现以下问题:

为避免这些问题

同步机制的说明

常用的线程同步机制

互斥锁(Mutex):

读写锁(ReadWrite Lock):

条件变量(Condition Variable):

线程标识  pthread_self(3)

线程的终止

线程相关函数  pthread_xxx

线程的同步  

代码实现 (互斥锁、条件变量)

线程的取消

线程安全函数

线程和信号

线程和IO


  1. 概念

    1. 线程是CPU调度的最小单位,每个进程可以包含多个线程,每个线程都是独立执行的,但在同一个进程内共享该进程的资源。
    2. 进程是内核直接提供的接口,先有实现,再定标准
    3. 线程是先定标准,再写的实现,所以所有的线程接口都是pthread_xxx
    4. man 7 pthreads
  2. 应用场景与涉及到的问题

    1. 线程在各种应用中广泛使用,特别是在需要同时处理多个任务或并发执行的程序中。例如,Web服务器可以通过为每个客户端请求创建一个线程来实现并发处理,从而提高服务响应速度。另外,图形界面程序中常常使用主线程负责用户界面的响应,而将耗时的任务放在其他线程中执行,以保持界面的流畅性。
    2. 有竞态条件、死锁和资源争用等问题。
    3. 多线程并发访问同一块内存可能会出现以下问题:
      1. 竞争条件:多个线程同时访问相同的内存地址(称为临界区),导致数据的不确定性,甚至可能导致程序崩溃。

      2. 死锁:若两个或多个线程同时试图锁定同一块内存区域,则可能会出现死锁情况,导致程序无法继续执行。

      3. 内存泄漏:若线程在访问内存后没有释放内存空间,则会导致内存泄漏,最终可能导致系统崩溃。

      4. 为避免这些问题
        1. 可以使用同步机制(例如锁、信号量、互斥量等)来控制对共享内存的访问,或者使用线程安全的数据结构。同时,在编写代码时,也应该尽量避免直接对内存进行操作,而是采用封装好的函数库和数据结构,以降低出错的概率。

    4. 同步机制的说明

      1. 锁(lock)是一种最基本的同步机制,它保证了临界区代码的互斥执行。在程序中,当线程进入某个临界区时,就需要获取锁,当线程执行完临界区后,就需要释放锁。如果没有获取到锁,线程则会阻塞等待。锁常用于简单的同步,如对资源的访问,但并不能解决复杂的同步问题。

      2. 互斥量(mutex)是一种更加灵活的同步机制,它可以在某些情况下代替锁。与锁相似,互斥量也是用于保护临界区代码的执行,但它可以支持更多的功能,如递归加锁和非阻塞地尝试加锁。与锁不同的是,互斥量可以是进程间共享的,而锁只能在同一个进程内进行同步。

      3. 信号量(Semaphore)是一种计数器,用于同步线程的执行。当一个线程需要访问某个共享资源时,它需要获取信号量的许可,当线程完成后,它会释放信号量。信号量有两种类型:二元信号量和计数信号量。二元信号量只有0和1两个值,它一般用于互斥操作;计数信号量则可以有多个取值,并且支持多线程的访问。

    5. 常用的线程同步机制

      1. 互斥锁(Mutex)
      2. 互斥锁是最常见的线程同步机制,通过对临界区代码进行加锁,确保同一时间只有一个线程可以访问临界资源。以下是使用互斥锁实现多线程加锁的基本步骤:

        • 创建一个互斥锁对象。
        • 在需要保护的临界区前调用锁定函数(如pthread_mutex_lock)对互斥锁进行加锁。
        • 执行临界区代码。
        • 在临界区代码执行完毕后调用解锁函数(如pthread_mutex_unlock)对互斥锁进行解锁。
      3. 读写锁(ReadWrite Lock):
      4. 读写锁允许多个线程同时读取共享数据,但只允许一个线程进行写操作。这在某些情况下可以提高性能。以下是使用读写锁实现多线程加锁的基本步骤:

        • 创建一个读写锁对象。
        • 在需要保护的读操作前调用读锁定函数(如pthread_rwlock_rdlock)对读写锁进行加读锁。
        • 在需要保护的写操作前调用写锁定函数(如pthread_rwlock_wrlock)对读写锁进行加写锁。
        • 执行读或写操作的代码。
        • 在操作完成后调用解锁函数(如pthread_rwlock_unlock)对读写锁进行解锁。
      5. 条件变量(Condition Variable):
      6. 条件变量允许线程在满足特定条件之前等待,并在条件满足时被唤醒。它通常与互斥锁一起使用。以下是使用条件变量实现多线程加锁的基本步骤:

        • 创建一个条件变量对象和一个与之关联的互斥锁对象。
        • 在临界区代码执行之前,使用互斥锁对共享资源进行加锁。
        • 检查条件是否满足,如果不满足,则调用等待函数(如pthread_cond_wait)将线程置于等待状态,并释放互斥锁。
        • 当其他线程改变了条件并发出信号时,等待的线程将被唤醒,重新获取互斥锁,并重新检查条件。
        • 在满足特定条件后,执行临界区代码。
        • 临界区代码执行完毕后,释放互斥锁。
  3. 线程标识  pthread_self(3)

    1. pthread_t是一个结构,注意不能printf直接输出
    2. 也不能直接用等于==比较,pthread_equal(3)函数进行线程标识的比较
    3. 获取调用线程的线程标识:pthread_self(3)
  4. 线程的终止

    1. 从启动例程返回 return
    2. 调用pthread_exit(3)
    3. 被取消
    4. 进程终止
  5. 线程相关函数  pthread_xxx

    1. 创建 pthread_create(3)
    2. 终止 pthread_exit(3)
    3. 资源回收  pthread_join(3)
    4. 线程的同步  

      1. 互斥量
      2. 数据类型  pthread_mutex_t
      3. pthread_mutex_init(3);    使用宏(仅限于定义的同时给值)
      4. pthread_mutex_lock(3);  
        1. 如果互斥量已经是locked,那么再调用pthread_mutex_lock(3)会阻塞,直到成功将互斥量的状态改为locked
      5. pthread_mutex_unlock(3)
      6. pthread_mutex_destroy(3);
      7. 条件变量
      8. 数据类型 pthread_cond_t
      9. pthread_cond_init(3)    使用宏(仅限于定义的同时给值)
      10. 通知
      11. pthread_cond_broadcast(3)
      12. pthread_cond_signal(3)
      13. 等待条件变化
        1. pthread_cond_wait(3);
      14. 销毁
        1. pthread_cond_destroy(3);
    5. 实例

代码实现 (互斥锁、条件变量)

#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;
}



  1. 线程的取消

    1. 向目标线程发送取消请求   pthread_cancel(3)
    2. 目标线程是否以及何时 (Whether and when )对请求做出反应取决于 state and type
    3. whether--->state
      1. enable(default)
      2. disable
    4. when--->type
      1. asynchronous  立即响应
      2. deferred(default)   延时取消,等下一次调用完取消点函数再响应取消
    5. 目标线程可以注册终止处理函数(有捆绑函数)
      1. pthread_cleanup_push(3)
      2. 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);
}
  1. 线程安全函数

    1. 线程安全是指多个线程访问共享数据时不会出现数据不一致、冲突或其他异常行为。要保证线程安全,可以采取以下方法:

      互斥锁:使用互斥锁保护共享资源的访问。
      原子操作:使用原子操作来对共享数据进行操作,确保操作的原子性。
      无锁数据结构:使用无锁的数据结构来避免锁竞争。
      线程本地存储:将共享数据拷贝一份给每个线程使用,避免竞争条件。

    2. 常见的线程安全函数类型:

      1. 原子操作函数:原子操作函数能够保证操作的原子性,即不会被中断。

        1. __atomic_add_fetch: 原子加法操作
        2. __atomic_sub_fetch: 原子减法操作
        3. __atomic_compare_exchange: 原子比较交换操作
      2. 互斥锁函数:互斥锁函数能够保证同一时间只有一个线程可以访问共享资源,其他线程需要等待。

        1. pthread_mutex_init: 初始化互斥锁
        2. pthread_mutex_destroy: 销毁互斥锁
        3. pthread_mutex_lock: 加锁互斥锁
        4. pthread_mutex_unlock: 解锁互斥锁
      3. 条件变量函数:条件变量函数用于线程之间的等待和通知机制,可以实现线程的同步和通信。

        1. pthread_cond_init: 初始化条件变量
        2. pthread_cond_destroy: 销毁条件变量
        3. pthread_cond_wait: 等待条件变量满足
        4. pthread_cond_signal: 唤醒等待条件变量的一个线程
        5. pthread_cond_broadcast: 唤醒等待条件变量的所有线程
      4. 自旋锁函数:自旋锁函数使用忙等待的方式来保护共享资源,线程在获取锁失败时会循环等待,直到成功获取锁为止。

        1. pthread_spin_lock:自旋锁的上锁

        2. pthread_spin_unlock:自旋锁的解锁

      5. 线程局部存储函数(线程特定数据函数):线程局部存储函数可以为每个线程分配独立的内存空间,避免了线程间对局部变量的竞争。

        1. pthread_key_create: 创建线程特定数据键
        2. pthread_key_delete: 删除线程特定数据键
        3. pthread_setspecific: 设置线程特定数据值
        4. pthread_getspecific: 获取线程特定数据值
  2. 线程和信号

    1. 同一个进程内的多个线程共享信号pending,信号的行为,有独立的信号屏蔽字
    2. 设置线程的信号屏蔽字pthread_sigmask(3) (与 sigprocmask(2) 用法基本一样
    3. 等待信号到来sigwait(3)
    4. 示例代码
      #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;
      }
      
  3. 线程和IO

    1. 同一个进程内的多个线程共享打开的文件,使用的是同一个进程表项
    2. 如果多个线程对同一个文件需要定位读写,那么建议直接只用pread(3)代替lseek(2) + read(2), 用pwrite(2)代替lseek(2) + write(2)
    3. pread(3)仅适用于普通文件和某些类型的特殊文件
    4. ssize_t pread(int fd, void *buf, size_t count, off_t offset);
  • pread 函数是原子的,它会保证读取操作在调用期间不受其他线程或进程的干扰。这使得多个线程可以同时从同一个文件读取数据,而不需要进行额外的同步。
  • pread 函数不会影响文件偏移量,因此在读取完成后,文件偏移量仍然保持不变。
  • pread 函数返回读取的字节数,可以根据返回值进行错误处理和判断是否读取到了文件的末尾。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值