线程与同步及流处理技术详解
线程的执行与中断
在多线程环境中,线程的执行顺序和状态变化是复杂的。例如,首先第一个线程启动并从 100 递减到 99,接着第二个线程启动,两个线程会交错执行一段时间,随后第三和第四个线程也会启动。在执行过程中,可能会出现线程被中断或中止的情况。如 Thread2 报告自己已被中止,然后报告正在退出;稍后,Thread1 报告自己被中断。中断操作需要等待线程进入等待状态,所以相较于调用 Abort 方法,中断的响应可能不会那么即时。最后,剩下的两个线程会继续执行直到完成,然后自然退出,而主线程在等待所有子线程结束后,会恢复执行并打印退出消息。
同步机制的必要性
在编程中,有时需要控制对资源(如对象的属性或方法)的访问,确保同一时间只有一个线程可以修改或使用该资源。可以将对象类比为飞机上的洗手间,各个线程就像排队等待使用洗手间的人。通过对对象加锁来实现同步,这样可以避免在第一个线程使用对象时,第二个线程强行进入。
共享资源模拟
为了演示同步机制,首先需要创建一个共享资源,这里使用一个简单的整数变量 counter,并将其初始化为 0。以下是相关代码:
int counter = 0;
然后修改 Incrementer 方法来递增 counter 变量:
public void Incrementer( )
{
try
{
while (counter < 1000)
{
int temp = counter;
temp++; // increment
// simulate some work in this method
Thread.Sleep(1);
// assign the Incremented value
// to the counter variable
// and display the results
counter = temp;
Console.WriteLine(
"Thread {0}. Incrementer: {1}",
Thread.CurrentThread.Name,
counter);
}
}
}
这个方法的思路是模拟对受控资源的操作。就像打开文件、操作其内容然后关闭文件一样,这里先将 counter 的值读取到临时变量中,递增临时变量,睡眠 1 毫秒模拟工作,然后将递增后的值赋回 counter。但这种方式存在问题,当第一个线程读取 counter 的值(0)并赋给临时变量,然后递增临时变量时,第二个线程可能也读取到 counter 的值(仍然是 0)并赋给另一个临时变量。第一个线程完成工作后将临时值(1)赋回 counter 并显示,第二个线程也会做同样的操作,最终输出的结果可能是 1,1,而不是期望的 1,2,3,4 依次递增。
完整示例代码
以下是模拟共享资源的完整代码:
using System;
using System.Threading;
namespace SharedResource
{
class Tester
{
private int counter = 0;
static void Main( )
{
// make an instance of this class
Tester t = new Tester( );
// run outside static Main
t.DoTest( );
}
public void DoTest( )
{
Thread t1 = new Thread(new ThreadStart(Incrementer));
t1.IsBackground = true;
t1.Name = "ThreadOne";
t1.Start( );
Console.WriteLine("Started thread {0}",
t1.Name);
Thread t2 = new Thread(new ThreadStart(Incrementer));
t2.IsBackground = true;
t2.Name = "ThreadTwo";
t2.Start( );
Console.WriteLine("Started thread {0}",
t2.Name);
t1.Join( );
t2.Join( );
// after all threads end, print a message
Console.WriteLine("All my threads are done.");
}
// demo function, counts up to 1K
public void Incrementer( )
{
try
{
while (counter < 1000)
{
int temp = counter;
temp++; // increment
// simulate some work in this method
Thread.Sleep(1);
// assign the decremented value
// and display the results
counter = temp;
Console.WriteLine(
"Thread {0}. Incrementer: {1}",
Thread.CurrentThread.Name,
counter);
}
}
catch (ThreadInterruptedException)
{
Console.WriteLine(
"Thread {0} interrupted! Cleaning up...",
Thread.CurrentThread.Name);
}
finally
{
Console.WriteLine(
"Thread {0} Exiting. ",
Thread.CurrentThread.Name);
}
}
}
}
同步机制介绍
为了解决上述共享资源访问的问题,有几种同步机制可供使用,下面分别介绍:
-
Interlocked 类
:CLR 提供了多种同步机制,其中 Interlocked 类专门用于处理递增和递减操作。它有 Increment 和 Decrement 两个方法,不仅可以递增或递减值,还能在同步控制下进行操作。修改 Incrementer 方法如下:
public void Incrementer( )
{
try
{
while (counter < 1000)
{
int temp = Interlocked.Increment(ref counter);
// simulate some work in this method
Thread.Sleep(0);
// display the incremented value
Console.WriteLine(
"Thread {0}. Incrementer: {1}",
Thread.CurrentThread.Name, temp);
}
}
}
Interlocked.Increment( ) 方法期望传入一个 int 类型的引用,由于 int 值是按值传递的,所以需要使用 ref 关键字。修改后,对 counter 成员的访问实现了同步,输出结果符合预期。
-
C# lock 语句
:虽然 Interlocked 对象适用于递增或递减值,但有时需要控制对其他对象的访问,这时可以使用 C# 的 lock 特性。lock 用于标记代码的关键部分,在锁生效期间为指定的对象提供同步。使用 lock 的语法是请求对一个对象加锁,然后执行一个语句或语句块,在语句块结束时释放锁。修改 Incrementer 方法使用 lock 语句:
public void Incrementer( )
{
try
{
while (counter < 1000)
{
int temp;
lock (this)
{
temp = counter;
temp++;
Thread.Sleep(1);
counter = temp;
}
// assign the decremented value
// and display the results
Console.WriteLine(
"Thread {0}. Incrementer: {1}",
Thread.CurrentThread.Name, temp);
}
}
}
使用 lock 语句的代码输出与使用 Interlocked 类的结果相同。
-
Monitor 类
:对于更复杂的资源控制需求,可以使用 Monitor 类。它允许开发者决定何时进入和退出同步状态,并可以等待代码的其他部分释放资源。当需要开始同步时,调用 Monitor 的 Enter 方法并传入要锁定的对象:
Monitor.Enter(this);
如果监视器不可用,说明受其保护的对象正在被使用。可以在等待监视器可用时执行其他工作,然后再次尝试。也可以显式调用 Wait 方法,暂停线程直到监视器可用且开发者调用 Pulse 方法。例如,在下载并打印网页文章的场景中,打印线程可以等待获取文件的线程发出足够文件已读取的信号。以下是使用 Monitor 类的示例代码:
using System;
using System.Threading;
namespace UsingAMonitor
{
class Tester
{
private long counter = 0;
static void Main( )
{
// make an instance of this class
Tester t = new Tester( );
// run outside static Main
t.DoTest( );
}
public void DoTest( )
{
// create an array of unnamed threads
Thread[] myThreads =
{
new Thread( new ThreadStart(Decrementer) ),
new Thread( new ThreadStart(Incrementer) )
};
// start each thread
int ctr = 1;
foreach (Thread myThread in myThreads)
{
myThread.IsBackground = true;
myThread.Start( );
myThread.Name = "Thread" + ctr.ToString( );
ctr++;
Console.WriteLine("Started thread {0}", myThread.Name);
Thread.Sleep(50);
}
// wait for all threads to end before continuing
foreach (Thread myThread in myThreads)
{
myThread.Join( );
}
// after all threads end, print a message
Console.WriteLine("All my threads are done.");
}
void Decrementer( )
{
try
{
// synchronize this area of code
Monitor.Enter(this);
// if counter is not yet 10
// then free the monitor to other waiting
// threads, but wait in line for your turn
if (counter < 10)
{
Console.WriteLine(
"[{0}] In Decrementer. Counter: {1}. Gotta Wait!",
Thread.CurrentThread.Name, counter);
Monitor.Wait(this);
}
while (counter > 0)
{
long temp = counter;
temp--;
Thread.Sleep(1);
counter = temp;
Console.WriteLine(
"[{0}] In Decrementer. Counter: {1}. ",
Thread.CurrentThread.Name, counter);
}
}
finally
{
Monitor.Exit(this);
}
}
void Incrementer( )
{
try
{
Monitor.Enter(this);
while (counter < 10)
{
long temp = counter;
temp++;
Thread.Sleep(1);
counter = temp;
Console.WriteLine(
"[{0}] In Incrementer. Counter: {1}",
Thread.CurrentThread.Name, counter);
}
// I'm done incrementing for now, let another
// thread have the Monitor
Monitor.Pulse(this);
}
finally
{
Console.WriteLine("[{0}] Exiting...",
Thread.CurrentThread.Name);
Monitor.Exit(this);
}
}
}
}
在这个示例中,decrementer 方法先启动,当 counter 的值小于 10 时,它会等待。只有当 Incrementer 方法调用 Pulse 方法后,decrementer 方法才会开始工作。
线程同步问题
在多线程编程中,还需要注意线程同步可能带来的问题,如竞态条件和死锁:
-
竞态条件
:当程序的成功依赖于两个独立线程的不受控完成顺序时,就会出现竞态条件。例如,一个线程负责打开文件,另一个线程负责写入文件,如果不控制第二个线程,可能会在第一个线程还未完成文件打开操作时,第二个线程就尝试写入文件,从而导致异常或程序崩溃。可以通过 Join 方法或使用 Monitor 类的 Wait 方法来解决这个问题。
-
死锁
:当线程等待资源释放时,可能会出现死锁情况,也称为死锁拥抱。例如,两个线程 ThreadA 和 ThreadB,ThreadA 锁定了一个 Employee 对象,然后尝试锁定数据库中的一行,而这一行已经被 ThreadB 锁定,所以 ThreadA 等待。同时,ThreadB 要更新这一行需要锁定 Employee 对象,但该对象已被 ThreadA 锁定,两个线程都无法继续执行,形成死锁。为避免死锁,有两个重要的准则:一是要么获取所有需要的锁,要么释放所有已持有的锁;二是尽量锁定最小的代码段,并尽可能短时间持有锁。
流处理概述
在许多应用程序中,数据通常存储在内存中,就像三维实体一样,可以通过名称直接访问变量或对象。但当需要将数据移入或移出文件、通过网络传输或在互联网上传输时,数据必须以流的形式进行处理。在流中,数据就像水流中的气泡一样流动。
流的基本概念
流的端点通常是一个后备存储,它为流提供数据源,就像湖泊为河流提供水源一样。后备存储通常是文件,但也可以是网络或 Web 连接。.NET 框架中的类对文件和目录进行了抽象,提供了创建、命名、操作和删除磁盘上文件和目录的方法和属性。
.NET 框架的流处理支持
.NET 框架提供了缓冲和非缓冲流,以及用于异步 I/O 的类。使用异步 I/O 时,可以指示 .NET 类读取文件,在它们从磁盘读取数据的同时,程序可以继续执行其他任务。异步 I/O 任务完成后会通知程序。这些异步类功能强大且稳定,有时可以避免显式创建线程。
流处理的应用场景
将数据流式传输到文件和从文件流式传输数据与通过网络进行流式传输没有本质区别。后续会介绍使用 TCP/IP 和 Web 协议进行流式传输的方法。为了创建数据流,通常需要对对象进行序列化,即将对象作为一系列位写入流中。.NET 框架提供了广泛的序列化支持,后续会详细介绍如何控制对象的序列化过程。
以下是流处理相关概念的关系图:
graph LR
A[数据] --> B[内存存储]
A --> C[流处理]
C --> D[后备存储]
D --> E[文件]
D --> F[网络连接]
D --> G[Web 连接]
C --> H[序列化]
H --> I[数据传输]
综上所述,线程同步和流处理是编程中非常重要的概念,掌握这些知识可以帮助开发者编写更高效、稳定的程序。在实际应用中,需要根据具体需求选择合适的同步机制和流处理方式,并注意避免竞态条件和死锁等问题。
线程与同步及流处理技术详解(续)
流处理的具体操作
在了解了流处理的基本概念和.NET 框架的支持后,下面来详细介绍一些流处理的具体操作。
文件流操作
文件流是流处理中最常见的场景之一。以下是一个简单的使用文件流读取和写入文件的示例代码:
using System;
using System.IO;
class FileStreamExample
{
static void Main()
{
// 写入文件
string filePath = "test.txt";
using (FileStream fs = new FileStream(filePath, FileMode.Create, FileAccess.Write))
{
string data = "Hello, World!";
byte[] bytes = System.Text.Encoding.UTF8.GetBytes(data);
fs.Write(bytes, 0, bytes.Length);
}
// 读取文件
using (FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read))
{
byte[] buffer = new byte[fs.Length];
fs.Read(buffer, 0, (int)fs.Length);
string readData = System.Text.Encoding.UTF8.GetString(buffer);
Console.WriteLine(readData);
}
}
}
上述代码中,首先使用
FileStream
类以创建和写入模式打开一个文件,将字符串数据转换为字节数组并写入文件。然后,以读取模式打开同一文件,将文件内容读取到字节数组中,并将其转换为字符串输出。
网络流操作
网络流处理在网络编程中非常重要。以下是一个简单的使用 TCP 协议进行网络流传输的示例:
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
class TcpClientExample
{
static void Main()
{
try
{
// 创建 TCP 客户端
TcpClient client = new TcpClient();
client.Connect(IPAddress.Parse("127.0.0.1"), 8888);
// 获取网络流
NetworkStream stream = client.GetStream();
// 发送数据
string message = "Hello, Server!";
byte[] data = Encoding.UTF8.GetBytes(message);
stream.Write(data, 0, data.Length);
// 接收数据
byte[] buffer = new byte[1024];
int bytesRead = stream.Read(buffer, 0, buffer.Length);
string response = Encoding.UTF8.GetString(buffer, 0, bytesRead);
Console.WriteLine("Received: " + response);
// 关闭连接
stream.Close();
client.Close();
}
catch (Exception ex)
{
Console.WriteLine("Error: " + ex.Message);
}
}
}
在这个示例中,创建了一个 TCP 客户端并连接到本地的 8888 端口。通过获取网络流,将数据发送到服务器,并接收服务器的响应。
序列化与反序列化
在流处理中,序列化和反序列化是将对象转换为字节流和将字节流转换回对象的过程。.NET 框架提供了多种序列化方式,下面介绍常见的二进制序列化和 XML 序列化。
二进制序列化
二进制序列化是将对象转换为二进制格式的字节流。以下是一个二进制序列化和反序列化的示例:
using System;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
[Serializable]
class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
class BinarySerializationExample
{
static void Main()
{
// 创建对象
Person person = new Person { Name = "John", Age = 30 };
// 序列化对象
using (FileStream stream = new FileStream("person.bin", FileMode.Create))
{
BinaryFormatter formatter = new BinaryFormatter();
formatter.Serialize(stream, person);
}
// 反序列化对象
using (FileStream stream = new FileStream("person.bin", FileMode.Open))
{
BinaryFormatter formatter = new BinaryFormatter();
Person deserializedPerson = (Person)formatter.Deserialize(stream);
Console.WriteLine("Name: " + deserializedPerson.Name + ", Age: " + deserializedPerson.Age);
}
}
}
在这个示例中,定义了一个
Person
类,并使用
[Serializable]
特性标记该类可序列化。通过
BinaryFormatter
类进行序列化和反序列化操作。
XML 序列化
XML 序列化是将对象转换为 XML 格式的文本。以下是一个 XML 序列化和反序列化的示例:
using System;
using System.IO;
using System.Xml.Serialization;
class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
class XmlSerializationExample
{
static void Main()
{
// 创建对象
Person person = new Person { Name = "Jane", Age = 25 };
// 序列化对象
using (FileStream stream = new FileStream("person.xml", FileMode.Create))
{
XmlSerializer serializer = new XmlSerializer(typeof(Person));
serializer.Serialize(stream, person);
}
// 反序列化对象
using (FileStream stream = new FileStream("person.xml", FileMode.Open))
{
XmlSerializer serializer = new XmlSerializer(typeof(Person));
Person deserializedPerson = (Person)serializer.Deserialize(stream);
Console.WriteLine("Name: " + deserializedPerson.Name + ", Age: " + deserializedPerson.Age);
}
}
}
同样,定义了一个
Person
类,使用
XmlSerializer
类进行 XML 序列化和反序列化操作。
流处理的性能优化
在进行流处理时,性能优化是一个重要的考虑因素。以下是一些常见的性能优化建议:
-
使用缓冲流
:缓冲流可以减少对底层资源的频繁访问,提高读写性能。例如,使用
BufferedStream
类对文件流或网络流进行包装:
using (FileStream fs = new FileStream("test.txt", FileMode.Open))
{
using (BufferedStream bs = new BufferedStream(fs))
{
// 进行读写操作
}
}
-
异步操作
:对于 I/O 密集型操作,使用异步方法可以避免阻塞线程,提高程序的响应性。例如,使用
FileStream的异步读写方法:
using (FileStream fs = new FileStream("test.txt", FileMode.Open))
{
byte[] buffer = new byte[1024];
await fs.ReadAsync(buffer, 0, buffer.Length);
}
- 合理设置缓冲区大小 :根据实际情况合理设置缓冲区的大小,避免过大或过小的缓冲区影响性能。
总结
线程同步和流处理是编程中不可或缺的重要技术。线程同步机制(如 Interlocked 类、C# lock 语句和 Monitor 类)可以帮助开发者解决多线程环境下的资源竞争问题,避免竞态条件和死锁的发生。而流处理则为数据的存储、传输和处理提供了强大的支持,通过.NET 框架提供的各种流类和序列化方式,可以方便地实现数据的读写、网络传输和对象的序列化与反序列化。同时,通过合理的性能优化措施,可以提高程序的性能和响应性。在实际开发中,开发者需要根据具体的需求和场景,灵活运用这些技术,编写出高效、稳定的程序。
以下是线程同步和流处理相关技术的总结表格:
| 技术类别 | 具体技术 | 作用 |
| ---- | ---- | ---- |
| 线程同步 | Interlocked 类 | 用于递增和递减操作的同步控制 |
| 线程同步 | C# lock 语句 | 标记代码关键部分,提供对象同步 |
| 线程同步 | Monitor 类 | 实现复杂的资源同步控制 |
| 流处理 | 文件流 | 实现文件的读写操作 |
| 流处理 | 网络流 | 实现网络数据的传输 |
| 流处理 | 序列化与反序列化 | 将对象转换为字节流和将字节流转换回对象 |
通过对这些技术的深入理解和应用,开发者可以更好地应对多线程和数据处理的挑战,提升程序的质量和性能。
超级会员免费看
6243

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



