推荐一个零声教育学习教程,个人觉得老师讲得不错,分享给大家:[Linux,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK等技术内容,点击立即学习: https://github.com/0voice
线程
线程是在互动之中产生的。如果事件之中只有自己一个人作为参与方,则是没办法有线程的概念。我主动与世界多方人员互动,并且等待世界给予自己反馈的时候,线程尤为重要,这关乎了效率问题,我们在等待的时间里是可以干别的事情的。著名的时间管理大师罗小猪(zhixiang)老师,他一天不过24小时,却能以超出常人的社交能力,一天能在多个女性朋友之间周旋。这就是线程的魅力,时间管理大师,效率!
线程锁
当我们在使用多线程编程的时候,我们就不是单纯的在写代码了,我们要理解我们电脑的运行规律。对器具本身进行细致了解。还要注意的是,是操作系统在管理实际线程的执行,这是存在许多的偶然性的。我们只是定义线程要执行什么任务,执行模式是怎样的。
我们的电脑的 CPU 是有寄存器这个元件的,它负责存储喂给 CPU 的数据和 CPU 的运算结果。可是存储是需要时间,尽管这段时间很短,但也相当于程序在休眠。如果我们的操作系统发现一个程序在休眠或等待响应,那么系统会安排别的线程去执行别的程序,而且刚好这个新的线程也是需要把数据存进 CPU 的寄存器。那么此时,情况就糟糕了,因为原来线程的工作就被破坏了,这项任务就如同被丢弃了一样。
我举一个简单的例子。
count ++ ;
第一步,先把count变量存储到CPU的内存之中(也就是寄存器);
第二步,CPU对寄存器的内容进行 inc 加和操作;
第三步,把经过运算的寄存器中的结果返还给count变量
在这个过程中,这三步都是互相分离的操作,很容易就因为运算需要时间的问题,而被系统调度其他线程来抢先执行下一步,导致了有些计算结果重复产生
为了解决该问题,我们便需要线程锁,来保护对应的程序运行过程中不会因为休眠而被打断。我们在本篇文章介绍两种线程锁,分别是自旋锁和互斥锁。
自旋锁:当一个线程获取该锁后,另一个线程想要进入该程序碰上了锁被前者所占用,那么它就一直在此处等待,自循环,等待前者运行成功了再说。等待的代价:占用内存+CPU,影响程序响应,慢。当内容简短,是一些赋值操作,确定性高的操作,就用自旋锁。
互斥锁:当一个线程获取该锁后,另一个线程想要进入该程序碰上了锁被前者所占用,那么他就会被系统切换出去,换下一个程序进来。切换线程的代价:实行上下文切换,寄存器要保存当前上下文的内容,而且有可能面临内存丢失,导致重新加载,CPU的调度其他线程又需要判断时间。当锁程序的内容大且复杂时候,尽量用互斥锁,叫线程去做其他的,这里要等很久。
多线程编程
在教科书里,推荐使用 <threads.h> 头文件用于多线程编程。我是在 Linux 系统下进行 C语言的编程,我有一个更好的选择,那就是利用头文件 <pthread.h> 进行多线程编程。同时,我们还必须有 POSIX 库文件可链接,否则无法创建线程。
当我们在 Linux 系统下安装 C 编译器的时候,这个头文件 <pthread.h> 和对应的 POSIX 库文件都已经系统地被安装了。Linux 在这方面是真的不错,文件系统的命名规范且统一,十分适合编程的工作。我们如果在 Linux 上下载了 C编译器 gcc,系统会按图索骥地帮我们把各个头文件和库文件都分别安排到一个地方去。我们可以先查看一下自己到底有没有这两个关键的文件。
1、查看自己是否有头文件 <pthread.h> 。
qiming@qiming:~/share/CTASK$ cd /
qiming@qiming:/$ cd /usr/include
qiming@qiming:/usr/include$ ls
查找到以下文件即可
pthread.h
2、查看自己是否拥有对应于线程的 POSIX 库。
qiming@qiming:~/share/CTASK$ cd /
qiming@qiming:/$ cd /usr/lib/x86_64-linux-gnu/
qiming@qiming:/usr/lib/x86_64-linux-gnu$ ls
查找到 这两个文件就说明我们有关于线程的 POSIX 库
libpthread.a
libpthread.so.0
代码案例
我们这里只介绍简单的线程锁使用案例。即写一个程序,让一个整型变量 0 按等差数列,每次加 1 地形式加到 1 百万。我们分 10 个线程去执行该操作,每个线程对同一个变量执行 + 1 操作共 10 万次。每次 + 1 操作完成后,都休眠一小段时间,目的就是营造多线程编程的环境,让操作系统自己去调度线程,高效执行程序。我们就是要观察到,线程锁是可以保护我们想要保护的程序。
我们采用条件编译的 C 代码技巧,分别展示自旋锁和线程锁的应用。
首先,先定义两把线程锁和宏定义线程个数
#include <stdio.h>
#include <pthread.h>
#include <unistd.h> //提供睡眠函数 usleep(以微妙算) 和 sleep(以秒算)
#define THREAD_COUNT 10
pthread_mutex_t mutex; // 这个锁是全局变量
pthread_spinlock_t spinlock; // 这个锁是全局变量
然后,条件编译 + 1 任务的三种形式。即分别是,不带线程锁,带互斥锁以及带自旋锁共 3 类,读者可自行选用其中一种。
void* thread_callback (void *arg) {
int *pcount= (int *) arg;
int i=0;
// 每个线程执行 100000 个任务(十万个);共有 10 个线程;总共执行1000000次,即百万并发
while (i++ < 100000) {
#if 1
(*pcount) ++ ; // 不带线程锁
#elif 0
//这种线程锁,是“互斥锁”,其他线程无法过来干扰。
pthread_mutex_lock(&mutex);
(*pcount) ++ ;
pthread_mutex_unlock(&mutex);
#else
//这种线程锁,是“自旋锁”,
//自旋锁:当一个线程获取该锁后,另一个线程想要进入该程序碰上了锁被前者所占用,那么它就一直在此处等待,自循环,等待他成功了再说
pthread_spin_lock(&spinlock);
(*pcount) ++ ;
pthread_spin_unlock(&spinlock);
#endif
usleep(1); //睡眠1微秒
}
}
最后,我们在主函数中分配线程任务。
int main () {
pthread_t threadid[THREAD_COUNT] = {0}; //它是用于标识线程ID的,现有十个线程,每个线程的ID都一样,还分不出
pthread_mutex_init(&mutex,NULL);
pthread_spin_init(&spinlock, PTHREAD_PROCESS_SHARED); //共用自旋锁
int i=0;
int count=0;
for (i=0; i< THREAD_COUNT; i++) {
pthread_create (&threadid[i], NULL, thread_callback, &count) ;
//这个 pthread_create 函数来自于一个线程库,编译的时候需要链接(第三方lib包),成功时,返回值为 0
//这个函数是创建线程,每个线程执行 thread_callback ( &count ) ,线程的属性是NULL,线程ID随时更新,反正就是会每个都不一样,每时每刻都不一样,标记他做到什么程度
//也必须注意到,创建线程的函数传入的应该是一个“函数指针”,以表达任务本身。
//for循环只负责激发线程的建立,至于这些任务的执行是交给操作系统去调度的,他去判断执行顺序,而非程序自己作主。这正合了那句“只有与他者的互动中,才产生了线程”,线程的命运不掌握在程序自己手上,而是有个大家长与外界代为交流。
}
for (i=0; i<100; i++) {
printf("count : %d\n", count);
sleep(1); //睡眠1秒,共分100次打印
}
for (i =0; i<10; i++) { // 这个函数是要截断,销毁线程的,不然所有线程都在干活,主线程就先自己结束了,就算后面所有线程都完成了,也无法通过主线程来反映。
pthread_join(threadid[i], NULL); // 在我们设定线程,并分配任务的时候,我们就已经制定了什么参数要传出,无需在此时指定什么参数要传出
} // pthread_join 函数的第二个参数,是一个二级指针,不能填写“&count”,这个仅仅是一级指针
printf("the end of count : %d\n", count);
}
代码执行情况
编译:我们发现命令行中加入了一个编译选项 “-lpthread”,这就是来源于前文的 POSIX 库文件 libpthread.a 和 libpthread.so.0 。
qiming@qiming:~/share/CTASK$ gcc -o Lock Lock_thread_ver1.c -lpthread
qiming@qiming:~/share/CTASK$ ./Lock
当我们不带自旋锁时候的条件编译。我们发现,有些任务被损毁了,导致最终不能加到 1 百万
count : 766016
count : 774802
count : 784678
count : 798696
count : 811186
count : 824455
count : 834803
count : 843404
count : 852960
count : 860290
count : 871305
count : 881571
count : 893986
count : 906008
count : 917633
count : 927705
count : 938811
count : 948331
count : 958753
count : 967810
count : 977084
the end of count : 987738
使用互斥锁版本的编译,线程程序的任务得到保护,顺利地加到 1 百万。
count : 810802
count : 819039
count : 830124
count : 840485
count : 849377
count : 860119
count : 867993
count : 880766
count : 890518
count : 901481
count : 910888
count : 918579
count : 928716
count : 939254
count : 949681
count : 958902
count : 968739
count : 975384
count : 982024
count : 986824
the end of count : 1000000
使用自旋锁版本的编译,线程程序的任务得到保护,顺利地加到 1 百万。但我们又发现了些许的不一样,打印只是打印到 29 万左右。我们程序中的打印函数最多执行 100 次,执行它的是主线程,而非我们的所设置线程。这揭示了,自旋锁版本的运行速度其实是很慢的,因为他会让线程自循环,在那里干等。主线程早就匆忙地完成了打印了。
// 前面的输出略去
count : 202340
count : 212737
count : 219991
count : 231512
count : 241189
count : 252629
count : 262103
count : 272330
count : 284668
count : 294268
the end of count : 1000000
故而,我们可以认为,互斥锁是更有优势的
10万+

被折叠的 条评论
为什么被折叠?



