什么是线程?
**线程(Thread)是程序执行的最小单元,是操作系统进行调度和执行的基本单位。**一个线程可以看作是一个轻量级的进程,它独立地执行特定的任务或代码段,并拥有自己的程序计数器、寄存器集合和栈空间。
线程是操作系统提供的一种并发执行的机制,它允许在同一个程序中同时执行多个线程,每个线程都有自己的执行流和上下文信息。相对于多进程编程,多线程编程更加轻量级,线程之间的切换开销更小。
一个程序通常至少有一个主线程(Main Thread),它是程序的执行入口。主线程会按照顺序执行程序中的指令,可以创建和管理其他线程。除了主线程外,程序还可以创建额外的线程,这些线程可以并行执行不同的任务,或者在需要时进行协作。
线**程共享同一进程的资源,如内存空间、文件描述符和全局变量等。**这意味着多个线程可以访问和修改相同的数据,从而实现并发的数据处理和共享数据的通信。然而,由于线程之间的并发执行,必须谨慎处理线程之间的同步和互斥,以避免竞态条件和数据不一致性的问题。
线程的优点包括:
- 并发性:多个线程可以同时执行不同的任务,提高程序的并发性和响应性。
- 资源共享:线程可以共享进程的资源,避免资源的重复分配和浪费。
- 轻量级:相对于进程来说,线程的创建和切换开销更小。
然而,线程编程也面临一些挑战,如线程同步、数据共享和竞态条件等问题。合理地管理和同步线程之间的操作是多线程编程中需要注意的关键点。
线程的同步互斥机制
1.线程互斥锁
在多线程中,所有的线程共用同一个全局变量,但是多个线程在操作同一个变量的时候,就可能会出现冲突的现象,内核为了解决这一问题引入线程的互斥锁,获取到锁的线程可以操作变量,获取不到锁资源的线程阻塞等待,没有执行的先后顺序,谁抢占锁成功谁执行。
创建互斥锁
互斥锁的类型是 pthread_mutex_t ,所以定义一个变量就是创建了一个互斥锁并初始化:
pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER; // 初始化互斥锁
pthread_mutex_init()
使用这个互斥锁,需要对互斥锁进行初始化, 使用函数 pthread_mutex_init() 对互斥锁进行初始化操作,未进行初始化,则创建的互斥锁不会生效:
//第二个参数为 NULL,互斥锁的属性会设置为默认属性
pthread_mutex_init(&mtx, NULL);
pthread_create 函数
创建一个线程
int WINPTHREAD_API pthread_create(pthread_t *th, const pthread_attr_t *attr, void ( func)(void *), void *arg);
函数参数的解释如下:
th
是一个指向pthread_t
类型的指针,用于存储新创建的线程的标识符。attr
是一个指向pthread_attr_t
类型的指针,用于指定新线程的属性。可以使用默认属性,传递NULL
。func
是一个指针,指向一个函数,该函数将在新线程中执行。该函数应该具有void *
类型的参数,并返回void *
类型的值。这个函数可以执行线程所需的任何操作。arg
是一个指针,传递给func
函数作为参数。
pthread_join 函数
int WINPTHREAD_API pthread_join(pthread_t t, void **res);
用于等待指定的线程终止,并检索线程的返回值。
t
是一个pthread_t
类型的变量,表示要等待的线程的标识符。res
是一个指向指针的指针,用于存储线程的返回值。线程的返回值是通过这个指针间接传递的。
函数的返回类型是 int
,表示函数的执行结果。如果成功等待线程的终止,返回值为 0;如果发生错误,返回一个非零的错误代码。
这个函数的作用是等待指定的线程终止。它会暂停当前线程的执行,直到指定的线程完成执行。在等待期间,当前线程将阻塞,直到指定的线程终止为止。
当目标线程终止时,pthread_join
函数将线程的返回值存储在 res
指向的指针中。通过这种方式,可以获取目标线程的返回值,并在需要时进行处理。
Simple 执行结果不是交替的,多循环几次就看到了
//
// Created by Kunsir on 2023/8/7.
//
#include "stdio.h"
#include "pthread.h"
#include "string.h"
void *Thread1(void *args)
{
for(int i = 0; i < 1000; i ++)
{
printf("哈哈哈哈\n");
}
return "Thread1 执行成功";
}
void *Thread2(void *args)
{
for(int i = 0; i < 1000; i ++) {
printf("嘻嘻嘻嘻\n");
}
return "Thread2 执行成功";
}
int main()
{
int res;
pthread_t myThread1, myThread2;
void *threadResult;
res = pthread_create(&myThread1, NULL, Thread1, NULL);
if (res) {
printf("线程 1 创建失败\n");
return 0;
}
res = pthread_create(&myThread2, NULL, Thread2, NULL);
if (res){
printf("线程 2 创建失败\n");
return 0;
}
// 阻塞主线程直到 myThread1 线程执行结束,threadResult 用来接收返回值,阻塞状态才解除
res = pthread_join(myThread2, &threadResult);
printf("%s\n", (char*)threadResult);
// 阻塞主线程直到 myThread2 线程执行结束,threadResult 用来接收返回值,阻塞状态才解除
res = pthread_join(myThread1, &threadResult);
printf("%s\n", (char*)threadResult);
printf("主程序执行完毕\n");
return 0;
}
进程同步的概念
- 临界资源:在系统中有许多硬件和软件资源,如打印机、公共变量等,这些资源在一段时间内只允许一个进程访问或者使用,这种资源称之为临界资源。
- 临界区:作为临界资源,不论硬件临界资源还是软件临界资源,多个并发的进程都必须互斥地访问或者使用,这时候,把每个进程访问临界资源的那段代码称为临界区。
- 进程同步:进程同步是指多个相关进程在执行次序上的协调这些进程相互合作,在一些关键点上需要相互等待或者通信。通过临界区可以协调进程间的合作关系,这就是同步。
- 进程互斥:进程互斥是指当一个程序进入临界区使用临界资源时,另一个进程必须等待。当占用临界资源的进程退出临界区后,另一个进程才被允许使用临界资源。通过临界区可以协调程序间资源共享关系,就是进程互斥。进程互斥是同步的一种特例。
常用的线程上锁函数
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex, const struct timespec *restrict abs_timeout);
pthread_mutex_lock
pthread_mutex_lock
是 POSIX 线程库中的一个函数,用于获取互斥锁(mutex lock)。互斥锁是一种同步机制,用于保护共享资源,确保在任何给定时间内只有一个线程可以访问被保护的区域。
int pthread_mutex_lock(pthread_mutex_t *mutex);
参数说明:
mutex
:指向互斥锁的指针,用于对共享资源进行保护和同步。
pthread_mutex_lock
的工作流程如下:
- 如果互斥锁当前没有被其他线程占用,则线程成功获取互斥锁,继续执行后续代码。获取互斥锁的线程被称为拥有者。
- 如果互斥锁当前被其他线程占用,则线程将被阻塞,直到互斥锁被释放。线程将进入休眠状态,不会占用 CPU 资源。
- 一旦互斥锁被释放,被阻塞的线程将被唤醒,并且其中一个线程将成功获取互斥锁,成为新的拥有者。
互斥锁的作用是确保在任何给定时间内只有一个线程可以访问被保护的区域。当一个线程获得互斥锁后,其他线程将被阻塞,直到拥有互斥锁的线程释放锁。这样可以避免多个线程同时访问共享资源,从而保证数据的一致性和线程安全性。
需要注意的是,对于每个互斥锁,只有拥有它的线程才能释放它。一般情况下,线程应该在完成对共享资源的访问后调用 pthread_mutex_unlock
函数来释放互斥锁,以便其他线程可以获取它。
使用互斥锁时,应该遵循以下准则:
- 在访问共享资源之前,获取互斥锁。
- 访问共享资源。
- 在完成共享资源的访问后,释放互斥锁,以便其他线程可以获取它。
这样,通过互斥锁的获取和释放,可以保证共享资源的安全访问和同步。
pthread_mutex_unlock
pthread_mutex_unlock
是 POSIX 线程库中的一个函数,用于释放互斥锁(mutex lock)。互斥锁是一种同步机制,用于保护共享资源,确保在任何给定时间内只有一个线程可以访问被保护的区域。
int pthread_mutex_unlock(pthread_mutex_t *mutex);
参数说明:
mutex
:指向互斥锁的指针,需要被释放。
pthread_mutex_unlock
的工作流程如下:
- 当调用线程拥有互斥锁时,调用
pthread_mutex_unlock
函数将互斥锁释放。 - 一旦互斥锁被释放,其他线程可以通过调用
pthread_mutex_lock
来尝试获取互斥锁。
互斥锁的释放是为了允许其他线程获取它,以便访问共享资源。当一个线程完成对共享资源的访问后,应该调用 pthread_mutex_unlock
来释放互斥锁,以便其他线程可以获取它。
需要注意的是,只有拥有互斥锁的线程才能调用 pthread_mutex_unlock
来释放锁。如果一个线程尝试释放一个它没有拥有的互斥锁,行为是未定义的。
使用互斥锁时,应该遵循以下准则:
- 在访问共享资源之前,获取互斥锁。
- 访问共享资源。
- 在完成共享资源的访问后,释放互斥锁,以便其他线程可以获取它。
通过互斥锁的获取和释放,可以保证在任何给定时间内只有一个线程能够访问共享资源,从而实现数据的一致性和线程安全性。
pthread_cond_wait()
用于阻塞当前线程,等待别的线程使用pthread_cond_signal()
或pthread_cond_broadcast
来唤醒它 pthread_cond_wait()
必须与pthread_mutex
配套使用。pthread_cond_wait()
函数一进入wait
状态就会自动release mutex
。当其他线程通过pthread_cond_signal()
或pthread_cond_broadcast
,把该线程唤醒,使pthread_cond_wait()
通过(返回)时,该线程又自动获得该mutex
。
pthread_cond_signal
函数的作用是发送一个信号给另外一个正在处于阻塞等待状态的线程,使其脱离阻塞状态,继续执行.如果没有线程处在阻塞等待状态,pthread_cond_signal
也会成功返回。使用pthread_cond_signal
一般不会有“惊群现象”产生,他最多只给一个线程发信号。假如有多个线程正在阻塞等待着这个条件变量的话,那么是根据各等待线程优先级的高低确定哪个线程接收到信号开始继续执行。如果各线程优先级相同,则根据等待时间的长短来确定哪个线程获得信号。但无论如何一个pthread_cond_signal
调用最多发信一次。
但是pthread_cond_signal
在多处理器上可能同时唤醒多个线程,当你只能让一个线程处理某个任务时,其它被唤醒的线程就需要继续
wait
,而且规范要求pthread_cond_signal
至少唤醒一个pthread_cond_wait
上的线程,其实有些实现为了简单在单处理器上也会唤醒多个线程. 另外,某些应用,如线程池,pthread_cond_broadcast
唤醒全部线程,但我们通常只需要一部分线程去做执行任务,所以其它的线程需要继续wait.所以强烈推荐对pthread_cond_wait()
使用while
循环来做条件判断.
生产者消费者问题
//
// Created by Kunsir on 2023/8/8.
//
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#define BUFFER_SIZE 2
int buffer[BUFFER_SIZE];
int count = 0;
int in = 0;
int out = 0;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t empty = PTHREAD_COND_INITIALIZER;
pthread_cond_t full = PTHREAD_COND_INITIALIZER;
void *producer(void *arg) {
int i;
for (i = 0; i < 10; i++) {
pthread_mutex_lock(&mutex);
// sleep(1);
while (count == BUFFER_SIZE) {
pthread_cond_wait(&empty, &mutex);
}
buffer[in] = i;
in = (in + 1) % BUFFER_SIZE;
count++;
printf("Producer produced item: %d\n", i);
pthread_mutex_unlock(&mutex);
pthread_cond_signal(&full);
}
pthread_exit(NULL);
}
void *consumer(void *arg) {
int i;
for (i = 0; i < 10; i++) {
pthread_mutex_lock(&mutex);
// sleep(1);
while (count == 0) {
pthread_cond_wait(&full, &mutex);
}
int item = buffer[out];
out = (out + 1) % BUFFER_SIZE;
count--;
printf("Consumer consumed item: %d\n", item);
pthread_mutex_unlock(&mutex);
pthread_cond_signal(&empty);
}
pthread_exit(NULL);
}
int main() {
pthread_t producerThread, consumerThread;
pthread_create(&producerThread, NULL, producer, NULL);
pthread_create(&consumerThread, NULL, consumer, NULL);
pthread_join(producerThread, NULL);
pthread_join(consumerThread, NULL);
return 0;
}
三个经典同步问题
a.生产者-消费者(缓冲区问题)
产者一消费者问题(producer-consumerproblem)是指若干进程通过有限的共享缓冲区交换数据时的缓冲区资源使用问题。假设“生产者”进程不断向共享缓冲区写人数据(即生产数据),而“消费者”进程不断从共享缓冲区读出数据(即消费数据);共享缓冲区共有n个;任何时刻只能有一个进程可对共享缓冲区进行操作。所有生产者和消费者之间要协调,以完成对共享缓冲区的操作。
/*生产者进程结构:*/
do{
wait(empty) ;
wait(mutex) ;
add nextp to buffer
signal(mutex) ;
signal(full) ;
}while(1) ;
/*消费者进程结构:*/
do{
wait(full) ;
wait(mutex) ;
remove an item from buffer to nextp
signal(mutex) ;
signal(empty) ;
}while(1) ;
b.作者读者问题
读者一写者问题(readers-writersproblem)是指多个进程对一个共享资源进行读写操作的问题。
假设“读者”进程可对共享资源进行读操作,“写者”进程可对共享资源进行写操作;任一时刻“写者”最多只允许一个,而“读者”则允许多个。即对共享资源的读写操作限制关系包括:“读—写,互斥、“写一写”互斥和“读—读”允许。