Linux系统之 — 线程
- 线程介绍
- 线程使用
- 死锁(Deadlock)
- 竞态条件(Race Condition)
- 线程使用示例
- 服务器端代码示例
- 服务器端示例拆解
- 1. 引入头文件和宏定义
- 2. 定义全局变量
- 3. 定义线程函数
- 4. 主函数
- 5. 错误处理和资源释放
- 客户端代码示例
- 客户端示例拆解
- 1. 引入必要的头文件
- 2. 定义服务器的IP地址、端口和缓冲区大小
- 3. 主函数
- 4. 创建套接字
- 5. 错误检查
- 6. 设置服务器地址结构
- 7. 将点分十进制IP地址转换为二进制形式
- 8. 连接到服务器
- 9. 与服务器通信的循环
- 10. 用户输入
- 11. 退出条件
- 12. 发送数据到服务器
- 13. 接收服务器响应
- 14. 关闭套接字
- 15. 正常退出
线程介绍
在Linux系统中,线程是进程的一部分,是程序执行的最小单元。线程允许多个执行流程同时在同一个进程中运行,共享相同的内存空间和资源。
-
线程的定义:
- 线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运行单位。
-
线程与进程的区别:
- 进程拥有独立的内存地址空间,而线程共享同一进程的内存地址空间。
- 进程间通信(IPC)需要特定的机制,如管道、消息队列等,而线程间通信可以直接通过共享内存进行。
-
线程的调度:
- 线程由Linux内核的调度器进行调度,调度器根据线程的优先级和调度策略来决定线程的执行顺序。
- 调度策略可以是静态优先级调度、动态优先级调度等。
-
线程的栈:
- 每个线程都有自己的栈空间,用于存储局部变量和调用栈信息。
-
线程的局限性:
- 由于线程共享同一进程的地址空间,一个线程的崩溃可能导致整个进程的崩溃。
- 线程之间的同步和通信如果处理不当,可能会导致死锁、竞态条件等问题。
-
线程的实现:
- Linux内核支持两种线程实现方式:NPTL(Native POSIX Thread Library)和LinuxThreads。NPTL是较新的实现,提供了更好的性能和兼容性。
Linux线程是操作系统提供的一种并发执行机制,它允许在单个进程的上下文中运行多个执行流。线程共享进程的资源,如内存空间、文件描述符等,这使得线程之间的通信和数据共享变得非常高效,因为它们不需要通过进程间通信机制来交换信息。
线程的轻量级特性意味着它们比进程更易于创建和销毁,这减少了系统资源的消耗,并且可以更快地响应任务切换。这种快速的创建和销毁能力使得线程非常适合用于需要快速执行和频繁切换的应用程序,例如网络服务器和图形用户界面。
线程的并发执行能力提高了系统的效率,因为它允许多个任务同时进行,而不是顺序执行。这在处理大量并发请求或执行可以并行处理的计算任务时尤其有用。
线程还提供了同步机制,如互斥锁和条件变量,这些机制允许线程在需要时同步它们的执行,以避免数据竞争和其他并发问题。这对于需要保证数据一致性和顺序操作的应用程序至关重要。
在多核处理器上,线程可以被操作系统调度到不同的处理器核心上,实现真正的并行处理。这可以显著提高程序的性能,尤其是对于那些可以分解为多个独立任务的应用程序。
然而,线程的使用也带来了一些挑战,如线程安全问题、死锁和资源竞争。开发者需要仔细设计程序,以确保线程安全并避免这些问题。这通常涉及到使用锁、信号量和其他同步原语来控制对共享资源的访问。
Linux线程是实现高效并发编程的强大工具,它通过允许多个任务在单个进程中并发执行,提高了应用程序的性能和响应性。
线程使用
Linux线程的使用涉及到多方面的知识,包括线程的创建、同步、通信以及管理。
-
线程创建:
在Linux中,可以使用pthread
库来创建线程。使用pthread_create()
函数可以创建一个新的线程。#include <pthread.h> void *thread_function(void *arg) { // 线程要执行的代码 } int main() { pthread_t thread_id; pthread_create(&thread_id, NULL, thread_function, NULL); pthread_join(thread_id, NULL); // 等待线程结束 return 0; }
-
线程同步:
线程同步是确保多个线程在访问共享资源时不会发生冲突。可以使用互斥锁(mutexes)来实现同步。pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; pthread_mutex_lock(&mutex); // 加锁 // 访问共享资源 pthread_mutex_unlock(&mutex); // 解锁
-
线程间通信:
线程间可以通过共享内存来通信。确保在访问共享内存时使用适当的同步机制来避免竞态条件。 -
线程属性设置:
使用pthread_attr_t
可以设置线程的属性,如栈大小、调度策略等。pthread_attr_t attr; pthread_attr_init(&attr); pthread_attr_setstacksize(&attr, 1024); // 设置栈大小为1024字节 pthread_create(&thread_id, &attr, thread_function, NULL); pthread_attr_destroy(&attr);
-
线程取消:
Linux线程可以被其他线程取消。使用pthread_cancel()
可以请求取消一个线程。pthread_cancel(thread_id); // 请求取消线程
-
线程退出:
线程可以使用pthread_exit()
来正常退出,并可以返回一个值给主线程。void *thread_function(void *arg) { // 线程执行的代码 pthread_exit(NULL); // 线程退出 }
-
线程清理:
使用pthread_cleanup_push()
和pthread_cleanup_pop()
可以注册一个清理函数,该函数在线程退出时被调用。void thread_cleanup(void *arg) { // 清理资源 } pthread_cleanup_push(thread_cleanup, NULL); // 线程代码 pthread_cleanup_pop(0); // 0表示正常退出,非0表示取消
-
线程局部存储:
线程可以有自己的局部存储,使用pthread_key_create()
创建一个键,用于访问线程特定的数据。pthread_key_t key; pthread_key_create(&key, NULL); pthread_setspecific(key, (void*)data); // 设置线程特定的数据 void *data = pthread_getspecific(key); // 获取线程特定的数据
-
线程调度:
线程的调度可以通过设置不同的调度策略来控制,例如SCHED_FIFO
或SCHED_RR
。 -
线程安全:
编写线程安全代码是使用线程时的一个重要考虑。确保所有共享资源都通过适当的同步机制进行保护。
使用线程时,需要考虑线程的生命周期管理、资源的同步访问以及线程之间的协作。正确地使用线程可以提高程序的性能和响应性,但同时也需要仔细设计以避免并发问题,如死锁、竞态条件等。
死锁和竞态条件是并发编程中常见的问题,它们都与多个线程或进程对共享资源的访问有关。
死锁(Deadlock)
死锁是指两个或多个线程在执行过程中因争夺资源而造成的一种僵局。当每个线程都持有一个资源并等待其他线程释放它们需要的资源时,如果没有适当的机制来解决这种循环等待,那么这些线程就会永远等待下去,无法继续执行。
死锁的四个必要条件:
- 互斥:资源在一段时间内只能被一个线程使用。
- 占有和等待:线程至少持有一个资源,并且正在等待获取其他线程持有的资源。
- 不可剥夺:资源一旦被线程占有,就不能被其他线程强行剥夺,只能由占有它的线程主动释放。
- 循环等待:存在一个线程资源的循环等待链,每个线程都在等待下一个线程所占有的资源。
解决死锁的方法:
- 预防:通过设计来破坏死锁的必要条件之一。
- 避免:使用算法动态检测资源分配的安全性。
- 检测:允许死锁发生,然后检测并恢复。
- 解除:通过终止或回滚线程来释放资源。
竞态条件(Race Condition)
竞态条件发生在多个线程访问共享数据时,其执行顺序影响结果,而程序员并没有对这种顺序进行适当的同步。当多个线程试图同时修改同一变量或数据结构,并且最终结果依赖于这些修改的顺序时,就会发生竞态条件。
竞态条件的例子:
假设有两个线程同时增加一个全局计数器的值:
int counter = 0;
void increment() {
counter++; // 读取counter的值,增加1,然后写回
}
int main() {
pthread_t t1, t2;
pthread_create(&t1, NULL, (void*)increment, NULL);
pthread_create(&t2, NULL, (void*)increment, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
return counter; // 期望结果是2,但由于竞态条件,实际结果可能是1
}
解决竞态条件的方法:
- 互斥:使用互斥锁或其他同步机制,确保一次只有一个线程可以访问共享资源。
- 原子操作:使用原子操作来保证操作的不可分割性。
- 锁:使用读写锁等锁机制来控制对共享资源的访问。
线程使用示例
这是一个使用pthread
库在Linux上创建线程来处理TCP连接的简单示例。这个程序将创建一个服务器,它能够接受客户端的连接请求,并使用线程来处理每个连接。
目的:了解如何使用线程来并发处理多个TCP连接。每个线程独立地处理一个客户端连接,而主线程继续监听新的连接请求。这种模型适用于需要同时服务多个客户端的服务器应用程序。
服务器端代码示例
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#define PORT 8080
#define MAX_CLIENTS 5
// 互斥锁,用于同步对客户端数组的访问
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
// 客户端数组,存储活跃的客户端连接
int client_sockets[MAX_CLIENTS];
// 线程函数,用于处理客户端请求
void* handle_client(void* arg) {
int client_socket = *((int*) arg);
free(arg); // 释放分配给client_socket的内存
char buffer[1024];
while (1) {
int bytes_read = read(client_socket, buffer, sizeof(buffer));
if (bytes_read <= 0) {
// 客户端断开连接
close(client_socket);
pthread_mutex_lock(&mutex);
int i;
for (i = 0; i < MAX_CLIENTS; i++) {
if (client_sockets[i] == client_socket) {
client_sockets[i] = -1; // 标记为无效连接
break;
}
}
pthread_mutex_unlock(&mutex);
break;
}
// 简单的回显服务器,将接收到的数据发送回客户端
write(client_socket, buffer, bytes_read);
}
return NULL;
}
int main() {
int server_socket, client_socket;
struct sockaddr_in server_addr, client_addr;
socklen_t client_len = sizeof(client_addr);
// 创建TCP服务器套接字
server_socket = socket(AF_INET, SOCK_STREAM, 0);
if (server_socket < 0) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
// 绑定服务器地址
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(PORT);
if (bind(server_socket, (struct sockaddr*) &server_addr, sizeof(server_addr)) < 0) {
perror("bind failed");
exit(EXIT_FAILURE);
}
// 监听连接请求
listen(server_socket, MAX_CLIENTS);
printf("Server listening on port %d...\n", PORT);
while (1) {
client_socket = accept(server_socket, (struct sockaddr*) &client_addr, &client_len);
if (client_socket < 0) {
perror("accept failed");
continue;
}
// 锁定互斥锁以安全地添加新的客户端连接
pthread_mutex_lock(&mutex);
for (int i = 0; i < MAX_CLIENTS; i++) {
if (client_sockets[i] == -1) {
client_sockets[i] = client_socket;
break;
}
}
pthread_mutex_unlock(&mutex);
// 创建线程来处理客户端连接
pthread_t thread_id;
int* temp = malloc(sizeof(int)); // 临时存储client_socket
*temp = client_socket;
if (pthread_create(&thread_id, NULL, handle_client, temp) != 0) {
perror("pthread_create failed");
close(client_socket);
}
}
close(server_socket);
return 0;
}
说明:
初始化互斥锁:
pthread_mutex_t mutex
用于同步对client_sockets
数组的访问。创建服务器套接字:使用
socket()
函数创建一个TCP套接字。绑定地址:使用
bind()
函数将服务器地址绑定到套接字上。监听连接:使用
listen()
函数使服务器套接字进入监听状态。接受连接:使用
accept()
函数接受客户端的连接请求。处理客户端连接:为每个客户端连接创建一个新线程,使用
pthread_create()
函数。线程函数:
handle_client()
是线程执行的函数,它读取客户端发送的数据,并将其回显给客户端。互斥锁的使用:在添加新客户端到
client_sockets
数组时使用互斥锁来避免竞态条件。关闭连接:当客户端断开连接或发生错误时,关闭套接字并从数组中移除该客户端。
服务器端示例拆解
1. 引入头文件和宏定义
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#define PORT 8080
#define MAX_CLIENTS 5
这里引入了必要的头文件,用于处理套接字、线程和基本的输入输出。PORT
定义了服务器监听的端口号,MAX_CLIENTS
定义了服务器能够同时处理的最大客户端数量。
2. 定义全局变量
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
int client_sockets[MAX_CLIENTS];
这里定义了一个互斥锁mutex
,用于同步对client_sockets
数组的访问。client_sockets
是一个整型数组,用于存储当前活跃的客户端套接字。
3. 定义线程函数
void* handle_client(void* arg) {
int client_socket = *((int*) arg);
free(arg); // 释放分配给client_socket的内存
char buffer[1024];
while (1) {
int bytes_read = read(client_socket, buffer, sizeof(buffer));
if (bytes_read <= 0) {
// 客户端断开连接
close(client_socket);
pthread_mutex_lock(&mutex);
int i;
for (i = 0; i < MAX_CLIENTS; i++) {
if (client_sockets[i] == client_socket) {
client_sockets[i] = -1; // 标记为无效连接
break;
}
}
pthread_mutex_unlock(&mutex);
break;
}
// 简单的回显服务器,将接收到的数据发送回客户端
write(client_socket, buffer, bytes_read);
}
return NULL;
}
这是线程执行的函数handle_client
。它接收一个指向int
的指针作为参数,这个指针指向客户端的套接字描述符。函数首先将指针解引用并释放分配的内存。然后,它进入一个无限循环,不断读取客户端发送的数据,并将其回显给客户端。如果读取的字节数小于或等于0,表示客户端已经断开连接,此时关闭套接字,更新client_sockets
数组,并退出循环。
4. 主函数
int main() {
int server_socket, client_socket;
struct sockaddr_in server_addr, client_addr;
socklen_t client_len = sizeof(client_addr);
// 创建TCP服务器套接字
server_socket = socket(AF_INET, SOCK_STREAM, 0);
if (server_socket < 0) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
// 绑定服务器地址
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(PORT);
if (bind(server_socket, (struct sockaddr*) &server_addr, sizeof(server_addr)) < 0) {
perror("bind failed");
exit(EXIT_FAILURE);
}
// 监听连接请求
listen(server_socket, MAX_CLIENTS);
printf("Server listening on port %d...\n", PORT);
while (1) {
client_socket = accept(server_socket, (struct sockaddr*) &client_addr, &client_len);
if (client_socket < 0) {
perror("accept failed");
continue;
}
// 锁定互斥锁以安全地添加新的客户端连接
pthread_mutex_lock(&mutex);
for (int i = 0; i < MAX_CLIENTS; i++) {
if (client_sockets[i] == -1) {
client_sockets[i] = client_socket;
break;
}
}
pthread_mutex_unlock(&mutex);
// 创建线程来处理客户端连接
pthread_t thread_id;
int* temp = malloc(sizeof(int)); // 临时存储client_socket
*temp = client_socket;
if (pthread_create(&thread_id, NULL, handle_client, temp) != 0) {
perror("pthread_create failed");
close(client_socket);
}
}
close(server_socket);
return 0;
}
main
函数首先创建一个TCP套接字,并将其绑定到服务器的地址和端口上。然后,它监听端口上的连接请求。当接受到一个新的客户端连接时,accept
函数返回一个新的套接字描述符,该描述符用于与客户端通信。
接下来,代码使用互斥锁来确保对client_sockets
数组的访问是安全的。它检查数组以找到第一个空位,并将新的客户端套接字存储在那里。
然后,代码尝试创建一个新的线程来处理这个客户端。为此,它首先为客户端套接字分配内存,并将其作为参数传递给pthread_create
。如果线程创建失败,它将关闭客户端套接字并打印错误消息。
最后,主循环继续监听新的连接请求,而新创建的线程将独立地处理客户端通信。
5. 错误处理和资源释放
示例中包含了错误处理,例如在创建套接字、绑定地址、监听连接、接受连接和创建线程时检查错误。如果发生错误,将打印错误消息并适当地退出或继续执行。此外,当客户端断开连接时,示例会关闭套接字并释放相关资源。
客户端代码示例
客户端将展示如何使用套接字与服务器建立TCP连接,发送数据,并接收服务器的响应。
目的: 客户端程序是一个简单的回显客户端,它连接到服务器,发送消息,并接收服务器的回显响应。用户可以输入消息并发送,直到输入"exit"命令来退出程序。
该示例是基本的套接字通信过程,包括创建套接字、设置地址、建立连接、发送和接收数据以及关闭连接。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <arpa/inet.h>
#define SERVER_IP "127.0.0.1" // 服务器的IP地址
#define PORT 8080 // 服务器监听的端口号
#define BUFFER_SIZE 1024 // 数据缓冲区的大小
int main() {
int sock;
struct sockaddr_in server_addr;
char buffer[BUFFER_SIZE];
// 创建TCP客户端套接字
sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock == -1) {
perror("Could not create socket");
return 1;
}
// 设置服务器地址结构
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT);
if (inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr) <= 0) {
perror("Invalid address or Address family not supported ");
return 1;
}
// 连接到服务器
if (connect(sock, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
perror("Connection Failed");
close(sock);
return 1;
}
printf("Connected to the server.\n");
// 发送数据到服务器
while (1) {
printf("Enter message to send to server (or 'exit' to quit): ");
fgets(buffer, BUFFER_SIZE, stdin);
if (strcmp(buffer, "exit\n") == 0) {
break;
}
// 发送数据
if (send(sock, buffer, strlen(buffer), 0) < 0) {
perror("Send failed");
break;
}
// 接收服务器响应
if (recv(sock, buffer, BUFFER_SIZE, 0) < 0) {
perror("Receive failed");
break;
}
printf("Received from server: %s", buffer);
}
// 关闭连接
close(sock);
printf("Disconnected from the server.\n");
return 0;
}
说明:
引入头文件:包括了处理套接字和基本输入输出的头文件。
定义常量:包括服务器的IP地址、端口号和缓冲区大小。
创建套接字:使用
socket()
函数创建一个新的TCP套接字。设置服务器地址:使用
sockaddr_in
结构体设置服务器的IP地址和端口号。连接服务器:使用
connect()
函数尝试连接到服务器。输入循环:客户端进入一个循环,提示用户输入消息。如果用户输入"exit",则退出循环。
发送数据:使用
send()
函数将用户输入的消息发送到服务器。接收响应:使用
recv()
函数接收服务器回显的消息。错误处理:在创建套接字、解析地址、连接服务器、发送数据和接收数据的过程中,如果遇到错误,将打印错误消息并退出程序。
关闭套接字:在用户退出程序或发生错误时,关闭套接字以释放资源。
客户端示例拆解
1. 引入必要的头文件
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <arpa/inet.h>
头文件提供了创建套接字、进行网络通信以及处理字符串等所需的函数和数据结构。
2. 定义服务器的IP地址、端口和缓冲区大小
#define SERVER_IP "127.0.0.1" // 服务器的IP地址
#define PORT 8080 // 服务器监听的端口号
#define BUFFER_SIZE 1024 // 数据缓冲区的大小
宏定义提供了客户端连接服务器所需的参数。
3. 主函数
int main() {
// ...
}
main
函数是程序的入口点。
4. 创建套接字
int sock;
sock = socket(AF_INET, SOCK_STREAM, 0);
使用socket()
函数创建一个新的套接字。AF_INET
指定使用IPv4地址族,SOCK_STREAM
指定使用TCP协议。
5. 错误检查
if (sock == -1) {
perror("Could not create socket");
return 1;
}
如果套接字创建失败,将打印错误消息并返回非零值,表示程序异常退出。
6. 设置服务器地址结构
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT);
创建一个sockaddr_in
结构体并初始化,设置服务器的地址族和端口号。htons()
函数用于将主机字节序的端口号转换为网络字节序。
7. 将点分十进制IP地址转换为二进制形式
if (inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr) <= 0) {
perror("Invalid address or Address family not supported ");
return 1;
}
使用inet_pton()
函数将字符串形式的IP地址转换为网络字节序的二进制形式,并存储在server_addr
结构体中。
8. 连接到服务器
if (connect(sock, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
perror("Connection Failed");
close(sock);
return 1;
}
使用connect()
函数尝试连接到服务器。如果连接失败,打印错误消息,关闭套接字,并退出程序。
9. 与服务器通信的循环
char buffer[BUFFER_SIZE];
while (1) {
// ...
}
定义一个循环,用于持续接收用户输入并发送数据到服务器。
10. 用户输入
printf("Enter message to send to server (or 'exit' to quit): ");
fgets(buffer, BUFFER_SIZE, stdin);
提示用户输入消息,使用fgets()
函数从标准输入读取一行文本。
11. 退出条件
if (strcmp(buffer, "exit\n") == 0) {
break;
}
如果用户输入"exit",则退出循环。
12. 发送数据到服务器
if (send(sock, buffer, strlen(buffer), 0) < 0) {
perror("Send failed");
break;
}
使用send()
函数将用户输入的消息发送到服务器。如果发送失败,打印错误消息并退出循环。
13. 接收服务器响应
if (recv(sock, buffer, BUFFER_SIZE, 0) < 0) {
perror("Receive failed");
break;
}
printf("Received from server: %s", buffer);
使用recv()
函数接收服务器发送的数据。如果接收失败,打印错误消息并退出循环。如果接收成功,打印接收到的消息。
14. 关闭套接字
close(sock);
printf("Disconnected from the server.\n");
在通信结束后,关闭套接字并打印断开连接的消息。
15. 正常退出
return 0;
程序正常退出,返回零值。
客户端程序是一个简单的命令行工具,允许用户与服务器进行交互,发送消息并接收回显响应。
程序通过循环接收用户输入,直到用户输入"exit"命令。
程序中包含了必要的错误处理,确保在发生错误时能够给出清晰的提示并正确地清理资源。