8月4日建设部网站通报网站运营管理
多线程
- 线程
- 线程的优点
- C语言多线程
- 创建线程
- 终止线程
- 连接和分离线程
- 开启一个线程
- 最基本的多线程实现
- 开启两个线程
- 多线程进行协同运算
- 无参数传递的线程并发编程实例
- 简单参数传递的线程并发编程实例
- 结构体参数传递的线程并发编程实例
- 线程的连接编程实例
- 信号量同步进行写入
- 互斥信号量实现对临界资源操作
- 并发程序引起的共享内存的问题
在串口助手编程中,-k命令下需要实现等待接收message的同时可以发送键入message。但是,键入message使用的fgets()函数如果得不到键入就会一直等待,无法继续接收message,考虑采用多线程实现有键入则发送,否则一直等待接收message。
线程
线程在Unix系统下,通常被称为轻量级的进程,线程虽然不是进程,但却可以看作是Unix进程的表亲,同一进程中的多条线程将共享该进程中的全部系统资源,如虚拟地址空间,文件描述符和信号处理等等。但同一进程中的多个线程有各自的调用栈(call stack),自己的寄存器环境(register context),自己的线程本地存储(thread-local storage)。 一个进程可以有很多线程,每条线程并行执行不同的任务。
线程的优点
线程可以提高应用程序在多核环境下处理诸如文件I/O或者socket I/O等会产生堵塞的情况的表现性能。在Unix系统中,一个进程包含很多东西,包括可执行程序以及一大堆的诸如文件描述符地址空间等资源。在很多情况下,完成相关任务的不同代码间需要交换数据。如果采用多进程的方式,那么通信就需要在用户空间和内核空间进行频繁的切换,开销很大。但是如果使用多线程的方式,因为可以使用共享的全局变量,所以线程间的通信(数据交换)变得非常高效。
C语言多线程
多线程是多任务处理的一种特殊形式,多任务处理允许让电脑同时运行两个或两个以上的程序。一般情况下,两种类型的多任务处理:基于进程和基于线程。
基于进程的多任务处理是程序的并发执行。
基于线程的多任务处理是同一程序的片段的并发执行。
多线程程序包含可以同时运行的两个或多个部分。这样的程序中的每个部分称为一个线程,每个线程定义了一个单独的执行路径。
本教程假设您使用的是 Linux 操作系统,我们要使用 POSIX 编写多线程 C++ 程序。POSIX Threads 或 Pthreads 提供的 API 可在多种类 Unix POSIX 系统上可用,比如 FreeBSD、NetBSD、GNU/Linux、Mac OS X 和 Solaris。
创建线程
首先,c语言的多线程并发,需要用到 pthread.h 库。
#include <pthread.h>
For example: - pthread_t thrd1; - pthread_attr_t attr; - void thread_function(void argument); - char *some_argument;pthread_create(&thrd1, NULL, (void *)&thread_function, (void *) &some_argument)
线程创建函数包含四个变量,分别为: 1. 一个线程变量名,被创建线程的标识 2. 线程的属性指针,缺省为NULL即可 3. 被创建线程的程序代码 4. 程序代码的参数
创建一个POSIX 线程:
pthread_create (thread, attr, start_routine, arg)
pthread_create 创建一个新的线程,并让它可执行。
参数 | 描述 |
---|---|
thread | 指向线程标识符指针。 |
attr | 一个不透明的属性对象,可以被用来设置线程属性。您可以指定线程属性对象,也可以使用默认值 NULL。 |
start_routine | 线程运行函数起始地址,一旦线程被创建就会执行。 |
arg | 运行函数的参数。它必须通过把引用作为指针强制转换为 void 类型进行传递。如果没有传递参数,则使用 NULL。 |
创建线程成功时,函数返回 0,若返回值不为 0 则说明创建线程失败。
终止线程
pthread_exit(void *retval); //retval用于存放线程结束的退出状态
终止一个 POSIX 线程:
#include <pthread.h>
pthread_exit (status)
pthread_exit 用于显式地退出一个线程。通常情况下,pthread_exit() 函数是在线程完成工作后无需继续存在时被调用。
如果 main() 是在它所创建的线程之前结束,并通过 pthread_exit() 退出,那么其他线程将继续执行。否则,它们将在 main() 结束时自动被终止。
连接和分离线程
pthread_create调用成功以后,新线程和老线程谁先执行,谁后执行用户是不知道的,这一块取决与操作系统对线程的调度,如果我们需要等待指定线程结束,需要使用pthread_join函数,这个函数实际上类似与多进程编程中的waitpid。 举个例子,以下假设 A 线程调用 pthread_join 试图去操作B线程,该函数将A线程阻塞,直到B线程退出,当B线程退出以后,A线程会收集B线程的返回码。 该函数包含两个参数:
pthread_t th //th是要等待结束的线程的标识
void **thread_return //指针thread_return指向的位置存放的是终止线程的返回状态。
pthread_join (threadid, status)
pthread_detach (threadid)
pthread_join() 子程序阻碍调用程序,直到指定的 threadid 线程终止为止。当创建一个线程时,它的某个属性会定义它是否是可连接的(joinable)或可分离的(detached)。只有创建时定义为可连接的线程才可以被连接。如果线程创建时被定义为可分离的,则它永远也不能被连。pthread_join() 函数来等待线程的完成。
开启一个线程
最基本的多线程实现
#include<stdio.h>
#include<stdlib.h>
#include<pthread.h>void* func(void *args){printf("hello\n");return NULL;
}int main(){pthread_t th;pthread_create(&th, NULL, func, NULL);pthread_join(th, NULL);return 0;
}
主要分为三步:
- 声明一个线程变量
th
,类型为pthread_t
; - 使用
pthread_create
函数创建,第一个参数是线程变量的地址,第三个参数是线程执行的函数 pthread_join
函数等待;
注意:pthread库不是Linux系统默认的库,连接时需要使用库libpthread.a, 在使用pthread_create创建线程时,在编译中要加-lpthread参数:
gcc xxx.c -lpthread -o xxx.o ./xxx
开启两个线程
#include<stdio.h>
#include<stdlib.h>
#include<pthread.h>void* func(void *args){int i;for(i=1; i<500; i++){printf("%d\n", i);}return NULL;
}int main(){pthread_t th1;pthread_t th2;pthread_create(&th1, NULL, func, NULL);pthread_create(&th2, NULL, func, NULL);pthread_join(th1, NULL);pthread_join(th2, NULL);return 0;
}
两个线程同时执行func
函数,错序打印1~499
如果用到pthread_create
函数的第4个参数,这个参数的传入会反应到func
中的形参中去。
void* func(void *args){int i;char *name = (char*)args;for(i=1; i<500; i++){printf("%s:%d\n", name, i);}return NULL;
}int main(){pthread_t th1;pthread_t th2;pthread_create(&th1, NULL, func, "th1");pthread_create(&th2, NULL, func, "th2");pthread_join(th1, NULL);pthread_join(th2, NULL);return 0;
}
输出的结果,我们可以清晰地看出th1和th2的线程标记和交错运行。
多线程进行协同运算
创建一个数组,其中有5000个元素,我们想用两个线程来共同计算这5000个元素的加法和。
int arr[5000];
int s1 = 0;
int s2 = 0;
void *func1(void *args){int i;char *name = (char *)args;for(i = 1; i < 2500; i++){s1 += arr[i];}return NULL;
}
void *func1(void *args){int i;char *name = (char *)args;for(i = 2500; i < 5000; i++){s2 += arr[i];}return NULL;
}
从两个线程的函数可以看出,一个线程计算前2500个值的加法和,另一个线程计算后2500个值的加法和。
int main(){int i;for(i = 0; i <5000; i++){arr[i] = rand() % 50;}pthread_ th1;pthread_t th2;pthread_create(&th1, NULL, func1, NULL);pthread_create(&th2, NULL, func2, NULL);pthread_join(th1, NULL);pthread_join(th2, NULL);printf("s1 = %d, s2 = %d, s1+s2 = %d\n", s1, s2, s1+s2);return 0;
}
main
函数中,在pthread_join
函数等待的th1
和th2
都结束后,输出对应的值。
代码里,th1和th2的执行函数中有大量的相似代码,所以我们最后用一个函数来复用。不难想到,需要通过传参的方式来实现代码复用。这里我们定义一个结构体,结构体中有循环的起始标记first,终止标记last,区间内加法和result。
typedef struct{int first;int last;int result;
}MY_ARGS;
int arr[5000];
void *func(void *args){int i;int s = 0;//参数强制类型转换MY_ARGS *my_args = (MY_ARGS *)args;for(i = my_args->first; i < my_args->last; i++){s += arr[i];}my_args->result = s;return NULL;
}
把func1
和fun2
整合到了func
中去。而在main
函数中,我们创建线程的时候传入的参数正是结构体指针:
int main(){int i;for(i = 0; i < 5000; i++){arr[i] = rand() % 50;}pthread_t th1;pthread_t th2;MY_ARGS args1 = {0, 2500, 0};MY_ARGS args2 = {2500, 5000, 0};pthread_create(&th1, NULL, func, &args1);pthread_create(&th2, NULL, func, &args2);pthread_join(th1, NULL);pthread_join(th2, NULL);printf("s1 = %d, s2 = %d, s1+s2 = %d\n", args1.result, args2.result, args1.result+args2.result);return 0;
}
这样在func
函数中,我们就可以对传入的结构体参数中的元素进行利用了,将计算所得传到结构体的result
中去。这样我们输出加法和,就可以得到跟上面一样的结果,但是代码会更整洁漂亮
无参数传递的线程并发编程实例
// 基于线程的并发编程
#include <stdio.h>
#include <pthread.h>
#define NUM_Threads 5// 线程的运行函数
void *PrintHello(void *arg)
{printf("Hello,World of Thread in C!\n");return 0;
}int main()
{int i;int ret;// 定义线程的id变量,多个变量使用数组pthread_t tids[NUM_Threads];for (i=0; i<NUM_Threads; i++){// 参数依次是: 创建的线程id,线程参数,调用的函数,传入的函数参数ret = pthread_create(&tids[i], NULL, PrintHello, NULL);if (ret != 0){printf("pthread_create error: error_code = \n");}}// 等各个线程推出后,进程才结束pthread_exit(NULL);return 0;
}/** 在CLion(Ubuntu)中输出结果为
Hello,World of Thread in C!
Hello,World of Thread in C!
Hello,World of Thread in C!
Hello,World of Thread in C!
Hello,World of Thread in C!* */
简单参数传递的线程并发编程实例
// 基于线程的并发编程,向线程传递参数1
// Test_2_createThread
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#define NUM_Threads 5// 线程的运行函数
void *PrintHelloId(void *threadid)
{// 对传入的参数进行强制类型转换,由无类型指针变为整形指针,然后再读取int tid = *((int *)threadid);printf("Hello,World, Thread %d\n",tid);return 0;
}int main()
{pthread_t pthreads[NUM_Threads];int i, rc;// 用数组存储i的数值int indexes[NUM_Threads];for (i=0; i<NUM_Threads; i++){printf("main() : 创建线程 %d \n",i);indexes[i] = i; // 保存i的数值// 在indexes传入参数的时候必须转换为无类型指针rc = pthread_create(&pthreads[i], NULL, PrintHelloId, (void *)&indexes[i]);if (0 != rc){printf("Error: 无法创建线程!\n");exit(-1);}}pthread_exit(NULL);return 0;
}/** 在CLion(Ubuntu)中输出结果是
main() : 创建线程 0
main() : 创建线程 1
Hello,World, Thread 0
main() : 创建线程 2
Hello,World, Thread 1
main() : 创建线程 3
Hello,World, Thread 2
main() : 创建线程 4
Hello,World, Thread 3
Hello,World, Thread 4* */
结构体参数传递的线程并发编程实例
// 基于线程的并发编程,向线程传递参数2(传递结构体)
// Test_3_createThread
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#define NUM_Threads 5typedef struct thread_data{int threadid;char message;
}THDATA,*PTHDATA;void * PrintHello(void * pthreadid)
{PTHDATA tid = (PTHDATA)pthreadid;printf("This is Pthread : %d ;info : %c \n",tid->threadid, tid->message);return 0;
}int main(void)
{pthread_t Pthread[NUM_Threads];THDATA index[NUM_Threads];int i, ret;for (i = 0; i < NUM_Threads; i++){printf("main() : 创建线程 %d \n",i);index[i].threadid = i;index[i].message = 'A'+i%10;ret = pthread_create(&Pthread[i], NULL, PrintHello, (void *)&index[i]);if (0 != ret){printf("Error: 创建线程失败!\n");exit(-1);}}pthread_exit(NULL);return 0;
}/** 在CLion(Ubuntu)中输出结果是
main() : 创建线程 0
main() : 创建线程 1
This is Pthread : 0 ;info : A
main() : 创建线程 2
main() : 创建线程 3
This is Pthread : 2 ;info : C
main() : 创建线程 4
This is Pthread : 3 ;info : D
This is Pthread : 4 ;info : E
This is Pthread : 1 ;info : B* */
线程的连接编程实例
// 基于线程的并发编程,连接或分离线程
// Test_4_createThread
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>#define NUM_Pthread 5void *PrintHello(void * pthreadid)
{int tid = *((int *)pthreadid);printf("Sleeping in thread %d ,...exiting \n",tid);return 0;
}int main(void)
{int i, ret;pthread_t Pthread[NUM_Pthread];pthread_attr_t attr; // 定义线程属性void * status;int index[NUM_Pthread];// 初始化并设置线程为可连接pthread_attr_init(&attr);pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);for (i=0; i<NUM_Pthread; i++){printf("main() : 创建线程 %d \n",i);index[i] = i;ret = pthread_create(&Pthread[i], NULL, PrintHello, (void *)&index[i]);}// 删除属性,并等待其他线程pthread_attr_destroy(&attr);for (i=0; i<NUM_Pthread; i++){ret = pthread_join(Pthread[i], status);if (0 != ret){printf("Error: unable to join,%d\n",ret);exit(-1);}printf("main(): complete thread id : %d",i);printf(" exiting with status : %p\n",status);}printf("main() : program exiting.\n");pthread_exit(NULL);return 0;
}
信号量同步进行写入
// 用信号量进行同步
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <semaphore.h>#define Len 100 // 设置输入内容长度sem_t bin_sem;
char work_area[Len]; // 存放输入内容void *Thread_func(void *arg)
{// 等待信号量有大于0的值然后退出sem_wait(&bin_sem);while (0 != strncmp("end", work_area, 3)){printf("Input %ld characters\n", strlen(work_area)-1);}return 0;
}int main(void)
{int res; // 存放命令的返回值pthread_t Pthread; // 创建线程void *thread_result; // 存放线程处理结果// 初始化信号量,并设置初始值为0res = sem_init(&bin_sem, 0, 0);if (0 != res){perror("Semaphore initialization failes");exit(EXIT_FAILURE);}// 创建新线程 0res = pthread_create(&Pthread, NULL, Thread_func, NULL);if (0 != res){perror("Thread creation failed");exit(EXIT_FAILURE);}printf("Enter 'end' to finish\n");// 当工作区内不是以end开头的字符串,则继续输入while (0 != strncmp("end", work_area, 3)){// 以标准输入获取输入到工作区内fgets(work_area, Len, stdin);sem_post(&bin_sem); // 信号量+1}printf("\n Waiting for thread to finish...\n");// 等待线程结束res = pthread_join(Pthread, &thread_result);if (0 != res){perror("Thread join failed");exit(EXIT_FAILURE);}printf("Thread joined\n");sem_destroy(&bin_sem); // 销毁信号量exit(EXIT_SUCCESS);return 0;
}
互斥信号量实现对临界资源操作
// 用互斥信号量进行同步
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <string.h>#define Len 3 // 自增计算次数
#define NUM_Pthread 5 // 设置线程的长度int count = 1; // 在数据段共享资源,多个进程抢占临界资源
// 对于临界资源,应该添加互斥锁
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;void *Thread_func(void *threadid)
{int tid = *((int *)threadid);int i, val;printf("Pthread ID : %d \n",tid);for (i=0; i<NUM_Pthread; i++){pthread_mutex_lock(&mutex);val = count;printf("val = %d \n",val++);count = val;pthread_mutex_unlock(&mutex);}return 0;
}int main(void)
{int res; // 存放命令的返回值int i;pthread_t Pthread[NUM_Pthread]; // 创建线程int index[NUM_Pthread];for (i=0; i<NUM_Pthread; i++){index[i] = i;// 创建线程res = pthread_create(&Pthread[i], NULL, Thread_func, (void *)&index[i]);if (0 != res){printf("Error: 创建线程失败!\n");exit(-1);}}for (i=0; i<NUM_Pthread; i++){// 汇合线程pthread_join(Pthread[i], NULL);}printf("count = %d\n",count);pthread_exit(NULL);return 0;
}// 在运行此程序无互斥锁时,我们不仅得到错误的答案,而且每次得到的答案都不相同
// 分析
// 当多个对等线程在一个处理器上并发运行时,机器指令以某种顺序完成,每个并发执行定义了线程中指令的某种顺序
并发程序引起的共享内存的问题
有两个进程,两个进程共享全局变量s。两个进程都执行一个计数功能的函数,直观地看过去,th1运行时s++要执行10000次,th2运行时s++也要执行10000次,似乎计算得到的最后s应该是20000。但实际上是这样的吗?
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
int s = 0;
void *func(void*args){int i;for(i = 0; i < 10000; i++){s++;}return NULL;
}
int main(){pthread_t th1;pthread_t th2;pthread_create(&th1, NULL, func, NULL);pthread_create(&th2, NULL, func, NULL);pthread_join(th1, NULL);pthread_join(th2, NULL);printf("%s = %d\n", s);return 0;
}
编译运行后,发现输出并不是20000,而是12657。
原因:当我们执行s++,底层发生的事件其实是:内存中读取s→将s+1→将s写入到内存。这不是一个原子化操作,当两个线程交错运行的时候,很容易发生结果的丢失。因此最后的结果肯定是要小于20000的。这种情况有种专有名词,叫race condition。
为了解决这个问题,我们可以加锁。
#include <pthread.h>
int s = 0;
pthread_mutex_t lock; //锁的声明
void *func(void *args){int i;for(i = 0; i < 10000; i++){ //给临界区代码加锁实现原子化操作pthread_mutex_lock(&lock);s++;pthread_mutex_unlock(&lock);}return NULL;
}
int main(){pthread_t th1;pthread_t th2;//锁初始化pthread_mutex_init(&lock, NULL);pthread_create(&th1, NULL, func, NULL);pthread_createe(&th2, NULL, func, NULL);pthread_join(th1, NULL);pthread_join(th2, NULL);printf("s = %d\n, s);return 0;
}
改进后的代码如下,学过操作系统会很好理解,无非就是为了保证共享内存区(临界区)的原子化操作,我们可以在进这段代码之前加锁(pthread_mutex_lock),意味着其他线程看到这段内存被其他人占有的时候,就不去抢占,等这段内存被解锁(pthread_mutex_unlock)之后,它才有读写这段临界区的权利。
但其实这种方式的执行速度并不快,比如这段代码里,每个线程都要进行10000次加解锁的操作,它能解决内存读写冲突的问题,但是却牺牲了效率。